原文參見一個簡單的 dubbo 服務

這篇文章將簡單的介紹如何寫一個 dubbo 服務。dubbo 的準備工作請參照前文。

一個簡單的 dubbo demo 服務

一個簡單的 dubbo demo 服務

從此圖可知,當註冊中心和監控已經工作起來之後,我們需要寫的就是消費方和服務方的程式碼了。一個簡單的一對一提供服務的程式碼結構如下

~/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 內容為:

assembly

tar。gz

true

${project。build。directory}/dubbo/META-INF/assembly/bin

bin

0755

src/main/assembly/conf

conf

0644

lib

完成之後,執行 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 配置

一個簡單的 dubbo demo 服務

一個簡單的 dubbo demo 服務

接下來就可以在 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’ 不是來自目標主機的回覆,是網路問題。