一個簡單的 dubbo demo 服務
來自專欄 分散式和人工智慧
原文參見一個簡單的 dubbo 服務
這篇文章將簡單的介紹如何寫一個 dubbo 服務。dubbo 的準備工作請參照前文。從此圖可知,當註冊中心和監控已經工作起來之後,我們需要寫的就是消費方和服務方的代碼了。一個簡單的一對一提供服務的代碼結構如下
~/IdeaProjects/dubbox/dubbo-demo master> lsdubbo-demo-api dubbo-demo-provider pom.xmldubbo-demo-consumer dubbo-demo.iml
其中 api 部分定義了 provider 和 consumer 都需要用到的介面,這樣 provider 實現這些介面,consumer 就可以像本地調用一樣來調用這些介面了。
介面定義與步驟
該 dubbo 服務將實現獲取許可權數組的功能,即調用PermissionService.getPermissions獲取許可權數組。在本文的例子中,我們將實現 1) 直接返回字元串的數組;2) 返回 java 對象(POJO) 的 json/xml 序列化的結果。
# 直接返回字元串的數組[permission_1, permission_2, permission_3]# 返回 json/xml 序列化的結果[ { "id": 1, "name": "x1_permission", }, { "id": 2, "name": "x2_permission", }]
協議分類
dubbo 的服務方和提供方通信支持多種協議,默認的是 dubbo 協議。官方給出的協議建議為:
協議優勢劣勢Dubbo採用NIO復用單一長連接,並使用線程池並發處理請求,減少握手和加大並發效率,性能較好(推薦使用)在大文件傳輸時,單一連接會成為瓶頸Rmi可與原生RMI互操作,基於TCP協議偶爾會連接失敗,需重建StubHessian可與原生Hessian互操作,基於HTTP協議需hessian.jar支持,http短連接的開銷大除此之外,噹噹還通過擴展 dubbo 得到 dubbox,支持了 HTTP 的 Rest 介面。關於 Rest 的優缺點參見 dubbox 的介紹。本文使用的就是 dubbox。
步驟
我們將循序漸進的實現一個獲取許可權數組的服務。代碼最終結果在 wwulfric/dubbodemo。
- 通過 dubbo 協議實現獲取許可權數組的服務
- 通過 rest 規範實現獲取許可權數組的服務
- 將提供方以服務的形式部署到伺服器
dubbo 協議簡例
代碼參見 protocol/dubbo 分支。
創建 maven 項目 dubbo-test,編輯 pom 文件,並仿照官方示例,在 maven 項目下分別創建 3 個 module:api, provider 和 consumer。
<!-- dubbo test 的 pom 文件直接用默認生成的即可 --><?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>dubbotest</groupId> <artifactId>dubbotest</artifactId> <version>1.0-SNAPSHOT</version></project>
創建 api module
通過 IDE 創建 api 子模塊(或者手動創建文件夾),並創建 api.PermissionService 類:
package api;import java.util.List;public interface PermissionService { List<String> getPermissions(Long id);}
文件夾結構如下:
.├── api│ ├── pom.xml│ └── src│ ├── main│ │ ├── java│ │ │ └── api│ │ │ └── PermissionService.java│ │ └── resources│ └── test...├── pom.xml└── src...
創建 provider module
用同樣的方式創建子模塊,並編輯 pom 文件,引入 dubbo 等必要的依賴包:
... <dependencies> <!-- 引入 api,在 provider 中實現 api 定義的介面 --> <dependency> <groupId>dubbotest</groupId> <artifactId>api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!-- 引入 dubbo --> <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo</artifactId> <version>2.8.4</version> </dependency> <!-- 引入 log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- 引入 zookeeper client --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency> </dependencies>...
阿里的 Dubbo 框架已經集成了 Zookeeper、Spring 等框架的依賴,但是有一個例外就是 zkclient,如果沒有引用將會拋出如下異常信息:
Exception in thread "main" java.lang.NoClassDefFoundError: org/I0Itec/zkclient/exception/ZkNoNodeException...
創建 provider.DemoProvider 類:
package provider;import java.io.IOException;public class DemoProvider { public static void main(String[] args) throws IOException { // 如果 spring 配置文件的位置是默認的,則可以直接這樣啟動服務 com.alibaba.dubbo.container.Main.main(args); }}
如果 spring 配置文件在默認的 resources/META-INF/spring 下,則可以直接這樣啟動服務,否則需要聲明指定其位置:
// 建議使用上一種方法package provider;import org.springframework.context.support.ClassPathXmlApplicationContext;import java.io.IOException;public class DemoProvider { public static void main(String[] args) throws IOException { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath*:META-INF/spring/*.xml"); System.out.println(context.getDisplayName() + ": here"); context.start(); System.out.println("服務已經啟動..."); System.in.read(); }}
創建 api 中定義 api.PermissionService 介面的實現類 provider.PermissionServiceImpl:
package provider;import api.PermissionService;import java.util.ArrayList;import java.util.List;public class PermissionServiceImpl implements PermissionService { public List<String> getPermissions(final Long id) { // 該函數應該實現 getPermissions 的業務邏輯,這裡簡單返回一個 list List<String> permissions = new ArrayList<String>(); permissions.add(String.format("Permission_%d", id - 1)); permissions.add(String.format("Permission_%d", id)); permissions.add(String.format("Permission_%d", id + 1)); return permissions; }}
創建 spring 配置文件,重點是 dubbo 相關服務的配置:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <!--定義了提供方應用信息,用於計算依賴關係;在 dubbo-admin 或 dubbo-monitor 會顯示這個名字,方便辨識--> <dubbo:application name="demotest-provider" owner="programmer" organization="dubbox"/> <!--使用 zookeeper 註冊中心暴露服務,注意要先開啟 zookeeper--> <dubbo:registry address="zookeeper://localhost:2181"/> <!-- 用dubbo協議在20880埠暴露服務 --> <dubbo:protocol name="dubbo" port="20880" /> <!--使用 dubbo 協議實現定義好的 api.PermissionService 介面--> <dubbo:service interface="api.PermissionService" ref="permissionService" protocol="dubbo" /> <!--具體實現該介面的 bean--> <bean id="permissionService" class="provider.PermissionServiceImpl"/></beans>
最後,在 resources 目錄下創建 log4j 的配置文件:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"><log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false"> <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="[%d{dd/MM/yy hh:mm:ss:sss z}] %t %5p %c{2}: %m%n" /> </layout> </appender> <root> <level value="INFO" /> <appender-ref ref="CONSOLE" /> </root></log4j:configuration>
文件創建完畢,接下來在根目錄下執行mvn clean package,然後執行 provider.DemoProvider 的 main 函數,即可啟動該服務了。查看 monitor/admin,看我們定義的 demotest-provider 這個名稱的提供者是否出現在服務列表中;或者在 console 輸出中發現如下信息,即表示成功啟動:
...[05/03/17 11:56:43:043 CST] main INFO container.Main: [DUBBO] Dubbo SpringContainer started!, dubbo version: 2.8.4, current host: 127.0.0.1[2017-03-05 23:56:43] Dubbo service server started!
provider 的文件結構如下:
.├── pom.xml├── src│ ├── main│ │ ├── java│ │ │ └── provider│ │ │ ├── DemoProvider.java│ │ │ └── PermissionServiceImpl.java│ │ └── resources│ │ ├── META-INF│ │ │ └── spring│ │ │ └── dubbotest-provider.xml│ │ └── log4j.xml│ └── test...└── target...
創建 consumer module
consumer 子模塊的創建和 provider 很像。pom 文件:
<dependencies> <dependency> <!-- 引入 api,在 consumer 中調用 api 定義的介面 --> <groupId>dubbotest</groupId> <artifactId>api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo</artifactId> <version>2.8.4</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency> </dependencies>
創建 spring 配置文件:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <dubbo:application name="demotest-consumer" owner="programmer" organization="dubbox"/> <!--向 zookeeper 訂閱 provider 的地址,由 zookeeper 定時推送--> <dubbo:registry address="zookeeper://localhost:2181"/> <!--使用 dubbo 協議調用定義好的 api.PermissionService 介面--> <dubbo:reference id="permissionService" interface="api.PermissionService"/></beans>
創建 consumer.DemoConsumer 類:
package consumer;import api.PermissionService;import org.springframework.context.support.ClassPathXmlApplicationContext;public class DemoConsumer { public static void main(String[] args) { //測試常規服務 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath*:META-INF/spring/*.xml"); context.start(); PermissionService permissionService = context.getBean(PermissionService.class); System.out.println(permissionService.getPermissions(1L)); }}
log4j 配置相同。
執行mvn clean package,然後執行 consumer.DemoConsumer 的 main 函數,即可啟動該服務了。查看 monitor/admin,看我們定義的 demotest-consumer 這個名稱的提供者是否出現在服務列表中;或者在 console 輸出中發現如下信息,即表示成功啟動(輸出的數組即為調用的結果):
...[Permission_0, Permission_1, Permission_2]
可見,在 consumer 中調用 provider 的實現,代碼上看起來和本地調用一樣,即 provider 相對於 consumer 來說是透明的。
consumer 文件結構如下:
.├── pom.xml├── src│ ├── main│ │ ├── java│ │ │ └── consumer│ │ │ └── DemoConsumer.java│ │ └── resources│ │ ├── META-INF│ │ │ └── spring│ │ │ └── dubbotest-consumer.xml│ │ └── log4j.xml│ └── test...└── target...
dubbo rest 介面
代碼參見 protocol/rest 分支。
需要在 api 中定義 rest 介面,並在 provider 中實現這個介面。
api module
添加 api.PermissionRestService 類:
package api;import javax.validation.constraints.Min;import java.util.List;public interface PermissionRestService { List<String> getPermissions(@Min(value = 1L, message = "User ID must be greater than 1") Long id);}
pom 文件添加依賴:
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.0.Alpha1</version> </dependency>
provider module
在 pom 文件中添加依賴:
<!--rest 規範,比如 Get, Path, MediaType 等--> <dependency> <groupId>javax.ws.rs</groupId> <artifactId>javax.ws.rs-api</artifactId> <version>2.0.1</version> </dependency> <!--使用 netty 啟動 rest 服務--> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-netty</artifactId> <version>3.0.7.Final</version> </dependency> <!--rest json 輸出--> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson-provider</artifactId> <version>3.0.7.Final</version> </dependency> <!--rest 需要的依賴--> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-client</artifactId> <version>3.0.7.Final</version> </dependency> <!--驗證--> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.2.0.Final</version> </dependency> <!--slf4j 和其依賴--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> </dependency>
其中 slf4j 的問題參見 stackoverflow。
創建 provider.PermissionRestServiceImpl 實現 上面的介面:
package provider;import api.PermissionRestService;import api.PermissionService;import com.alibaba.dubbo.rpc.RpcContext;import com.alibaba.dubbo.rpc.protocol.rest.support.ContentType;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.ws.rs.*;import javax.ws.rs.core.MediaType;import java.util.List;@Path("permissions")@Consumes({MediaType.APPLICATION_JSON})@Produces({ContentType.APPLICATION_JSON_UTF_8})public class PermissionRestServiceImpl implements PermissionRestService { private PermissionService permissionService; public void setPermissionService(PermissionService permissionService) { this.permissionService = permissionService; } @GET @Path("{id : \d+}") public List<String> getPermissions(@PathParam("id") Long id) { if (RpcContext.getContext().getRequest(HttpServletRequest.class) != null) { System.out.println("Client IP address from RpcContext: " + RpcContext.getContext().getRequest(HttpServletRequest.class).getRemoteAddr()); } if (RpcContext.getContext().getResponse(HttpServletResponse.class) != null) { System.out.println("Response object from RpcContext: " + RpcContext.getContext().getResponse(HttpServletResponse.class)); } // 上面是輸出相應的測試信息的,真實的實現只有下面這句 return permissionService.getPermissions(id); }}
編輯 spring 配置:
... <!--使用 netty 服務,將 rest 服務暴露在 4567 埠--> <dubbo:protocol name="rest" port="4567" threads="500" contextpath="services" server="netty" accepts="500" extension="com.alibaba.dubbo.rpc.protocol.rest.support.LoggingFilter"/> <!--使用 rest 規範實現定義好的 api.PermissionRestService 介面--> <dubbo:service interface="api.PermissionRestService" ref="permissionRestService" protocol="rest" validation="true"/> <!--具體實現該介面的 bean--> <bean id="permissionRestService" class="provider.PermissionRestServiceImpl"> <property name="permissionService" ref="permissionService"/> </bean>
啟動 DemoProvider 的 main 函數,此時輸入 http://localhost:4567/services/permissions/3.json 即可訪問提供者的 rest 介面了。
consumer module
編輯 DemoConsumer 類,添加 rest 調用:
public class DemoConsumer { public static void main(String[] args) { // 測試常規服務 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath*:META-INF/spring/*.xml"); context.start(); // dubbo 協議 PermissionService permissionService = context.getBean(PermissionService.class); System.out.println(permissionService.getPermissions(1L)); // rest 規範 PermissionRestService permissionRestService = context.getBean(PermissionRestService.class); System.out.println(permissionRestService.getPermissions(2L)); }}
在噹噹的 dubbox 文檔中,rest 調用分 3 種場景:
- 非 dubbo 的消費端調用 dubbo 的 REST 服務(non-dubbo –> dubbo)
- dubbo 消費端調用 dubbo 的 REST 服務 (dubbo –> dubbo)
- dubbo的消費端調用非 dubbo 的 REST 服務 (dubbo –> non-dubbo)
我們直接通過 rest 的 uri 調用就是第 1 種,上面實現的是第 2 種。注意到第 1 種調用實際上是直接訪問的地址,所以就不具備 dubbo 提供的服務發現功能了。
編輯 spring 配置,添加 permissionRestService:
<!--使用 dubbo 協議調用定義好的 api.PermissionRestService 介面--> <dubbo:reference id="permissionRestService" interface="api.PermissionRestService"/>
執行 DemoConsumer 的 main 函數,報錯:
java.lang.IllegalStateException: Unsupported protocol rest in notified url: ...
文檔中指明,這種調用方式必須把 JAX-RS 的 annotation 添加到服務介面上,這樣在 dubbo 在消費端才能共享相應的 REST 配置信息,並據之做遠程調用,編輯 api.PermissionRestService 類:
// 注意這裡編輯的是 api module 下的文件package api;import com.alibaba.dubbo.rpc.protocol.rest.support.ContentType;import javax.validation.constraints.Min;import javax.ws.rs.*;import javax.ws.rs.core.MediaType;import java.util.List;@Path("permissions")@Consumes({MediaType.APPLICATION_JSON})@Produces({ContentType.APPLICATION_JSON_UTF_8})public interface PermissionRestService { @GET @Path("{id : \d+}") List<String> getPermissions(@PathParam("id") @Min(value = 1L, message = "User ID must be greater than 1") Long id);}
同時需要在 api module 的 pom 文件下添加對應的依賴:
<dependencies> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>javax.ws.rs</groupId> <artifactId>javax.ws.rs-api</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo</artifactId> <version>2.8.4</version> </dependency> </dependencies>
再次mvn clean package,遇到錯誤 :
1. org.springframework.beans.factory.BeanCreationException...2. ERROR integration.RegistryDirectory: Unsupported protocol rest in notified url...3. ...
從該 issue 來看是 consumer 缺少相關依賴,添加上:
<!--使用 netty 啟動 rest 服務--> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-netty</artifactId> <version>3.0.7.Final</version> </dependency> <!--rest json 輸出--> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson-provider</artifactId> <version>3.0.7.Final</version> </dependency> <!--rest 需要的依賴--> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-client</artifactId> <version>3.0.7.Final</version> </dependency> <!--驗證--> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.2.0.Final</version> </dependency> <!--slf4j 和其依賴--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.5</version> </dependency>
再次執行,發現一切正常了,輸出的結果也是對的:
[Permission_0, Permission_1, Permission_2][Permission_1, Permission_2, Permission_3]
打包
代碼參見 package 分支。
按照 dubbo 推薦的方式打包成一個 .tar.gz 文件。在 provider 的 pom 文件中添加打包的依賴插件(或直接看最終結果 pom.xml):
<!-- 前面是 dependencies --> <build> <plugins> <plugin> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>unpack</id> <phase>package</phase> <goals> <goal>unpack</goal> </goals> <configuration> <artifactItems> <artifactItem> <groupId>com.alibaba</groupId> <artifactId>dubbo</artifactId> <version>2.8.4</version> <outputDirectory>${project.build.directory}/dubbo</outputDirectory> <includes>META-INF/assembly/**</includes> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptor>src/main/assembly/assembly.xml</descriptor> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
在 src/main 下創建文件夾 assembly,並創建相應文件,如下所示:
.├── pom.xml├── src│ ├── main│ │ ├── assembly│ │ │ ├── assembly.xml│ │ │ └── conf│ │ │ └── dubbo.properties│ │ ├── java...│ │ └── resources...│ └── test...└── target...
其中 dubbo.properties 留空即可,dubbo 的配置已經寫在了 spring 的配置中。assembly.xml 內容為:
<assembly> <id>assembly</id> <formats> <format>tar.gz</format> </formats> <includeBaseDirectory>true</includeBaseDirectory> <fileSets> <fileSet> <directory>${project.build.directory}/dubbo/META-INF/assembly/bin</directory> <outputDirectory>bin</outputDirectory> <fileMode>0755</fileMode> </fileSet> <fileSet> <directory>src/main/assembly/conf</directory> <outputDirectory>conf</outputDirectory> <fileMode>0644</fileMode> </fileSet> </fileSets> <dependencySets> <dependencySet> <outputDirectory>lib</outputDirectory> </dependencySet> </dependencySets></assembly>
完成之後,執行 maven 的清理和打包。
打包結果為 provider-1.0-SNAPSHOT-assembly.tar.gz,解壓,其文件結構為:
~/IdeaProjects/dubbotest/provider/target/provider-1.0-SNAPSHOT> lsbin conf lib logs
執行 bin/start.sh,查看 logs/stdout.log,並訪問 http://localhost:4567/services/permissions/3.json,確認啟動成功。
如此一來,一個簡單的 dubbo 服務搭建成功。
遠程調試
微服務化後,遠程調試必然是一個剛需。java 的遠程調試比較簡單,這裡就以 intellij 為例說明。
服務端配置
將打包好的服務 .tar.gz 傳到阿里雲伺服器上,並解壓,進入文件夾,編輯 bin/start.sh 文件:
# 調整調試的埠號if [ "$1" = "debug" ]; then JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n "fi
然後按照 start.sh 的提示,執行bin/start.sh debug,啟動服務。
intellij 配置
接下來就可以在 intellij 里打斷點調試了。
P.S.: 從 dubbo 包引入的腳本內存需求是 2G,伺服器可能不夠用,應調小對應數值;
P.S.: java8 打包的結果不能在 java7 上運行。如果遇到錯誤:
could not be instantiated: java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
請考慮將依賴的包全部以 jdk7 的 maven 重新安裝一遍,尤其是 dubbo 的。mac 下 java 環境的管理可以參考這篇文章;
P.S.: 如果你是從阿里雲的伺服器訪問另一台阿里雲的伺服器,可能會遇到 no route to host 錯誤,這種情況一般都是 ip 和 host 問題,注意阿里雲互相訪問需要使用它們的內網 ip。可以參考這個回答,no route to host 不是來自目標主機的回復,是網路問題。
推薦閱讀:
※Dubbo 新編程模型之外部化配置
※Spark Streaming場景應用-Kafka數據讀取方式
※有沒有人能對twitter的finagle和國內的dubbo做個對比?
※北京沙龍報名 | 關於Dubbo開源的那些事兒
※zookeeper在dubbo起了什麼作用