Service is defined using .proto \ Client app directly invokes server method on a different machine.
Generate java codes using protoc.
$ cd [project-root]/src/main/proto
$ protoc --proto_path=. --java_out=../java *.proto
Platform neutral, Language neutral data type. \ Serialize and Deserialize structured data.
message Person {
string name = 1;
int32 age = 2;
}
compile 하고나면 OuterClass가 나옴.. 에 \ 아니 근데 gralde 에서 설정만으로 바로 protoc 자동실행 못하나? \ 아래처럼 CLI 명령어 치는거 너무 추한데.. maven 쓰긴 싫고 아
You have to install proto compiler (protoc) on protocolbuffers github page
Register protoc bin file path on PATH
.
You can give options as shown below.
Place the java files from *.proto compiled by protoc in java directory
protoc --proto_path=. --java_out=../java *.proto
Place the source files from *.proto compiled by protoc into src/main/package/proto
constructor's access modifier is 'private'!
You should use builder pattern!
public final class Balance extends com.google.protobuf.GeneratedMessageV3 implements BalanceOrBuilder { // Builder 패턴만 적용 가능 // Use Balance.newBuilder() to construct. private Balance(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) { super(builder); }
private Balance() {
}
//..Embedding Builder class!
public static final class Builder extends {
public static Builder newBuilder() {
return DEFAULT_INSTANCE.toBuilder();
}
}
}
#### Client.java
```java
public class Client {
public static void main(String[] args) {
Balance.newBuilder()
.setAmount(10_000)
.build();
}
}
Protobuf is light-weight and faster than Json.
I tested code executing 1_000_000 times for running Balance
class serialize and deserialize \
The test result in as shown below.
Index | JSON | protobuf |
---|---|---|
provided by | jackson | Google protobuf |
Data type | JSON | byte stream |
Encoding/Decoding | Do | No need |
json 443 ms elapsed
protobuf 40 ms elapsed
10 x times better on protobuffer!
Data show format as shown below.
{
"amount": 10000
}
1=10000
Each property in protobuf has own tag
syntax = "proto3";
message Balance {
int32 amount = 1; // 1 is tag
}
Proto buffer's data size is also smaller than JSON.
JSON is key-value type on the other hand, protobuf only has tag. \ 1 ~ 15 tag number charge 1 byte. 16 ~ 2047 tag number charge 2 bytes. \ So it's recommended using 1 ~ 15 tag number.
in JSON, if sent empty collection deserialized as [] (if it's list) \ but in protobuf, sending empty collection, that is not sent.
📝 protobuf is smaller and faster and no need en/decoding than JSON.
reserved
keyword when removing fields will not break old proto reserved
keyword help not update newer value and providing information how it's reserved. syntax = "proto3";
enum ResolutionLevel {
HD = 0;
FHD = 1;
UHD = 2;
}
message Television {
string brand = 1;
reserved 2;
reserved "year", "model";
ResolutionLevel resolution = 3;
int64 price = 4;
}
Java Type | Proto Type |
---|---|
Collection / List | repeated |
Map | map |
Use 0 as default value It's important to keep in mind that the default value of 0 is only used if the field is not set.
syntax = "proto3";
message Product {
int64 serial_number = 1;
}
enum BodyStyle {
// enum 에서 0 은 default value 로 쓰여야한다.
UNKNOWN = 0;
SEDAN = 1;
COUPE = 2;
SUV = 3;
}
Proto type | Default value |
---|---|
any number type | 0 |
bool | false |
string | empty string |
enum | first value (0) |
repeated | empty list |
map | wrapper / empty map |
@DisplayName("Default value")
@Test
void testDefaultValue() {
User user = User.newBuilder().build();
assertThat(user.getAge()).isEqualTo(0); // default number value is zero
assertThat(user.hasAddress()).isEqualTo(false);
}
syntax = "proto3";
import "car.proto";
import "address.proto";
message User {
string name = 1;
int32 age = 2;
Address address = 3;
repeated Car car = 4;
}
We can use interface and concrete class in java on .proto using oneof
keyword. \
class created by protocol buffer has one's factory method. \
Refer to factory method pattern.
syntax = "proto3";
// This is interface having two concrete class
message Credentials {
oneof mode {
EmailCredentials emailMode = 1;
PhoneOtp phoneMode = 2;
}
}
message EmailCredentials {
string email = 1;
string password = 2;
}
message PhoneOtp {
int32 number = 1;
int32 code = 2;
}
class CredentialsTest {
private Credentials credentials;
private static void login(Credentials credentials) {
switch (credentials.getModeCase()) {
case EMAILMODE -> System.out.println(credentials.getEmailMode());
case PHONEMODE -> System.out.println(credentials.getPhoneMode());
}
}
@DisplayName("이메일 인증")
@Test
void testEmail() {
EmailCredentials emailCredentials = EmailCredentials.newBuilder()
.setEmail("falcon@tistory.com")
.setPassword("khazix123")
.build();
credentials = Credentials.newBuilder()
.setEmailMode(emailCredentials)
.build();
login(credentials);
}
}
In Protocol Buffers (.proto
files), the naming convention for properties is snake_case
Synchronous | Asynchronous |
---|---|
Blocking and wait for the response | Register a listner for callback. |
This is completely up to the client.
One to one request - response model
One request and multiple streaming response
When to use? response data size is big so required chunking and streaming from server to client instead of sending everything at once.
Client send multiple streaming requests but server only one response.
When to use? request large data to server (ex. file uploading) and server is required response only once.
This is not strictly one to one. \ ex) Send 30 requests and response 10.
when to use? Client and server need coordinate and work together
Separate client with stub vs server with interface.
Timeout for gRPC to complete. You can test this feature using
Uninterruptibles.sleepUninterruptibly()
on server sidewithDeadline(Deadline.after(2, TimeUnit.SECONDS))
on client sideI'll suggest example as shown below.
@Service
@RequiredArgsConstructor
public class DefaultBankServiceProxy implements BankService {
/** Test 3 wait seconds
*/
@Override
public BalanceDTO readBalance(BalanceVO balanceVO) {
// ..
Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);
return new BalanceDTO(balance);
}
}
@DisplayName("Deadline test 2 seconds but 3 sec blocking")
@Test
void getBalanceTestWithDeadLine() {
BalanceCheckRequest balanceCheckRequest = BalanceCheckRequest.newBuilder()
.setAccountNumber(5)
.build();
assertThrows(StatusRuntimeException.class, ()->{
Balance balance = this.bankServiceBlockingStub
.withDeadline(Deadline.after(2, TimeUnit.SECONDS)) // 2 second deadline.
.getBalance(balanceCheckRequest);
System.out.println(balance);
});
Handle cross-cutting concerns
Interceptor intercepts client request
Define class implementing ClientInterceptor
or ServerInterceptor
and register it each side.
public class DeadlineInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
// if custom deadline is set, pass it.
if (callOptions.getDeadline() != null) return next.newCall(method, callOptions);
return next.newCall(method, callOptions.withDeadline(Deadline.after(2, TimeUnit.SECONDS)));
}
}
You can use Context for handling in interceptor or transferring to Service It's safe to use the Context. Which has thread local context current RPC can only get data.
In gRPC, the context plays a critical role in providing metadata, cancellation signals, and deadlines for a gRPC call.
In interceptor, Context is not used explicitly. However, it's used implicitly to carry the call-specific information, including deadline and metadata between the client and server.
@Slf4j
public class RequestInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
String requestId = headers.get(Metadata.Key.of("request_id", Metadata.ASCII_STRING_MARSHALLER));
if (Objects.isNull(requestId)) {
Status invalidStatus = Status.UNAUTHENTICATED.withDescription("Should input request_id");
call.close(invalidStatus, headers);
}
// ThreadLocal current thread only the information
// it's safe to use
Context context = Context.current().withValue(
ServerHeaders.CTX_REQUEST_ID,
requestId
);
return Contexts.interceptCall(context, call, headers, next);
// return next.startCall(call, headers);
}
}
⚠️ gRPC server sees the last service definition, so last intercept is called first.
Server server = ServerBuilder.forPort(6443) // ⚠️ interceptors are executed in reverse order .intercept(new AuthInterceptor()) .intercept(new RequestInterceptor()) .addService(bankService) .build();
Standard exception (e.g. IllegalArgumentException) is apt to many developers. \
If standard one is enough to delivery meaning to client. Don't make custom error exception classes.
CustomError can be managed by GlobalExceptionHandler(*ControllerAdvice) and can reduce stacktrace memory cost by overriding fillInStackTrace()
as shown below
class CustomError extends RuntimeException {
@Override
public synchronized Throwable fillInStackTrace() {
// don't require calling super.fillInStackTrace() recursively.
return this;
}
}
Refer to this blog post
Both are used to send additional information. In terms of HTTP/2 both are headers. \ However, there's some difference. \ Metadata is available to both the client and server at all times during the lifecycle of the call. \ On the other hand, trailers can be sent only from server to client after the request has been processed and response has been sent. \ Trailers is useful when to send response size, checksum, error message, status code etc..