JMicro是一個用Java語言實現的開源微服務全家桶,
源碼地址:https://github.com/mynewworldyyl/jmicro,
Demo地址:http://jmicro.cn/。
摘要
假設你已經按照前面分享的文章下載JMicro源碼並編譯成功。現在開始開發一個JMicro微服務,並通過Java客戶端及JS調用此微服務,Java支持同步和異步調用,JS目前只支持異步調用。
服務提供者和消費者模式
上圖是最基本的服務提供者和消費者關系圖,服務方實現服務接口,消費方通過接口使用服務,面向接口編程,服務方與消費方沒有直接依賴。另一方面,JMicro為了給消費方提供異步調用支持,在消費方與服務接口之間自動加入一個異步接口,以提供異步調用支持,如下圖:
在ISimpleRpc$JMAsyncClient接口中,每個方法都與ISimpleRpc中的方法一一對應,ISimpleRpc$JMAsyncClient完全由JMicro代碼生成工具自動生成,不需要服務實現方及消息方做任何額外工作,非常便。異步接口后綴$JMAsyncClient及異步方法名后綴JMAsync是框架固定后綴,所以寫代碼時盡量避免使用,以免混淆。
開發服務提供者
1)建立接口項目
因為ISimpleRpc接口同時被服務實現方及服務消費方依賴,所以需要單獨建立一個項目存放此接口
新建接口對應的Maven項目,並在POM加主如下依賴
<dependency> <groupId>cn.jmicro</groupId> <artifactId>api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
新建接口類如下所示
package cn.jmicro.example.api.rpc; import cn.jmicro.api.test.Person; import cn.jmicro.codegenerator.AsyncClientProxy; @AsyncClientProxy public interface ISimpleRpc { String hello(String name); String hi(Person p); String linkRpc(String msg); }
@AsyncClientProxy注解指示JMicro為此接口生成異步接口,即ISimpleRpc$JMAsyncClient,在此項目根目錄執行mvn clean install,成功后將可在src/main/gen目錄下找到cn/jmicro/example/api/rpc/genclient/ISimpleRpc$JMAsyncClient.java接口,及此接口實現類cn/jmicro/example/api/rpc/genclient/SimpleRpc$JMAsyncClientImpl.java,此實現類只用於服務提供方,消費方用不到。
2)新建服務提供者項目
新建一個Maven項目,並在POM文件加入如下依賴
<dependency> <groupId>cn.jmicro</groupId> <artifactId>all</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>cn.jmicro</groupId> <artifactId>example.api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
基中example.api即為上面建立的接口項目
新建服務接口實現類,如下:
1 package cn.jmicro.example.rpc.impl; 2 3 import java.util.Random; 4 5 import cn.jmicro.api.JMicroContext; 6 import cn.jmicro.api.annotation.Component; 7 import cn.jmicro.api.annotation.Reference; 8 import cn.jmicro.api.annotation.SBreakingRule; 9 import cn.jmicro.api.annotation.SMethod; 10 import cn.jmicro.api.annotation.Service; 11 import cn.jmicro.api.async.IPromise; 12 import cn.jmicro.api.monitor.MC; 13 import cn.jmicro.api.monitor.SF; 14 import cn.jmicro.api.service.IServiceAsyncResponse; 15 import cn.jmicro.api.test.Person; 16 import cn.jmicro.common.Constants; 17 import cn.jmicro.example.api.rpc.ISimpleRpc; 18 import cn.jmicro.example.api.rpc.genclient.IRpcA$JMAsyncClient; 19 20 @Service(namespace="simpleRpc", version="0.0.1", monitorEnable=1, maxSpeed=-1,debugMode=1, 21 baseTimeUnit=Constants.TIME_SECONDS, clientId=1000,external=true) 22 @Component 23 public class SimpleRpcImpl implements ISimpleRpc { 24 25 @Reference(namespace="rpca", version="0.0.1") 26 private IRpcA$JMAsyncClient rpca; 27 28 private Random r = new Random(100); 29 30 @Override 31 @SMethod( 32 //breakingRule="1S 50% 500MS", 33 //1秒鍾內異常超50%,熔斷服務,熔斷后每80毫秒做一次測試 34 breakingRule = @SBreakingRule(enable=true,percent=50,checkInterval=5000), 35 logLevel=MC.LOG_DEBUG, 36 testingArgs="[\"test args\"]",//測試參數 37 monitorEnable=1, 38 timeWindow=5*60000,//統計時間窗口5分鍾 39 slotInterval=100, 40 checkInterval=5000,//采樣周期2S 41 timeout=5000, 42 retryInterval=1000, 43 debugMode=1, 44 maxSpeed=1000, 45 baseTimeUnit=Constants.TIME_MILLISECONDS 46 ) 47 public String hello(String name) { 48 if(SF.isLoggable(MC.LOG_DEBUG)) { 49 SF.eventLog(MC.MT_PLATFORM_LOG,MC.LOG_DEBUG,SimpleRpcImpl.class, name); 50 } 51 /*int rv = r.nextInt(); 52 if(rv < 50) { 53 throw new CommonException("test breaker exception"); 54 }*/ 55 System.out.println("Server hello: " +name); 56 return "Server say hello to: "+name; 57 } 58 59 @Override 60 @SMethod( 61 //breakingRule="1S 50% 500MS", 62 //1秒鍾內異常超50%,熔斷服務,熔斷后每80毫秒做一次測試 63 breakingRule = @SBreakingRule(enable=true,percent=50,checkInterval=5000), 64 logLevel=MC.LOG_DEBUG, 65 testingArgs="[{\"username\":\"Zhangsan\",\"id\":\"1\"}]",//測試參數 66 monitorEnable=1, 67 timeWindow=5*60000,//統計時間窗口5分鍾 68 slotInterval=100, 69 checkInterval=5000,//采樣周期2S 70 timeout=5000, 71 retryInterval=1000, 72 debugMode=0, 73 maxSpeed=1000, 74 baseTimeUnit=Constants.TIME_MILLISECONDS 75 ) 76 public String hi(Person person) { 77 if(SF.isLoggable(MC.LOG_DEBUG)) { 78 SF.eventLog(MC.MT_PLATFORM_LOG,MC.LOG_DEBUG,SimpleRpcImpl.class, person.getUsername()); 79 } 80 return "Server say hello to: " + person.toString(); 81 } 82 83 @Override 84 public String linkRpc(String msg) { 85 if(SF.isLoggable(MC.LOG_DEBUG)) { 86 SF.eventLog(MC.MT_APP_LOG,MC.LOG_DEBUG,SimpleRpcImpl.class, "linkRpc call IRpcA with: " + msg); 87 } 88 89 System.out.println("linkRpc: " + msg); 90 //return this.rpca.invokeRpcA(msg+" linkRpc => invokeRpcA"); 91 92 //IPromise<String> p = PromiseUtils.callService(this.rpca, "invokeRpcA","linkRpc call IRpcA with: " + msg); 93 94 IPromise<String> p = this.rpca.invokeRpcAJMAsync("invokeRpcA"); 95 JMicroContext cxt = JMicroContext.get(); 96 if(cxt.isAsync()) { 97 IServiceAsyncResponse cb = cxt.getParam(Constants.CONTEXT_SERVICE_RESPONSE,null); 98 p.then((rst,fail) -> { 99 cb.result(rst); 100 }); 101 return null; 102 } else { 103 return p.getResult(); 104 } 105 106 } 109 }
此服務實現類包含微服務相關的很多細節,如限流,超時,重試,監聽,日志,熔斷,異步。 另外服務路由和負載均衡保持默認沒有額外配置。下面對每行代碼做概要解釋
從第5到18行是jmicro框架相關類導入,你可以看到,JMicro實現微服務核心應用沒有任何第三方依賴。
第20行Service注解提示此類是一個服務實現類,其屬性說明如下:
namespace和version分別是服務命名空間和版本號,服務名稱不需要指定,固定是服務接口名全稱,3個值有值構成服務的唯一標識,使用服務時需要通過此3個值。
monitorable:此服務是否可監控,啟用監控后,可以在后台管理頁面看到服務調用的實時細節,比如QPS,失敗數,成功數,超時數,及各指標占比等等;
maxSpeed:服務最大qps,用於服務限流;
debugMode:是否開啟調試模式,在調試模式下,RPC會輸出更多日志,鋪助問題排查。
baseTimeUnit:此服務與時間相關的屬性的基本時間單位,可以是微秒,毫秒,秒,分,小時等;
external:是否可通過Api網關供外部調用,如果false則只能內部調用,外部不能訪問;
clientId:原本是用於服務隔離,但現在使用過程中覺得不盡如人間,所以后面大概率做調整或去除。
第21行Component注解告訴JMicro容器,這個類是一個組件,在JMicro容器啟動時自動做實例化,以供其他組件使用,功能和Spring的Component注解類似。
23行為類聲明,實現ISimpleRpc服務接口
25行和26行,Reference聲明26行這個成員變量是一個服務引用,引用服務名稱空間和版本分別為rpca及0.0.1,IRpcA$JMAsyncClient是另外一個服務接口的異步接口引用,JMicro內部使用另外一個服務時,只要這兩行代碼就能完成,相當方便。
第31到45行為服務方法注解,其很多屬性和Service注解相同,JMicro優先使用SMethod注解的屬性,如果SMethod屬性為-1,則使用Service注解的同名屬性,如果SMthod對應屬性值為0表示禁用,1表示啟用。
breakingRule : 服務方法的熔斷規則,enable=true表示啟用熔斷規則;percent=50表示服務失敗百分比超過50%時,開啟服務熔斷器,從而禁用服務;checkInterval=5000表示當服務熔斷后,間隔多長時間做一次自動調用服務,其單位由baseTimeUnit確定。業界大部份微服務實現中,都定義一個半熔斷狀態,在半熔斷狀態中,開啟部份RPC請求,以測試服務是否可以使用,這樣會導致部份請求失敗。在JMicro中,服務迷熔斷后,JMicro自動開啟服務調用,從而使熔斷器斷繼收到服務RPC成功率數據,當失敗率小於percent時,熔斷器自動關閉,服務進入正常使用狀態,這樣完全不影響正常服務使用。
logLevel:此方法日志級別,debug,info,warn,error,nolog;
testArgs:熔斷器做自動測試時使用的參數,其值可以是JSON格式,也可以是JMIcro編碼密文,一般當需要通過Api網關調用時,需要人工查看時使用JSON,如果只是內部服務調用,則使用JMicro編碼比較高率。
timeWindow:JMicro監控為每個服務方法啟動一個環形“旋轉時鍾”,這個環被分為N個Slot,每個Slot所占時間長度為slotInterval,即時鍾的一個“滴答”,則timeWindow=slotInterval*N,在每個“滴答”時間片內產生的統計數據歸類到這個時間Slot里,當數據所屬時間片超出timeWindow時,Slot所代表的數據超時丟棄。這樣在timeWindow內的統計數據就能夠”平滑“過度而不出現”跳躍“現像。源代碼實現核心類路徑為:https://github.com/mynewworldyyl/jmicro/blob/master/api/src/main/java/cn/jmicro/api/monitor/ServiceCounter.java
checkInterval: 當需要周其性地采集數據時,此值表示采集周期,如熔斷器多久采集一次成功或失敗率,限速器多久統計一次服務QPS等
timeout: 服務超時時間,消費者等待服務提供者返回結果等待時間,一般針對同步RPC調用,對異步RPC調用無效,注意,JMicro中,同步和異步是指客戶端調用RPC服務方法的方式,不針對服務方法,因為服務方法可以同時支持同步調用和異步調用,沒有同步方法和異步方法這一說。
retryInterval:發生超時時,間隔多久做一次重試;
retryCnt:發生超時間,需要連續重試多少次,但不包括第一次,如retryCnt=2,則最多重試兩次,加上第一次,最多調用3次;
其他和Service相同屬性不再重復
第48,49行向監控服務器發一條日志信息,eventLog方法第1個參數表示事件類型,JMicro監控服務根據事件類型做統計和分發,
第76行即上一章測試使用的RPC方法,Person是一個自定義的實體類,其定義(省略getter setter等代碼)如下所示
@SO public final class Person { private String username ="Yeu"; private int id = 222; }
其被@SO注解,JMicro序列化工具會對全部類路徑下的@SO注解的類做序列化增強,以提高序列化類實例性能,這就是為什么每個服務啟動時需要加-javaagent:/home/ubuntu0/jmicro/resp/jmicro-agent-0.0.1-SNAPSHOT.jar參數的原因。
具體方式是通過javassist注入encode和decode兩個方法,但是運行過程中發現javassist占用大量內存,如下圖,按理說只是在JVM啟動時類加載過程中使用javassist,生成目標class后javassist應該釋放這些內存,但是現在即使JVM因內存不足而退出,這部份內存都沒能釋放!有知道原因的大神嗎?
JMIcro RPC方法支持的參數類型定義如下:
a.Java支持的8種基本數據類型極其封裝類型;
b. String類型,Date類型,ByteBuffer類型
c. 由a,b構成的數組類型,及由a,b為元素構成的List,Set,Map類型
d. 由a,b,c作為成員變量構成的自定義類類型,必須要SO注解;
e. 由a,b,c,d作為元素構成的數組,List,Set,Map類型;
第94到104行在一個服務方法里面調用另一個RPC服務方法,並且根據上下文的異步配置以決定使用同步或異步調用。
到此簡單的服務提供者開發完成,對使用到的一些重點細節做簡單解釋。
部署服務提供者
編譯
cd ${base_dir}/example/example.provider mvn clean install -Dmaven.test.skip=true
在${base_dir}\example\example.provider\target找到jmicro-example.provider-0.0.1-SNAPSHOT-jar-with-dependencies.jar文件待用
上傳Jar包
以http://jmicro.cn/環境為例,在瀏覽器中打開此頁面,右上角點LOGIN,輸入用戶名:jmicro,密碼:jmicro123 登陸系統,只有登陸后,才可以上傳Jar包及部署操作。
登陸后除了自己建立的數據外,請不要做刪除和停止類的操作,特別是配置信息,以免影響系統正常運行。
選 擇菜單 deployment -> resposotory --> ADD,如下圖
彈出如下對話框,選擇要上傳的Jar文件,一定要選對如圖路徑下的可執行Jar文件
輸入名稱,默認名稱和所選文件同名。如果倉庫中已經存在同名文件,上傳會失敗,所以一定要輸入一個不重名的名稱,下一步部署配置時需要用到此名稱。
按確定開始上傳,如下圖
上傳成功后,在資源列表中查看如下
配置部署描述
選擇deployment --> deploy desc --> ADD ,如下圖所示
彈出對話框,輸入如下圖所示配置信息,JAR文件即為剛才上傳成功的jar包文件名,勾選Enable表示啟用此部署,最后加-Xmx32m -Xms8m限制一下內存,否則會因內存申請失敗而部署失敗。其他的選項先不用填或保持默認即可,如下圖
按確定后,等待10秒左右,如下圖打開進程列表頁面,可以看到啟動了新的JVM進程
服務調用
我們現在開始從Web前端調用這個服務方法,選擇Monitor --》 Service菜單,如下圖
彈出側欄服務列表,如下圖,右上角選擇Refresh刷新一下列表,剛剛部署的服務才顯示出來
點選hi結點后,在編輯區打開此方法的配置,如下圖。這些服務配置和上面源代碼中Service及SMethod注解的屬性基本上一一對應,並且可以動態修改,實時生效。
比如啟用和禁用監控,debugMode等。
在打開的服務方法配置頁面並往下拖到最后,看到Testing選項卡,如下圖,在Testing Args框輸入 [{"username":"Zhangsan","id":"1"}],點擊選項卡右上角Start按鈕,方法返回值輸出在Testing Result框中,如圖:
可以按相同的操作步聚調用別的遠程方法,注意方法參數要輸正確,正確的參數請查看上面接口定義,也可以通過Github查看其他的接口源碼。
通過Java API調用遠程服務
以Maven為例,首先在POM文件中引入如下依賴,example.api為服務接口所在項目
<dependency> <groupId>cn.jmicro</groupId> <artifactId>gateway.client</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>cn.jmicro</groupId> <artifactId>example.api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
寫Java main函數直接調用RPC源始同步方法
public static void main(String[] args) {
ApiGatewayClient socketClient = new ApiGatewayClient(new ApiGatewayConfig(Constants.TYPE_HTTP,"jmicro.cn",80)); ISimpleRpc srv = socketClient.getService(ISimpleRpc.class,"simpleRpc", "0.0.1"); System.out.println(srv.hi(new Person())); }
ApiGatewayConfig為Api網關客戶端配置類,支持3個參數,分別為連接類型,支持Http和Socket,另外兩個為Api網關IP和端口。ISimpleRpc為服務接口(接口,不是實現類),通過Api網關客戶端取得此遠程接口代理實現類,然后就可以像本地調用一樣調用遠程方法,完全不用關注HTTP協議細節,更不用關注Socket底層細節,完全面向Java接口及自定義參數的方法調用,當然,原生的Java 8種基本數據類型及封裝類型也是無差別支持的。
異步方式調用
1 public static void main(String[] args) { 2 ApiGatewayClient socketClient = new ApiGatewayClient(new ApiGatewayConfig(Constants.TYPE_HTTP,"jmicro.cn",80)); 6 ISimpleRpc$JMAsyncClient srv = socketClient.getService(ISimpleRpc$JMAsyncClient.class,"simpleRpc", "0.0.1"); 7 srv.hiJMAsync(new Person()).then((val,fail) -> { 8 System.out.println("Hi: " +val); 9 //System.out.println(fail); 10 }); 11 12 JMicro.waitForShutdown(); 13 }
這次直接通過ISimpleRpc$JMAsyncClient.class調用getService方法獲取異步代理實例。第7行調用異步方法hiJMAsync並得到IPromise實例,調用IPromise的then方法,並傳入方法引用接收異步結果。異步響應式編程相關內容請自行網上查看相關資源。
第12行讓線程等待,不然JVM在RPC結果沒返回前就退出了,正常的服務進程不需要這行代碼。
JS前端調用
在HTML頭部引入JS文件
<script type="text/javascript" src="js/utils.js"></script> <script type="text/javascript" src="js/ws.js"></script> <script type="text/javascript" src="js/rpc.js"></script>
這幾個JS文件可以在源碼路徑下的mng.web/public/js下找到,需要在rpc.js文件中修改API網關IP和端口,如下
JS調用ISimpleRpc的hi方法
function hi(person) {
let req = {}; req.serviceName ="cn.jmicro.example.api.rpc.ISimpleRpc"; //服務接口全名 req.namespace = "simpleRpc";//服務名稱空間 req.version = "0.0.1";//服務版本 req.method = "hi"; //服務方法名稱 req.args = [person]; //服務參數,一定要包裝在一個數組里面
//jm.mng.callRpc返回一個Promise實例,異步返回結果
jm.mng.callRpc(req,jm.rpc.Constants.PROTOCOL_JSON, jm.rpc.Constants.PROTOCOL_JSON)
.then((msg)=>{
//異步返回結果 console.log(msg); alert(msg); }).catch(err =>{ throw err; }); }
//調用hi方法
hi({"username":"Zhangsan","id":"1"});
通過上面代碼可以看出,JS這種異步調用風格和Java異步調用風格非常相像。
nodejs中可以使用同樣的代碼實現調用JMicro服務的方法。