Dubbo 新編程模型之註解驅動

原文地址

總體願景

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

爲了更好地實踐微服務架構,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 應用,二者之間能夠實現無縫遷移(無需任何調整)。git

註解驅動(Annotation-Driven)

@DubboComponentScan

起始版本: 2.5.7

<dubbo:annotation> 歷史遺留問題

1. 註解支持不充分

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

其中,@Service 做爲 XML 元素 <dubbo:service>的替代註解,與 Spring Framework @org.springframework.stereotype.Service 相似,用於服務提供方 Dubbo 服務暴露。與之相對應的@Reference,則是替代<dubbo:reference 元素,相似於 Spring 中的 @Autowiredspring

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <dubbo:application name="annotation-provider"/>
    <dubbo:registry address="127.0.0.1:4548"/>
    <dubbo:annotation package="com.alibaba.dubbo.config.spring.annotation.provider"/>

</beans>
2. @Service Bean 不支持 Spring AOP

同時,使用 <dubbo:annotation> 方式掃描後的Dubbo @Service ,在 Spring 代理方面存在問題,如 GitHub 上的 issue https://github.com/alibaba/du...bootstrap

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

在項目裏, 我使用了api

@Service
@Transactional
@com.alibaba.dubbo.config.annotation.Service
public class SUserJpushServiceImp

的形式, 來暴露服務。可是在發佈服務的時候, interface class 是經過
``
serviceConfig.setInterface(bean.getClass().getInterfaces()[0]);
``
的形式獲取, 恰好, 個人service都使用了@Transactional註解, 對象被代理了。因此獲取到的interface是Spring的代理接口...微信

很多熱心的小夥伴不只發現這個歷史遺留問題,並且提出了一些修復方案。同時,爲了更好地適配 Spring 生命週期以及將 Dubbo 徹底向註解驅動編程模型過渡,所以,引入了全新 Dubbo 組件掃描註解 - @DubboComponentScan架構

注: <dubbo:annotation> Spring AOP 問題將在 2.5.9 中修復: https://github.com/alibaba/du...
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 元素 <dubbo:annotation> 掃描後:

<dubbo:annotation package="com.alibaba.dubbo.examples.annotation.action"/>

字段 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 將會被拋出。說明<dubbo:annotation> 並不支持@Reference 字段繼承性。

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

設計原則

Spring Framework 3.1 引入了新 Annotation - @ComponentScan , 徹底替代了 XML 元素 <context:component-scan> 。一樣, @DubboComponentScan 做爲 Dubbo 2.5.7 新增的 Annotation,也是XML 元素 <dubbo:annotation> 的替代方案。

在命名上(類名以及屬性方法),爲了簡化使用和關聯記憶,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.
     * <p>
     * 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 <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 */
@Service
public 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:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
    ">

    <!-- 當前應用信息配置 -->
    <dubbo:application name="dubbo-annotation-provider"/>

    <!-- 鏈接註冊中心配置 -->
    <dubbo:registry id="my-registry" address="N/A"/>

    <dubbo:protocol name="dubbo" port="12345"/>

</beans>

以上裝配方式不予推薦,推薦使用 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 <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 */
@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 <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 */
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 <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 */
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 <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 */
@Configuration
@DubboComponentScan
public 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 <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 */
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:annotation> 註冊 Dubbo @Service 組件後,在 Spring AOP 支持方面存在問題。事務做爲 Spring AOP 的功能擴展,天然也會在 <dubbo:annotation> 中不支持。

@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
@Transactional
public 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字段注入的規則。

以上實現爲例,AnnotationDemoServiceConsumer 必須申明爲 Spring @Bean 或者 @Component(或者其派生註解),不然 @DubboComponentScan 不會主動將標註 @Reference字段所在的聲明類提成爲 Spring Bean,換句話說,若是 @Reference字段所在的聲明類不是 Spring Bean 的話, @DubboComponentScan 不會處理@Reference注入,其原理與 Spring @Autowired 一致。

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

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/du...

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

關於做者

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

github:https://github.com/mercyblitz

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

微信/微博:mercyblitz

下篇預告

《Dubbo 新編程模型以外部化配置(External Configuration)》

更多 Dubbo 以及 微服務相關內容,請關注小馬哥公衆號:小馬哥公衆號

相關文章
相關標籤/搜索