Java11實戰:模塊化的 Netty RPC 服務項目
作者:楓葉lhz
鏈接:https://www.jianshu.com/p/19b81178d8c1
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。
參考 [Java模塊系統]:https://www.oracle.com/corporate/features/understanding-java-9-modules.html
從 Java9 就引入了模塊化的新語法了。如果我們想在項目中使用 Java9 及以上的版本的話,模塊化是無法忽視的。它不像 Java8 的 lambda 表達式,我們可以不使用 lambda 這個新特性,仍然用老舊的 API 進行替代。但是模塊化就不同。我們甚至發現,新的版本里,
rt.jar
已經不存在了。JDK
的結構和基礎庫率先模塊化了。
模塊化 API
模塊化的背景和概念本篇文章就不概述了,讀者可以查看官方文檔或者通過網上不錯的博客進行了解。我們主要講解一下
module-info.java
里的一些配置含義。
自定義的 helo.service
模塊,包含了不少指令,我們分別進行介紹。
module helo.service { exports com.maple.netty.handler; exports com.maple.hello.service to hello.client; requires slf4j.api; requires io.netty.all; requires gson; requires hello.api; requires hello.common; uses com.google.gson.Gson; opens com.maple.hello; }
exports 和 exports to 指令
exports
指令用於指定一個模塊中哪些包對外是可訪問的。而 exports…to
指令則用來限定哪些模塊可以訪問當前模塊導出的類,通過逗號分隔可以指定多個模塊訪問當前模塊導出的類。這種方式稱為限定導出(qualified export
)。
require 指令
require
指令聲明一個模塊所依賴的其他模塊,在 Java9
之后,我們除了引入 Jar
包依賴后,如果想要使用它們,還需要在 module-info.java
中使用 require
聲明使用。
uses
指令
uses
指令用於指定一個模塊所使用的服務,使模塊成為服務的消費者。其實就是指定一個模塊下的某一個具體的類。
下面是 requires
和 uses
的 主要區別:
module hello.client { requires gson; uses com.google.gson.Gson; }
provides…with 指令
該指令用於說明模塊提供了某個服務的實現,因此模塊也稱為服務提供者。provides
后面跟接口名或抽象類名,與 uses
指令后的名稱一致,with
后面跟實現類該接口或抽象類的類名。
例如java.base
模塊里的其中一個聲明,with后面為前者的一個實現類。
provides java.nio.file.spi.FileSystemProvider with jdk.internal.jrtfs.JrtFileSystemProvider;
open, opens, opens…to 指令
Java9
之前,Java
類的屬性即使定義為 private
也是能夠被訪問到的,我們可以通過反射等手段達到目的。
Java9
模塊化推出的一個重要目的就是強封裝,可以完全的將不想暴露的類和屬性給保護起來。
默認情況下,除非顯式地導出或聲明某個類為 public
類型,那么模塊中的類對外部都是不可見的,模塊化要求我們對外部模塊以最小范圍進行暴露。
open
等相關的指令目的就是來限制哪些類或者屬性能夠通過反射技術訪問。
opens
指令
opens package 指定模塊某個包下的所有 public
類都能被其他模塊通過反射進行訪問。
opens ... to
指令
opens package to modules
指定某些特定的模塊才能去對當前 package
進行反射訪問。
open module
指令
外部模塊對該模塊下所有的類在運行時都可以進行反射操作。
open module hello.common { requires io.netty.all; exports com.maple.hello.common; exports com.maple.hello.common.netty; }
實戰:基於 Netty 的模塊化 RPC 服務例子
實戰部分將會項目將會基於最新的
Java11
版本,使用Maven
進行項目管理。Netty
作為網絡通訊框架。實現一個簡單的RPC功能,hello-client 將會通過 netty客戶端發送請求,netty服務端接收請求並返回處理結果。采用Gson
和Protobuf
對請求對象進行序列化/反序列化處理。網絡通訊采用 Reacter 模式,客戶端異步非阻塞模式請求。Netty層進行了TCP
粘包拆包的處理,心跳檢測和channel空閑檢測,channel斷線重連等功能。
本文實現的 RPC
例子,涵蓋了目前現有的基於Netty的RPC網絡通訊部分所有的技術點。
Maven 環境准備
編譯插件
我們需要對 maven-compiler-plugin
插件進行升級,以支持最新的 Java11
的字節碼版本(55),升級版本為最新版3.8.0
。
啟用 Java 11
語言支持
<properties> <maven.compiler.release>11</maven.compiler.release> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties>
編譯插件配置
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <!-- Fix breaking change introduced by JDK-8178012: Finish removal of -Xmodule Reference: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8178012 --> <executions> <execution> <id>default-testCompile</id> <phase>test-compile</phase> <goals> <goal>testCompile</goal> </goals> <configuration> <skip>true</skip> </configuration> </execution> </executions> <configuration> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> </configuration> </plugin>
工具鏈插件
這個插件主要是對Java11和Java8做兼容性選擇。由於現在Java版本更新很快,但是大部分項目還是基於 Java8 甚至更低版本。不適宜更改項目所有的環境變量,並將其指向JDK11的主目錄。使用 maven-toolchains-plugin
使您能夠輕松地使用各種環境。
創建 $HOME/.m2/toolchains.xml
<toolchains> <toolchain> <type>jdk</type> <provides> <version>11</version> <vendor>oracle</vendor> </provides> <configuration> <!-- Change path to JDK9 --> <jdkHome>/Library/Java/JavaVirtualMachines/jdk-11.jdk/Contents/Home</jdkHome> </configuration> </toolchain> <toolchain> <type>jdk</type> <provides> <version>8</version> <vendor>oracle</vendor> </provides> <configuration> <jdkHome>/Library/Java/JavaVirtualMachines/jdk-8.jdk/Contents/Home</jdkHome> </configuration> </toolchain> </toolchains>
注意:將配置文件中 <jdkHome>
更改為實際的 JDK
安裝 HOME
。
項目主POM 文件 添加 工具鏈插件
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-toolchains-plugin</artifactId> <version>1.1</version> <configuration> <toolchains> <jdk> <version>11</version> <vendor>oracle</vendor> </jdk> </toolchains> </configuration> <executions> <execution> <goals> <goal>toolchain</goal> </goals> </execution> </executions> </plugin>
構建項目 Java11-netty-demo
構建整個項目結構如下
├── hello-api │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── maple │ │ │ │ │ └── hello │ │ │ │ │ ├── HelloRequest.java │ │ │ │ │ └── HelloResponse.java │ │ │ │ └── module-info.java │ │ │ └── resources │ ├── hello-client │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── maple │ │ │ │ │ └── hello │ │ │ │ │ └── client │ │ │ │ │ ├── AppClient.java │ │ │ │ │ ├── Main.java │ │ │ │ │ ├── netty │ │ │ │ │ │ ├── NettyClient.java │ │ │ │ │ │ └── handler │ │ │ │ │ │ ├── RpcClientHandler.java │ │ │ │ │ │ ├── RpcClientMsgDecoder.java │ │ │ │ │ │ └── RpcClientMsgEncoder.java │ │ │ │ │ └── service │ │ │ │ │ └── HelloClient.java │ │ │ │ └── module-info.java │ │ │ └── resources │ │ │ └── logback.xml │ │ └── test │ │ └── java │ ├── hello-common │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── maple │ │ │ │ │ └── hello │ │ │ │ │ └── common │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── DumpUtil.java │ │ │ │ │ ├── RpcException.java │ │ │ │ │ └── netty │ │ │ │ │ └── RpcFrameDecoder.java │ │ │ │ └── module-info.java │ │ │ └── resources │ └── ├── hello-service │ ├── hello-service.iml │ ├── pom.xml │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── maple │ │ │ │ │ ├── AppServer.java │ │ │ │ │ ├── hello │ │ │ │ │ │ └── service │ │ │ │ │ │ ├── HelloService.java │ │ │ │ │ │ └── Person.java │ │ │ │ │ └── netty │ │ │ │ │ ├── NettySimpleServer.java │ │ │ │ │ └── handler │ │ │ │ │ ├── RpcLogHandler.java │ │ │ │ │ ├── RpcMsgDecoder.java │ │ │ │ │ ├── RpcMsgEncoder.java │ │ │ │ │ └── ServerHandler.java │ │ │ │ └── module-info.java │ │ │ └── resources │ │ │ └── logback.xml
從上面的 Tree
圖,我們可以看到項目分為四大模塊:
- hello-api API模塊,主要為
client
和service
共同依賴 - hello-common 公用的類模塊
- hello-client rpc客戶端模塊
- hello-service rpc服務端模塊
每個模塊src根目錄下都有一個 module-info.java
文件用來定義模塊
hello-api
module hello.api { exports com.maple.hello; }
hello-common
module hello.common { requires io.netty.all; exports com.maple.hello.common; exports com.maple.hello.common.netty; }
hello-client
module hello.client { requires hello.api; requires io.netty.all; requires slf4j.api; requires hello.common; requires gson; uses com.google.gson.Gson; }
hello-service
module helo.service { requires slf4j.api; requires io.netty.all; requires gson; requires hello.api; requires hello.common; }
以上 module-info.java
主要定義模塊的依賴關系和導出關系。
運行項目
通過上面幾步操作之后,我們便可以啟動項目運行。
首先我們啟動服務端,即 AppServer
,暴露 8000
端口
public class AppServer { public static void main(String[] args) { NettySimpleServer simpleServer = new NettySimpleServer(8000); simpleServer.start(); } }
然后我們啟動客戶端程序Main
,該程序簡單模擬控制台輸入作為請求
public static void main(String[] args) throws IOException { AppClient client = new AppClient(SERVER_URL, SERVER_PORT); logger.info("------ 歡迎進入JDK11的世界: 請輸入你的昵稱 --------- \n"); BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String name = in.readLine(); while (true) { try { logger.info("------ 請輸入任何你想輸入的內容: --------- \n"); Scanner scanner = new Scanner(System.in); if (scanner.hasNext()) { String msg = scanner.next(); int seq = SEQ_ID_ATOMIC.incrementAndGet(); CompletableFuture<HelloResponse> response = client.sendMessage(new HelloRequest(seq, name, msg)); response.whenComplete((result, ex) -> { if (ex != null) { logger.info(ex.getMessage(), ex); } logger.info("seq為 {} 的請求,服務端返回結果為:{}", seq, result.toString()); }); } else { int seq = SEQ_ID_ATOMIC.incrementAndGet(); CompletableFuture<HelloResponse> response = client.sendMessage(new HelloRequest(seq, name, "異常准備關閉")); response.whenComplete((result, ex) -> { if (ex != null) { logger.info(ex.getMessage(), ex); } logger.info("seq為 {} 的請求,服務端返回結果為:{}", seq, result.toString()); }); } } catch (Exception e) { logger.error(e.getMessage(), e); } } }
我們輸入內容后,馬上獲取到返回結果:

服務端處理日志:
09-28 00:51:57 271 netty-server-work-group-3-3 INFO - remote server /127.0.0.1:56546, channelRead, msg:com.maple.hello.HelloRequest@6400575a 09-28 00:51:57 271 netty-server-work-group-3-3 INFO - com.maple.hello.service.HelloService: 收到消息 seqId:2, request: com.maple.hello.HelloRequest@6400575a
一個簡單但功能齊全的基於 Netty
的例子演示成功。如果讀者對本例子感興趣,可以訪問如下地址獲取本項目源碼:
Java11-Netty-Demo: https://github.com/leihuazhe/Java11-Netty-Demo
遷移 Java11 注意事項
1. JavaEE 模塊被移除
Java11
移除了 JavaEE
模塊,所以很多諸如 javax JAXB 等已經被移除。
如果舊版本的項目有依賴 Javaee的組件,需要單獨加入 javaee-api
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> </dependency>
2.使用最新版本 Netty
由於在Java11中,JDK部分底層的類例如 Unsafe
等被移到了 jdk.internal
模塊。以及Java11 對模塊內類的保護,導致暴力反射訪問失效等問題。因此在最新的 Netty 版本中對這些做了優化了改變。
總結
本文首先從模塊化 API
及其功能說起,然后以實踐為目的介紹如何搭建基於Java11 的工程。通過一個基於 Netty
的案例工程來具體學習和深入模塊化的使用。
新版本的 Java11
對比 Java8
的改動個人感覺是有一點大的。如果我們要從一個以 Java8
甚至更低版本的項目遷移過來時,首先需要改變的就是一些依賴庫的變更,其次就是 模塊化的轉變,因此整個遷移還是需要考慮一定的兼容性。萬幸 Java11
是 Java
官方准備長期維護的一個版本,未來遷移到這個版本也是大勢所趨,后續博主將繼續跟進 Java11
的更多新特性。
本文例子源碼: Java11-Netty-Demo: https://github.com/leihuazhe/Java11-Netty-Demo
================== End