原文參見一個簡單的 dubbo 服務
這篇文章將簡單的介紹如何寫一個 dubbo 服務。dubbo 的準備工作請參照前文。
從此圖可知,當註冊中心和監控已經工作起來之後,我們需要寫的就是消費方和服務方的程式碼了。一個簡單的一對一提供服務的程式碼結構如下
~/IdeaProjects/dubbox/dubbo-demo master
> ls
dubbo-demo-api dubbo-demo-provider pom。xml
dubbo-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”?>
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” > 4。0。0 dubbotest dubbotest 1。0-SNAPSHOT
建立 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 等必要的依賴包:
。。。
<!—— 引入 api,在 provider 中實現 api 定義的介面 ——>
dubbotest
api
1。0-SNAPSHOT
<!—— 引入 dubbo ——>
com。alibaba
dubbo
2。8。4
<!—— 引入 log4j ——>
log4j
log4j
1。2。17
<!—— 引入 zookeeper client ——>
com。github。sgroschupf
zkclient
0。1
。。。
阿里的 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”?>
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 會顯示這個名字,方便辨識——> name= “demotest-provider” owner= “programmer” organization= “dubbox” /> <!——使用 zookeeper 註冊中心暴露服務,注意要先開啟 zookeeper——> address= “zookeeper://localhost:2181” /> <!—— 用dubbo協議在20880埠暴露服務 ——> name= “dubbo” port= “20880” /> <!——使用 dubbo 協議實現定義好的 api。PermissionService 介面——> interface= “api。PermissionService” ref= “permissionService” protocol= “dubbo” /> <!——具體實現該介面的 bean——> id= “permissionService” class= “provider。PermissionServiceImpl” />
最後,在 resources 目錄下建立 log4j 的配置檔案:
<?xml version=“1。0” encoding=“UTF-8”?>
<!DOCTYPE log4j:configuration SYSTEM “log4j。dtd”>
xmlns:log4j= “http://jakarta。apache。org/log4j/” debug= “false” > name= “CONSOLE” class= “org。apache。log4j。ConsoleAppender” > class= “org。apache。log4j。PatternLayout” > name= “ConversionPattern” value= “[%d{dd/MM/yy hh:mm:ss:sss z}] %t %5p %c{2}: %m%n” /> value= “INFO” /> ref= “CONSOLE” />
檔案建立完畢,接下來在根目錄下執行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 檔案:
<!—— 引入 api,在 consumer 中呼叫 api 定義的介面 ——>
dubbotest
api
1。0-SNAPSHOT
com。alibaba
dubbo
2。8。4
log4j
log4j
1。2。17
com。github。sgroschupf
zkclient
0。1
建立 spring 配置檔案:
<?xml version=“1。0” encoding=“UTF-8”?>
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” > name= “demotest-consumer” owner= “programmer” organization= “dubbox” /> <!——向 zookeeper 訂閱 provider 的地址,由 zookeeper 定時推送——> address= “zookeeper://localhost:2181” /> <!——使用 dubbo 協議呼叫定義好的 api。PermissionService 介面——> id= “permissionService” interface= “api。PermissionService” />
建立 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 檔案新增依賴:
javax。validation
validation-api
2。0。0。Alpha1
provider module
在 pom 檔案中新增依賴:
<!——rest 規範,比如 Get, Path, MediaType 等——>
javax。ws。rs
javax。ws。rs-api
2。0。1
<!——使用 netty 啟動 rest 服務——>
org。jboss。resteasy
resteasy-netty
3。0。7。Final
<!——rest json 輸出——>
org。jboss。resteasy
resteasy-jackson-provider
3。0。7。Final
<!——rest 需要的依賴——>
org。jboss。resteasy
resteasy-client
3。0。7。Final
<!——驗證——>
org。hibernate
hibernate-validator
4。2。0。Final
<!——slf4j 和其依賴——>
org。slf4j
slf4j-api
1。7。5
org。slf4j
slf4j-log4j12
1。7。5
其中 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 埠——>
name= “rest” port= “4567” threads= “500” contextpath= “services” server= “netty” accepts= “500” extension= “com。alibaba。dubbo。rpc。protocol。rest。support。LoggingFilter” /> <!——使用 rest 規範實現定義好的 api。PermissionRestService 介面——> interface= “api。PermissionRestService” ref= “permissionRestService” protocol= “rest” validation= “true” /> <!——具體實現該介面的 bean——> id= “permissionRestService” class= “provider。PermissionRestServiceImpl” > name= “permissionService” ref= “permissionService” /> 啟動 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 介面——> 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 檔案下新增對應的依賴: javax。validation validation-api RELEASE javax。ws。rs javax。ws。rs-api 2。0。1 com。alibaba dubbo 2。8。4 再次mvn clean package,遇到錯誤 : 1。 org。springframework。beans。factory。BeanCreationException。。。 2。 ERROR integration。RegistryDirectory: Unsupported protocol rest in notified url。。。 3。 。。。 從該 issue 來看是 consumer 缺少相關依賴,新增上: <!——使用 netty 啟動 rest 服務——> org。jboss。resteasy resteasy-netty 3。0。7。Final <!——rest json 輸出——> org。jboss。resteasy resteasy-jackson-provider 3。0。7。Final <!——rest 需要的依賴——> org。jboss。resteasy resteasy-client 3。0。7。Final <!——驗證——> org。hibernate hibernate-validator 4。2。0。Final <!——slf4j 和其依賴——> org。slf4j slf4j-api 1。7。5 org。slf4j slf4j-log4j12 1。7。5 再次執行,發現一切正常了,輸出的結果也是對的: [Permission_0, Permission_1, Permission_2] [Permission_1, Permission_2, Permission_3] 打包 程式碼參見 package 分支。 按照 dubbo 推薦的方式打包成一個 。tar。gz 檔案。在 provider 的 pom 檔案中新增打包的依賴外掛(或直接看最終結果 pom。xml): <!—— 前面是 dependencies ——> maven-dependency-plugin unpack package unpack com。alibaba dubbo 2。8。4 ${project。build。directory}/dubbo META-INF/assembly/** maven-assembly-plugin src/main/assembly/assembly。xml make-assembly package single 在 src/main 下建立資料夾 assembly,並建立相應檔案,如下所示: 。 ├── pom。xml ├── src │ ├── main │ │ ├── assembly │ │ │ ├── assembly。xml │ │ │ └── conf │ │ │ └── dubbo。properties │ │ ├── java。。。 │ │ └── resources。。。 │ └── test。。。 └── target。。。 其中 dubbo。properties 留空即可,dubbo 的配置已經寫在了 spring 的配置中。assembly。xml 內容為: 完成之後,執行 maven 的清理和打包。 打包結果為 provider-1。0-SNAPSHOT-assembly。tar。gz,解壓,其檔案結構為: ~/IdeaProjects/dubbotest/provider/target/provider-1。0-SNAPSHOT > ls bin 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’ 不是來自目標主機的回覆,是網路問題。