Dubbo 新編程模型之註解驅動

原文地址

整體願景

隨著微服務架構的廣泛地推廣和實施。在 Java 生態系統中,以 Spring Boot 和 Spring Cloud 為代表的微服務框架,引入了全新的編程模型,包括註解驅動(Annotation-Driven)、外部化配置(External Configuration)以及自動裝配(Auto-Configure)等。新的編程模型無需 XML 配置、簡化部署、提升開發效率。

為了更好地實踐微服務架構,Dubbo 從 2.5.7 版本開始, 針對 Spring 應用場景(包括 Spring Boot 和 Spring Cloud),新引入註解驅動(Annotation-Driven)、外部化配置(External Configuration)等編程模型。同時,新的編程模型也是即將發佈的 Spring Boot Starter(dubbo-spring-boot-starter) 的基礎設施。更為重要的是,從 Dubbo 2.5.8 開始,無論傳統 Spring 應用,還是 Spring Boot 應用,兩者之間可以實現無縫遷移(無需任何調整)。

註解驅動(Annotation-Driven)

@DubboComponentScan

起始版本: 2.5.7

歷史遺留問題

1. 註解支持不充分

在 Dubbo 2.5.7之前的版本 ,Dubbo 提供了兩個核心註解 @Service 以及 @Reference,分別用於Dubbo 服務提供和 Dubbo 服務引用。

其中,@Service 作為 XML 元素 的替代註解,與 Spring Framework@org.springframework.stereotype.Service 類似,用於服務提供方 Dubbo 服務暴露。與之相對應的@Reference,則是替代 元素,類似於 Spring 中的 @Autowired

2.5.7 之前的Dubbo,與早期的 Spring Framework 2.5 存在類似的不足,即註解支持不夠充分。註解需要和 XML 配置文件配合使用,如下所示:

   

2. @Service Bean 不支持 Spring AOP

同時,使用 方式掃描後的Dubbo @Service ,在 Spring 代理方面存在問題,如 GitHub 上的 issuehttps://github.com/alibaba/dubbo/issues/794:

關於dubbo @Service註解生成ServiceBean時, interface獲取成spring 的代理對象的bug

在項目裡, 我使用了

@Service@[email protected] class SUserJpushServiceImp

的形式, 來暴露服務。但是在發佈服務的時候, interface class 是通過

serviceConfig.setInterface(bean.getClass().getInterfaces()[0]);

的形式獲取, 剛好, 我的service都使用了@Transactional註解, 對象被代理了。所以獲取到的interface是Spring的代理接口…

不少熱心的小夥伴不僅發現這個歷史遺留問題,而且提出了一些修復方案。同時,為了更好地適配 Spring 生命週期以及將 Dubbo 完全向註解驅動編程模型過渡,因此,引入了全新 Dubbo 組件掃描註解 - @DubboComponentScan

注: Spring AOP 問題將在 2.5.9 中修復:https://github.com/alibaba/dubbo/issues/1125

3. @Reference 不支持字段繼承性

假設有一個 Spring Bean AnnotationAction 直接通過字段annotationService 標記 @Reference 引用 AnnotationService

package com.alibaba.dubbo.examples.annotation.action;import com.alibaba.dubbo.config.annotation.Reference;import com.alibaba.dubbo.examples.annotation.api.AnnotationService;import org.springframework.stereotype.Component;@Component("annotationAction")public class AnnotationAction { @Reference private AnnotationService annotationService; public String doSayHello(String name) { return annotationService.sayHello(name); }}

AnnotationAction 被 XML 元素 掃描後:

字段 annotationService 能夠引用到 AnnotationService,執行 doSayHello 方法能夠正常返回。

如果將字段

annotationService 抽取到AnnotationAction 的父類BaseAction 後,AnnotationService 無法再被引用,改造如下所示:

AnnotationAction.java

@Component("annotationAction")public class AnnotationAction extends BaseAction { public String doSayHello(String name) { return getAnnotationService().sayHello(name); }}

BaseAction.java

public abstract class BaseAction { @Reference private AnnotationService annotationService; protected AnnotationService getAnnotationService() { return annotationService; }}

改造後,再次執行 doSayHello 方法,NullPointerException 將會被拋出。說明 並不支持@Reference 字段繼承性。

瞭解了歷史問題,集合整體願景,下面介紹@DubboComponentScan 的設計原則。

設計原則

Spring Framework 3.1 引入了新 Annotation - @ComponentScan , 完全替代了 XML 元素

。同樣, @DubboComponentScan 作為 Dubbo 2.5.7 新增的 Annotation,也是XML 元素 的替代方案。

在命名上(類名以及屬性方法),為了簡化使用和關聯記憶,Dubbo 組件掃描 Annotation @DubboComponentScan,借鑑了 Spring Boot 1.3 引入的 @ServletComponentScan。定義如下:

public @interface DubboComponentScan { /** * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation * declarations e.g.: {@code @DubboComponentScan("org.my.pkg")} instead of * {@code @DubboComponentScan(basePackages="org.my.pkg")}. * * @return the base packages to scan */ String[] value() default {}; /** * Base packages to scan for annotated @Service classes. {@link #value()} is an * alias for (and mutually exclusive with) this attribute. * 

* Use {@link #basePackageClasses()} for a type-safe alternative to String-based * package names. * * @return the base packages to scan */ String[] basePackages() default {}; /** * Type-safe alternative to {@link #basePackages()} for specifying the packages to * scan for annotated @Service classes. The package of each class specified will be * scanned. * * @return classes from the base packages to scan */ Class>[] basePackageClasses() default {};}

注意:basePackages()value() 均能支持佔位符(placeholder)指定的包名

在職責上,@DubboComponentScan 相對於 Spring Boot @ServletComponentScan 更為繁重,原因在於處理 Dubbo @Service 類暴露 Dubbo 服務外,還有幫助 Spring Bean @Reference字段或者方法注入 Dubbo 服務代理。

在場景上,Spring Framework @ComponentScan 組件掃描邏輯更為複雜。而在 @DubboComponentScan 只需關注 @Service@Reference 處理。

在功能上, @DubboComponentScan 不但需要提供完整 Spring AOP 支持的能力,而且還得具備@Reference 字段可繼承性的能力。

瞭解基本設計原則後,下面通過完整的示例,簡介@DubboComponentScan 使用方法以及注意事項。

使用方法

後續通過服務提供方(@Serivce)以及服務消費方(@Reference)兩部分來介紹@DubboComponentScan 使用方法。

假設,服務提供方和服務消費分均依賴服務接口DemoService:

package com.alibaba.dubbo.demo;public interface DemoService { String sayHello(String name);}

服務提供方(@Serivce

實現 DemoService

服務提供方實現DemoService - AnnotationDemoService ,同時標註 Dubbo @Service

package com.alibaba.dubbo.demo.provider;import com.alibaba.dubbo.config.annotation.Service;import com.alibaba.dubbo.demo.DemoService;/*** Annotation {@link DemoService} 實現** @author  */@Servicepublic class AnnotationDemoService implements DemoService { @Override public String sayHello(String name) { return "Hello , " + name; }}

服務提供方 Annotation 配置

AnnotationDemoService 暴露成Dubbo 服務,需要依賴 Spring Bean:AplicationConfigProtocolConfig 以及RegistryConfig 。這三個 Spring Bean 過去可通過 XML 文件方式組裝 Spring Bean:

     

以上裝配方式不予推薦,推薦使用 Annotation 配置,因此可以換成 Spring

@Configuration Bean 的形式:

package com.alibaba.dubbo.demo.config;import com.alibaba.dubbo.config.ApplicationConfig;import com.alibaba.dubbo.config.ProtocolConfig;import com.alibaba.dubbo.config.RegistryConfig;import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/*** 服務提供方配置** @author  */@Configuration@DubboComponentScan("com.alibaba.dubbo.demo.provider") // 掃描 Dubbo 組件public class ProviderConfiguration { /** * 當前應用配置 */ @Bean("dubbo-annotation-provider") public ApplicationConfig applicationConfig() { ApplicationConfig applicationConfig = new ApplicationConfig(); applicationConfig.setName("dubbo-annotation-provider"); return applicationConfig; } /** * 當前連接註冊中心配置 */ @Bean("my-registry") public RegistryConfig registryConfig() { RegistryConfig registryConfig = new RegistryConfig(); registryConfig.setAddress("N/A"); return registryConfig; } /** * 當前連接註冊中心配置 */ @Bean("dubbo") public ProtocolConfig protocolConfig() { ProtocolConfig protocolConfig = new ProtocolConfig(); protocolConfig.setName("dubbo"); protocolConfig.setPort(12345); return protocolConfig; }}

服務提供方引導類

package com.alibaba.dubbo.demo.bootstrap;import com.alibaba.dubbo.demo.DemoService;import com.alibaba.dubbo.demo.config.ProviderConfiguration;import org.springframework.context.annotation.AnnotationConfigApplicationContext;/*** 服務提供方引導類** @author  */public class ProviderBootstrap { public static void main(String[] args) { // 創建 Annotation 配置上下文 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); // 註冊配置 Bean context.register(ProviderConfiguration.class); // 啟動上下文 context.refresh(); // 獲取 DemoService Bean DemoService demoService = context.getBean(DemoService.class); // 執行 sayHello 方法 String message = demoService.sayHello("World"); // 控制檯輸出信息 System.out.println(message); }}

ProviderBootstrap 啟動並執行後,控制輸出與預期一致:

Hello , World

以上直接結果說明 @DubboComponentScan("com.alibaba.dubbo.demo.provider") 掃描後,標註 Dubbo @ServiceAnnotationDemoService 被註冊成 Spring Bean,可從 Spring ApplicationContext 自由獲取。

服務消費方(@Reference

服務 DemoService

package com.alibaba.dubbo.demo.consumer;import com.alibaba.dubbo.config.annotation.Reference;import com.alibaba.dubbo.demo.DemoService;/*** Annotation 驅動 {@link DemoService} 消費方** @author  */public class AnnotationDemoServiceConsumer { @Reference(url = "dubbo://127.0.0.1:12345") private DemoService demoService; public String doSayHell(String name) { return demoService.sayHello(name); }}

服務消費方 Annotation 配置

與服務提供方配置類似,服務消費方也許 Dubbo 相關配置 Bean - ConsumerConfiguration

package com.alibaba.dubbo.demo.config;import com.alibaba.dubbo.config.ApplicationConfig;import com.alibaba.dubbo.config.RegistryConfig;import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan;import com.alibaba.dubbo.demo.consumer.AnnotationDemoServiceConsumer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/*** 服務消費方配置** @author  */@Configuration@DubboComponentScanpublic class ConsumerConfiguration { /** * 當前應用配置 */ @Bean public ApplicationConfig applicationConfig() { ApplicationConfig applicationConfig = new ApplicationConfig(); applicationConfig.setName("dubbo-annotation-consumer"); return applicationConfig; } /** * 當前連接註冊中心配置 */ @Bean public RegistryConfig registryConfig() { RegistryConfig registryConfig = new RegistryConfig(); registryConfig.setAddress("N/A"); return registryConfig; } /** * 註冊 AnnotationDemoServiceConsumer,@DubboComponentScan 將處理其中 @Reference 字段。 * 如果 AnnotationDemoServiceConsumer 非 Spring Bean 的話, * 即使 @DubboComponentScan 指定 package 也不會進行處理,與 Spring @Autowired 同理 */ @Bean public AnnotationDemoServiceConsumer annotationDemoServiceConsumer() { return new AnnotationDemoServiceConsumer(); }}

服務消費方引導類

服務消費方需要先引導服務提供方,下面的實例將會啟動兩個 Spring 應用上下文,首先引導服務提供方 Spring 應用上下文,同時,需要複用前面Annotation 配置 ProviderConfiguration

 /** * 啟動服務提供方上下文 */ private static void startProviderContext() { // 創建 Annotation 配置上下文 AnnotationConfigApplicationContext providerContext = new AnnotationConfigApplicationContext(); // 註冊配置 Bean providerContext.register(ProviderConfiguration.class); // 啟動服務提供方上下文 providerContext.refresh(); }

然後引導服務消費方Spring 應用上下文:

 /** * 啟動並且返回服務消費方上下文 * * @return AnnotationConfigApplicationContext */ private static ApplicationContext startConsumerContext() { // 創建服務消費方 Annotation 配置上下文 AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext(); // 註冊服務消費方配置 Bean consumerContext.register(ConsumerConfiguration.class); // 啟動服務消費方上下文 consumerContext.refresh(); // 返回服務消費方 Annotation 配置上下文 return consumerContext; }

完整的引導類實現:

package com.alibaba.dubbo.demo.bootstrap;import com.alibaba.dubbo.demo.config.ConsumerConfiguration;import com.alibaba.dubbo.demo.config.ProviderConfiguration;import com.alibaba.dubbo.demo.consumer.AnnotationDemoServiceConsumer;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;/*** 服務消費端引導類** @author  */public class ConsumerBootstrap { public static void main(String[] args) { // 啟動服務提供方上下文 startProviderContext(); // 啟動並且返回服務消費方上下文 ApplicationContext consumerContext = startConsumerContext(); // 獲取 AnnotationDemoServiceConsumer Bean AnnotationDemoServiceConsumer consumer = consumerContext.getBean(AnnotationDemoServiceConsumer.class); // 執行 doSayHello 方法 String message = consumer.doSayHello("World"); // 輸出執行結果 System.out.println(message); } /** * 啟動並且返回服務消費方上下文 * * @return AnnotationConfigApplicationContext */ private static ApplicationContext startConsumerContext() { // 創建服務消費方 Annotation 配置上下文 AnnotationConfigApplicationContext consumerContext = new AnnotationConfigApplicationContext(); // 註冊服務消費方配置 Bean consumerContext.register(ConsumerConfiguration.class); // 啟動服務消費方上下文 consumerContext.refresh(); // 返回服務消費方 Annotation 配置上下文 return consumerContext; } /** * 啟動服務提供方上下文 */ private static void startProviderContext() { // 創建 Annotation 配置上下文 AnnotationConfigApplicationContext providerContext = new AnnotationConfigApplicationContext(); // 註冊配置 Bean providerContext.register(ProviderConfiguration.class); // 啟動服務提供方上下文 providerContext.refresh(); }} 

運行ConsumerBootstrap結果,仍然符合期望,AnnotationDemoServiceConsumer 輸出:

Hello , World

Spring AOP 支持

前面提到 註冊 Dubbo @Service 組件後,在 Spring AOP 支持方面存在問題。事務作為 Spring AOP 的功能擴展,自然也會在 中不支持。

@DubboComponentScan 針對以上問題,實現了對 Spring AOP 是完全兼容。將上述服務提供方 Annotation 配置做出一定的調整,標註@EnableTransactionManagement 以及自定義實現PlatformTransactionManager :

@Configuration@DubboComponentScan("com.alibaba.dubbo.demo.provider") // 掃描 Dubbo 組件@EnableTransactionManagement // 激活事務管理public class ProviderConfiguration { // 省略其他配置 Bean 定義 /** * 自定義事務管理器 */ @Bean @Primary public PlatformTransactionManager transactionManager() { return new PlatformTransactionManager() { @Override public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { System.out.println("get transaction ..."); return new SimpleTransactionStatus(); } @Override public void commit(TransactionStatus status) throws TransactionException { System.out.println("commit transaction ..."); } @Override public void rollback(TransactionStatus status) throws TransactionException { System.out.println("rollback transaction ..."); } }; }}

同時調整 AnnotationDemoService - 增加@Transactional 註解:

@Service@Transactionalpublic class AnnotationDemoService implements DemoService { // 省略實現,保持不變}

再次運行ConsumerBootstrap , 觀察控制檯輸出內容:

get transaction ...commit transaction ...Hello , World

輸入內容中多處了兩行,說明自定義 PlatformTransactionManager getTransaction(TransactionDefinition) 以及commit(TransactionStatus) 方法被執行,進而說明 AnnotationDemoServicesayHello(String) 方法執行時,事務也伴隨執行。

注意事項

ConsumerConfiguration 上的 @DubboComponentScan 並沒有指定 basePackages 掃描,這種情況會將ConsumerConfiguration當做 basePackageClasses ,即掃描ConsumerConfiguration 所屬的 package com.alibaba.dubbo.demo.config 以及子 package。由於當前示例中,不存在標註 Dubbo @Service的類,因此在運行時日誌(如果開啟的話)會輸出警告信息:

WARN : [DUBBO] No Spring Bean annotating Dubbo's @Service was found in Spring BeanFactory, dubbo version: 2.0.0, current host: 127.0.0.1

以上信息大可不必擔憂,因為 @DubboComponentScan 除了掃描 Dubbo @Service 組件以外,還將處理 @Reference字段注入。然而讀者特別關注@Reference字段注入的規則。

以上使用不當可能會導致相關問題,如 GitHub 上曾有小夥伴提問:https://github.com/alibaba/dubbo/issues/825

li362692680 提問:

@DubboComponentScan註解在消費端掃描包時掃描的是 @Service註解??不是@Reference註解??

啟動時報

DubboComponentScanRegistrar-85]-[main]-[INFO] 0 annotated @Service Components { [] }

筆者(mercyblitz)回覆:

@Reference 類似於 @Autowired 一樣,首先其申明的類必須被 Spring 上下文當做一個Bean,因此,Dubbo 並沒有直接將 @Reference 字段所在的類提升成 Bean。

綜上所述,這並不是一個問題,而是用法不當!

已知問題

最新發布的 Dubbo 2.5.8 中,@DubboComponentScan 在以下特殊場景下存在 Spring @Service 不兼容情況:

假設有兩個服務實現類 AB,同時存放在com.acme 包下:

  • A 標註 Dubbo @Service

  • B 標註 Dubbo @Service 和 Spring @Service

當 Spring @ComponentScan 先掃描com.acme 包時,B 被當做 Spring Bean 的候選類。隨後,@DubboComponentScan 也掃描相同的包。當應用啟動時,AB 雖然都是 Spring Bean,可僅 A 能夠暴露 Dubbo 服務,B 則丟失。

問題版本:2.5.72.5.8

問題詳情:https://github.com/alibaba/dubbo/issues/1120

修復版本:2.5.9(下個版本)

關於作者

小馬哥,十餘年Java EE 從業經驗,架構師、微服務佈道師、Dubbo 維護者。目前主要負責阿里巴巴集團微服務技術實施、架構衍進、基礎設施構建等。重點關注雲計算、微服務以及軟件架構等領域。通過SUN Java(SCJP、SCWCD、SCBCD)以及Oracle OCA 等的認證。

github:https://github.com/mercyblitz

sf.gg : https://segmentfault.com/u/mercyblitz


分享到:


相關文章: