SpringBoot-18 創(chuàng)建基于Dubbo框架的SpringBoot微服務(wù)

Dubbo 是原阿里巴巴 B2B 平臺(tái)技術(shù)部?jī)A力打造的一款開源且優(yōu)秀的面向 Java 平臺(tái)的服務(wù)框架,歷經(jīng)三次大的迭代,從最早盲從 OSGi 的坑里跳出來,再到序列化協(xié)議和通信框架的定型,最終才完善和成型,可以說,國(guó)內(nèi)開源服務(wù)化框架中無出其右者。

但是,Dubbo 研發(fā)的年代處于 Spring 框架的 XSD 時(shí)代,所以,使用上還是會(huì)更多以 XML 形式定義服務(wù)和訪問服務(wù),但好在 SpringBoot 框架在 IoC 容器的配置方式上“不挑食”,所以,我們還是可以讓 Dubbo 和 SpringBoot 框架友好相處。

直接使用 Dubbo 開發(fā)微服務(wù)了,為什么還要用 SpringBoot?,這是一個(gè)很好的問題。

使用 SpringBoot 開發(fā)基于 Dubbo 框架的微服務(wù)的原因就是,我們將 Dubbo 微服務(wù)以 SpringBoot 形式標(biāo)準(zhǔn)化了。如此一來,我們既可以享受 SpringBoot 框架和周邊的一系列研發(fā)支持,還可以用統(tǒng)一的形式發(fā)布、部署和運(yùn)維。

微服務(wù)的一個(gè)特點(diǎn)就是數(shù)量多,一個(gè)人應(yīng)對(duì) 1000 個(gè)相同操作接口的微服務(wù)和一個(gè)人應(yīng)對(duì) 1000 個(gè)不同操作接口的微服務(wù)相比來說,相信你會(huì)選擇前者。

2015 年到 2016年,美元升值預(yù)期持續(xù)升溫,大家想必對(duì)匯率也比較關(guān)注,所以,我們不妨實(shí)現(xiàn)一個(gè)基于央行匯率的查詢服務(wù)。

1. 定義匯率查詢服務(wù)接口

我們新建 Maven 工程 currency-rates-api:

<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>com.keevol.springboot</groupId>
    <artifactId>currency-rates-api</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>currency-rates-api</name>
    <url>http://maven.apache.org</url>
    <properties>
        <java_source_version>1.8</java_source_version>
        <java_target_version>1.8</java_target_version>
        <file_encoding>UTF-8</file_encoding>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java_source_version}</source>
                    <target>${java_target_version}</target>
                    <encoding>${file_encoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                <configuration>
                    <charset>${file_encoding}</charset>
                    <encoding>${file_encoding}</encoding>
                    <additionalparam>-Xdoclint:none</additionalparam>
                </configuration>
                <executions>
                    <execution>
                        <id>attach-javadocs</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project> 

然后定義匯率查詢服務(wù)接口:

public interface CurrencyRateService {
    ExchangeRate quote(CurrencyPair currencyPair) throws IOException;
} 

之后通過 mvn install 或者 mvn deploy 將 currency-rates-api 發(fā)布到本地或者遠(yuǎn)程 maven 倉(cāng)庫(kù)。

2. 實(shí)現(xiàn)匯率查詢服務(wù)

新建 Maven 工程 currency-rates-service:

<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>com.keevol.springboot</groupId>
    <artifactId>currency-rates-service</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>currency-rates-service</name>
    <url>http://maven.apache.org</url>
    <properties>
        <java_source_version>1.8</java_source_version>
        <java_target_version>1.8</java_target_version>
        <file_encoding>UTF-8</file_encoding>
        <spring_version>4.1.6.RELEASE</spring_version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.keevol.springboot</groupId>
            <artifactId>currency-rates-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.5.3</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring_version}</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring_version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring_version}</version>
        </dependency>
    </dependencies>
</project> 

主要關(guān)注針對(duì) currency-rates-api 以及 Dubbo 框架的依賴定義。

項(xiàng)目創(chuàng)建完成后,我們著手實(shí)現(xiàn)服務(wù)接口:

public class CurrencyRateServiceImpl implements CurrencyRateService {
    private CurrencyRateRepository rateRepository;
    public ExchangeRate quote(CurrencyPair currencyPair) throws IOException {
        return rateRepository.get(currencyPair);
    }
    public CurrencyRateRepository getRateRepository() {
        return rateRepository;
    }
    public void setRateRepository(CurrencyRateRepository rateRepository) {
        this.rateRepository = rateRepository;
    }
}

其中,CurrencyRateRepository 可以根據(jù)情況給出相應(yīng)的實(shí)現(xiàn),比如通過直接對(duì)接銀行的系統(tǒng)獲取匯率或者通過爬取官網(wǎng)數(shù)據(jù)獲得數(shù)據(jù)都可以,這里不做過多解釋。

服務(wù)實(shí)現(xiàn)之后,我們需要通過 Dubbo 框架暴露出去給外部使用,所以,我們通過 Dubbo 的服務(wù)描述文件(即標(biāo)準(zhǔn)的 Spring 框架 XML 形式的配置文件)完成最終服務(wù)的對(duì)外開放:

<?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:context="http://www.springframework.org/schema/context"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:c="http://www.springframework.org/schema/c"
    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://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util.xsd
        http://code.alibabatech.com/schema/dubbo
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
    <dubbo:application name="${dubbo.application.name}"
        owner="${dubbo.application.owner}" />
    <dubbo:protocol
        name="${dubbo.application.protocol.name}"
        port="${dubbo.application.protocol.port}" />
    <dubbo:service
        interface="com.keevol.springboot.services.currency.rates.CurrencyRateService"
        ref="serviceImpl" registry="N/A" />
    <bean id="serviceImpl"
        class="com.keevol.springboot.services.currency.rates.CurrencyRateServiceImpl" />
</beans>

因?yàn)橹皇窃褪纠?,所以,我們沒有定義更加嚴(yán)謹(jǐn)?shù)姆?wù)注冊(cè)方式(registry="N/A"),生產(chǎn)環(huán)境下,大家還是需要選擇合適的注冊(cè)服務(wù),比如 Zookeeper。

使用 Dubbo 框架的服務(wù)不依賴傳統(tǒng) J2EE 的容器對(duì)外提供服務(wù),而是以獨(dú)立進(jìn)程的形式對(duì)外服務(wù),所以,我們還需要提供一個(gè) Bootstrap 類用于啟動(dòng)我們的 Dubbo 服務(wù):

public class Bootstrap {
    public static void main(String[] args) throws IOException {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/services.xml");
        ((AbstractApplicationContext) context).registerShutdownHook();
        System.in.read();
    }
}

至此,一個(gè)獨(dú)立部署運(yùn)行的 Dubbo 服務(wù)宣告完成。

3. 沒有 SpringBoot 什么事兒

不管是否使用 SpringBoot,基于 Dubbo 框架的服務(wù)從其服務(wù)的定義,到服務(wù)的實(shí)現(xiàn),都是常規(guī)而無法省略的工作,但是:

  • 服務(wù)完成后,啟動(dòng) main 函數(shù)里的邏輯貌似每次都要編寫一樣的?
  • 服務(wù)完成后,以什么樣的形式發(fā)布并部署?zip 包,還是 jar 包,亦或 war 包,甚至其他形式?

考慮到這些,就會(huì)涉及 SpringBoot,一旦將 Dubbo 服務(wù)以 SpringBoot 的形式封裝,Dubbo 服務(wù)就可以既享受 SpringBoot 的開發(fā)便捷性,又能以統(tǒng)一的形式發(fā)布和部署(比如可執(zhí)行的 jar 形式)。

雖然我們無法省略和簡(jiǎn)化服務(wù)的定義和實(shí)現(xiàn)這些步驟,但 Dubbo 服務(wù)的 main 函數(shù)邏輯實(shí)際上是可以固化下來并且復(fù)用的,否則,每個(gè)人給出的 Dubbo 服務(wù)實(shí)現(xiàn)的啟動(dòng)類都可能不一樣,進(jìn)而導(dǎo)致運(yùn)維操作不一樣。

在上面我們給出的 main 函數(shù)實(shí)現(xiàn)中,為了阻止 Dubbo 服務(wù)的進(jìn)程退出(主線程執(zhí)行完畢,無其他非 daemon 線程存活)。

我們使用了 IO 操作來達(dá)成這一目的(System.in.read()),但實(shí)際上,這不是一個(gè)很好的做法。更好的做法是,我們?cè)O(shè)置一個(gè)服務(wù)是否關(guān)閉的開關(guān),只有當(dāng)外部調(diào)用相應(yīng)管理接口將服務(wù)關(guān)閉之后,再關(guān)閉當(dāng)前的 Dubbo 服務(wù),所以,基于此思路,我們可以實(shí)現(xiàn)一個(gè) ShutdownLatch:

public interface ShutdownLatchMBean {
    String shutdown();
}
public class ShutdownLatch implements ShutdownLatchMBean {
    protected AtomicBoolean running = new AtomicBoolean(false);
    public long checkIntervalInSeconds = 10;
    private String domain = "com.wacai.lifecycles";
    public ShutdownLatch() {
    }
    public ShutdownLatch(String domain) {
        this.domain = domain;
    }
    public void await() throws Exception {
        if (running.compareAndSet(false, true)) {
            MBeanServer mBeanServer = ManagementFactory.get - PlatformMBeanServer();
            mBeanServer.registerMBean(this, new ObjectName(domain, "name", "ShutdownLatch"));
            while (running.get()) {
                TimeUnit.SECONDS.sleep(checkIntervalInSeconds);
            }
        }
    }
    @Override
    public String shutdown() {
        if (running.compareAndSet(true, false)) {
            return "shutdown signal sent, shutting down..";
        } else {
            return "shutdown signal had been sent, no need again and again and again...";
        }
    }
    public static void main(String[] args) throws Exception {
        ShutdownLatch latch = new ShutdownLatch("your_domain_for_mbeans");
        latch.await();
    }
}

ShutdownLatch 默認(rèn)以 JMX 的 MBean 暴露為管理接口,所以,Dubbo 服務(wù)的 main 函數(shù)可以規(guī)范為:

ApplicationContext context = new ClassPathXmlApplicationContext("spring/services.xml");
((AbstractApplicationContext) context).registerShutdownHook();
ShutdownLatch latch = new ShutdownLatch("your_domain_for_mbeans");
latch.await();

為了簡(jiǎn)化基于 SpringBoot 的 Dubbo 服務(wù)開發(fā),我們可以將針對(duì) Dubbo 框架的依賴以及對(duì)服務(wù)啟動(dòng)類的規(guī)范封裝為一個(gè) spring-boot-starter-dubbo 這樣的自動(dòng)配置模塊,此后,要開發(fā)一個(gè)基于 SpringBoot 的 Dubbo 服務(wù),只要依賴這一自動(dòng)配置模塊即可。

所以,我們新建 Maven 項(xiàng)目 spring-boot-starter-dubbo:

<?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>com.keevol.springboot</groupId>
    <artifactId>spring-boot-starter-dubbo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>spring-boot-starter-dubbo</name>
    <description>Demo project for Spring Boot</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.1.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java_source_version>1.8</java_source_version>
        <java_target_version>1.8</java_target_version>
        <file_encoding>UTF-8</file_encoding>
        <spring_version>4.1.6.RELEASE</spring_version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.5.3</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

項(xiàng)目的 pom.xml 中,我們可以重點(diǎn)關(guān)注針對(duì) spring-boot-starter 和 Dubbo 框架的依賴。

所有的 SpringBoot 應(yīng)用啟動(dòng)都是基于標(biāo)準(zhǔn)的 SpringApplication.run 完成的,為了在啟動(dòng)類執(zhí)行完成后可以阻止 Dubbo 服務(wù)進(jìn)程的推出,我們需要在 SpringBoot 啟動(dòng)類的 main 函數(shù)最后調(diào)用 ShutdownLatch.await()。

但是如果要求每個(gè)開發(fā)者在自己的 SpringBoot 啟動(dòng)類中調(diào)用這段代碼,顯然這并沒有給開發(fā)者的工作帶來任何簡(jiǎn)化,所以,我們選擇使用 CommandLineRunner 來完成針對(duì) ShutdownLatch.await() 的調(diào)用工作。

教程前面已經(jīng)介紹過 CommandLineRunner,任何注冊(cè)到 SpringBoot 應(yīng)用的 CommandLineRunner 都將在 SpringApplication.run 執(zhí)行完成后再執(zhí)行,恰好符合我們當(dāng)前場(chǎng)景需要,代碼如下所示:

public class DubboServiceLatchCommandLineRunner implements CommandLineRunner {
    private String domain = "com.keevol.services.management";
    @Override
    public void run(String... args) throws Exception {
        ShutdownLatch latch = new ShutdownLatch(getDomain());
        latch.await();
    }
    public String getDomain() {
        return domain;
    }
    public void setDomain(String domain) {
        this.domain = domain;
    }
}

然后我們需要將 DubboServiceLatchCommandLineRunner 注冊(cè)到 SpringBoot 應(yīng)用的容器之中,所以,定義一個(gè) JavaConfig 類如下:

@Configuration
@Order
public class DubboAutoConfiguration {
    protected Logger logger = LoggerFactory.getLogger(DubboAutoConf - iguration.class);
    @Value("${shutdown.latch.domain.name: com.keevol.services.management}")
    private String shutdownLatchDomainName;
    @Bean
    @ConditionalOnClass(name = "com.alibaba.dubbo.rpc.Exporter")
    public DubboServiceLatchCommandLineRunner configureDubboService-LatchCommandLineRunner() {
        logger.debug("DubboAutoConfiguration enabled by adding Dubbo-ServiceLatchCommandLineRunner.");
        DubboServiceLatchCommandLineRunner runner = new DubboServi-ceLatchCommandLineRunner();
        runner.setDomain(shutdownLatchDomainName);
        return runner;
    }
}

我們使用 @Order 標(biāo)注了該配置類以保證 DubboServiceLatchCommandLineRunner 在最后執(zhí)行,以避免阻塞其他邏輯的執(zhí)行。

萬里長(zhǎng)征走到了最后一步,要實(shí)現(xiàn)一個(gè) SpringBoot 的自動(dòng)配置模塊,我們需要將 DubboAutoConfiguration 配置類通過 META-INF/spring.factories 配置文件注冊(cè)并發(fā)布:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.keevol.springboot.dubbo.autoconfigure.DubboAutoConfiguration 

至此,我們只要通過 mvn install 或者 mvn deploy 將 spring-boot-starter-dubbo 發(fā)布到本地或者遠(yuǎn)程 Maven 倉(cāng)庫(kù),以后開發(fā) Dubbo 服務(wù)就只需要在服務(wù)的項(xiàng)目依賴中添加如下依賴:

<dependency>
    <groupId>com.keevol.springboot</groupId>
    <artifactId>spring-boot-starter-dubbo</artifactId>
    <version>1.0.0</version>
</dependency>

然后以標(biāo)準(zhǔn)的 SpringApplication 加載 Dubbo 服務(wù)的配置并啟動(dòng)就可以了:

@SpringBootApplication
public class DubboWithSpringbootApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(Dubb - oWithSpringbootApplication.class,
                "classpath*:/spring/**/*.xml");
        application.run(args);
    }
}

當(dāng)然,規(guī)范 Dubbo 依賴以及服務(wù)的啟動(dòng)邏輯只是使用 SpringBoot 獲得的好處之一,最主要的,我們提供了一種基于 SpringBoot 的標(biāo)準(zhǔn)化的 Dubbo 服務(wù)開發(fā)和發(fā)布實(shí)踐,這對(duì)于海量微服務(wù)的開發(fā)和運(yùn)維來說是很重要的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容