从零放弃学习 Spring - Java GRPC 和 Spring Boot

Java GRPC From Scratch

创建项目

直接使用 IDEA 创建一个项目,选择 Maven 构建系统,JDK 我一直用 GraalVM JDK 17。

依赖和构建插件

项目要用到的依赖有

  • 日志依赖 logback-classic,相对于 logback 和 slf4j
  • GRPC 依赖 grpc-netty、grpc-protobuf、grpc-stub
  • 以及 javax.annotation-api,GRPC 生成的代码用到了 javax 库注解,但是新版 JDK 默认不提供
  • lombok,这个依赖可以让 IDEA 自动添加

另外为了开发方便,还要在构建系统里加入自动编译 proto 文件的构建插件 protobuf-maven-plugin。

最后对 pom.xml 改动如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<properties>
...
<grpc.version>1.53.0</grpc.version>
<protobuf.version>3.21.7</protobuf.version>
</properties>
<dependencies>
...
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

项目代码

大概结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── pom.xml
├── src
│   └── main
│   ├── java
│   │   └── org
│   │   └── example
│   │   ├── Main.java
│   │   └── service
│   │   └── GreeterImpl.java
│   ├── proto
│   │   └── helloworld.proto
│   └── resources
│   ├── logback.xml
│   └── META-INF
│   └── MANIFEST.MF

helloworld.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

package hellogrpc;

option java_package = "com.example.grpc.generated";
option java_outer_classname = "HelloWorldProto";
option java_multiple_files = true;

service Greeter {
rpc SayHello (SayHelloReq) returns (SayHelloResp);
}

message SayHelloReq {
string name = 1;
}

message SayHelloResp {
string message = 1;
}

一个很经典的 helloworld.proto,介绍一下三个 option:

  • java_package 是当前文件通过 protoc 生成代码的包名
  • java_outer_classname 是当前文件通过 protoc 生成代码后的类名,如果不去定义会生成出很奇怪的名字,我推荐手动固定这个名字,例如 XxxProto
  • java_multiple_files 指定后会让各个生成的类都有独立文件,在例子里,如果不指定这个选项,所有的类都会在 HelloWorldProto 类之下

然后我们用 mvn compile 重新编译一次,让 protoc 在 target/generated-sources 下生成对应的代码。IDEA 可能没办法识别新生成的代码,如果你删掉 .idea 后重新打开项目,IDEA 是可以自动识别出 target/generated-sources 的,正确做法是打开 Module Settings,把 target/generated-sources/protobuf/grpc-javatarget/generated-sources/protobuf/java 设置成 Source Folder 并选中 For generated sources

实现 RPC

Java GRPC 服务实现是通过实现生成代码中的 XxxImplBase 抽象类来实现业务 RPC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
public class GreeterImpl extends GreeterGrpc.GreeterImplBase {

@Override
public void sayHello(SayHelloReq request, StreamObserver<SayHelloResp> responseObserver) {
if (log.isDebugEnabled()) {
log.info("sayHello: {}", request);
}

var reply = SayHelloResp.newBuilder()
.setMessage("Hello " + request.getName())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}

启动服务

这部分代码基本上是从网上学习来的,但和 Go GRPC 服务端流程差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
public class Main {
public static void main(String[] args) throws IOException, InterruptedException {
var server = Grpc.newServerBuilderForPort(50051, InsecureServerCredentials.create())
.addService(new GreeterImpl())
.build();
server.start();
log.info("Server started on {}", server.getPort());
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down server");
try {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}));
server.awaitTermination();
}
}

构建

IDEA 的 Build Artifacts

构建可以通过 IDEA 的 Build Artifacts 功能:

  1. 打开 Project Settings - Artifacts
  2. 添加一个 Jar - From modules with dependencies
  3. 指定 Main Class,然后点 OK
  4. 菜单 Build - Build Artifacts,然后在弹出窗口里双击 xxxx.jar

然后你会可以在项目下一个 out 目录里找到你构建的 jar 包。

maven-assembly-plugin

更推荐的做法是用 maven-assembly-plugin,在上篇博客里我提过构建方式,这里要用到的是 maven-assembly-plugin,它把依赖的 jar 解压重新构建,更重要的是它能直接集成到 Maven 里。

修改 pom.xml 支持插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

<build>
...
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>org.example.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assemble</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

descriptorRef 指定 jar-with-dependencies 才能打出包含依赖的 jar。

然后执行

1
mvn package

你就可以在 target 目录下看到一个 xxx-1.0-SNAPSHOT-jar-with-dependencies.jar 文件。

项目所有代码参考 grpc-from-scratch

GRPC with Spring Boot

通过上面内容我们可以知道 Java GRPC 开发是不需要 Spring Boot 的,但是接入 Spring Boot 的好处也是不用多介绍的。使用 Spring Boot 开发 GRPC 服务最方便的方式是 grpc-spring-boot-starter,详情参考官方文档,比较遗憾的是没办法选择 GRPC 版本。下面聊聊直接基于 Spring Boot 实现 GRPC。

自定义注解

自定义注解的目的是让实现 RPC 的服务能被作为 Bean 管理,这样服务代码也能通过自动装配使用。

1
2
3
4
5
6
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface GrpcService {
}

服务代码

1
2
3
4
5
@Slf4j
@GrpcService
public class GreeterImpl extends GreeterGrpc.GreeterImplBase {
...
}

在具体实现 RPC 方法上代码差异不大。

Spring Boot 集成

有了 Spring Boot 加持,就可以把配置也放进 application.properties 里了,只需要写一个 Configuration Bean 去承接配置的值。

如果没有指明的扫描的包的话,Spring Boot 启动时会扫描 SpringBootApplication 所在的包和它的子包。

最后就是创建出相应 Bean,比较简单的是拿到 ApplicationContext,然后使用 getBeansWithAnnotation 扫描所有的 RPC 服务类。

项目代码参考 spring-boot-grpc-demo

结尾

以我两个 demo 项目来说,GRPC 项目如果不通过 Spring Boot 项目构建,大概 jar 包大小在 14M 左右,加上 Spring Boot 后项目 jar 包来到 23M,我不太了解 Java 是不是在乎一个包的大小,不过 Spring 带来的能力还是值得那么 10M 左右的开销的吧。

前段时间我上线了一个 Java GRPC 项目就不得不用到 Spring Boot,原因也很扯。一开始直接裸项目编写的 GRPC 服务已经能通过 IDEA 运行测试通过了,结果一部署到环境里,发现开始报错,原因是我用了 BC(BouncyCastle) 加密库,IDEA 运行时直接通过 classpath 引入 BC 的 jar 文件,所以是可以通过 JCE 验证的,但是通过 maven-assembly-plugin 构建的 jar 包不可以,这里就要用到之前提过的 jar in jar(或者把 BC 的 jar 包放到 classpath 里),Spring Boot 的构建插件直接支持 jar in jar。还有一个原因是我的这个服务是 Native Image 构建,这里说来就复杂了,接下来会写篇博客专门介绍一下这个鬼东西,总结一句话就是如果用当前时间能下载到的最新版,是没办法构建包含了 BC 库的非 Spring Boot 应用的,回退几个大版本后可以成功构建。

参考