前言
本博文主要介绍Dubbo的应用案例。在介绍之前,我们选择使用ZooKeeper来做注册中心。我们在搭建Dubbo的时候需要先在本地启动一个ZooKeeper服务,可以参考博文ZooKeeper之环境搭建。
应用案例
案例准备
任何的Dubbo项目都需要三个角色:公用API、服务提供者和服务消费者。所以我们所有的示例项目都有对应的三个子模块。
我们新建一个Maven工程dubbo-demo,在父工程里面添加如下依赖:
<?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.rubin</groupId>
<artifactId>dubbo-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<dubbo.version>2.7.5 </dubbo.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>${dubbo.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-dubbo</artifactId>
<version>${dubbo.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-netty4</artifactId>
<version>${dubbo.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
<version>${dubbo.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 日志配置 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
<!-- json数据化转换 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
XML应用案例
在父工程dubbo-demo下我们添加第一个示例xml-demo,Maven配制如下:
<?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">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<artifactId>xml-demo</artifactId>
</project>
创建完毕之后,我们的准备工作就做完了。下面开始分步创建三个子模块工程。
公用API
我们在xml-demo模块下创建公用API子模块xml-service-api。Maven依赖如下:
<?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">
<parent>
<artifactId>xml-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xml-service-api</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-netty4</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
</dependency>
</dependencies>
</project>
编写我们的共用接口:
package com.rubin.dubbo.xml.api;
public interface IXmlService {
String sayHello(String name);
}
服务提供者
我们在xml-demo模块下创建服务提供者子模块xml-service-provider。Maven依赖如下:
<?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">
<parent>
<artifactId>xml-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xml-service-provider</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>xml-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
创建其服务实现:
package com.rubin.dubbo.xml.provider.service;
import com.rubin.dubbo.xml.api.IXmlService;
public class XmlServiceImpl implements IXmlService {
@Override
public String sayHello(String name) {
return "Hello " + name + "! This is a xml service response.";
}
}
创建其XML配置文件xml-service-provider.xml:
<?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:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="xml-service-provider" />
<!-- 使用zk广播注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.rubin.dubbo.xml.api.IXmlService" ref="xmlService" />
<!-- 和本地bean一样实现服务 -->
<bean id="xmlService" class="com.rubin.dubbo.xml.provider.service.XmlServiceImpl" />
</beans>
添加log4j的配置文件log4j.properties:
log4j.rootCategory=INFO,CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
最后创建我们的启动类来启动服务提供者,注册到注册中心开始对外提供服务:
package com.rubin.dubbo.xml.provider;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
public class XmlServiceProviderBootstrap {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("xml-service-provider.xml");
applicationContext.start();
System.out.println("The xml provider is ready.Please enter any key to exit.");
System.in.read();
}
}
服务消费者
我们在xml-demo模块下创建服务提供者子模块xml-service-consumer。Maven依赖如下:
<?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">
<parent>
<artifactId>xml-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xml-service-consumer</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>xml-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
创建XML配置文件 xml-service-consumer.xml :
<?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:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="xml-service-consumer" />
<!-- 使用zk广播注册中心暴露发现服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 生成远程服务代理,可以和本地bean一样使用xmlService -->
<dubbo:reference id="xmlService" interface="com.rubin.dubbo.xml.api.IXmlService" />
</beans>
添加log4j的配置文件log4j.properties:
log4j.rootCategory=INFO,CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
最后创建我们的启动类来启动服务消费者来开始消费服务:
package com.rubin.dubbo.xml.consumer;
import com.rubin.dubbo.xml.api.IXmlService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
import java.util.UUID;
public class XmlServiceConsumerBoostrap {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext applicationContext =
new ClassPathXmlApplicationContext("xml-service-consumer.xml");
applicationContext.start();
System.out.println("The xml consumer is ready.Please enter any key to make a remote call.");
IXmlService iXmlService = applicationContext.getBean(IXmlService.class);
while (true) {
System.in.read();
System.out.println(iXmlService.sayHello(UUID.randomUUID().toString()));
}
}
}
启动之后,我们在控制台输入几个字符回车,会看到有相应的调用产生:
注解应用案例
在父工程dubbo-demo下我们添加第一个示例basic-demo,Maven配制如下:
<?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">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<artifactId>basic-demo</artifactId>
<description>dubbo基本使用案例</description>
</project>
创建完毕之后,我们的准备工作就做完了。下面开始分步创建三个子模块工程。
公用API
同样,我们在basic-demo模块下创建我们的公用API子模块basic-service-api。Maven定义如下:
<?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">
<parent>
<artifactId>basic-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>basic-service-api</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-dubbo</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-netty4</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
</dependency>
</dependencies>
</project>
创建公共API接口:
package com.rubin.dubbo.basic.api;
public interface IBasicService {
String sayHello(String name);
}
服务提供者
在basic-demo模块下创建我们的服务提供者子模块basic-service-provider。Maven定义如下:
<?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">
<parent>
<artifactId>basic-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>basic-service-provider</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>basic-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
创建我们的服务实现类,并通过Dubbo提供的@Service
注解暴露服务,代码详情如下:
package com.rubin.dubbo.basic.provider.service;
import com.rubin.dubbo.basic.api.IBasicService;
import org.apache.dubbo.config.annotation.Service;
@Service
public class BasicServiceImpl implements IBasicService {
public String sayHello(String name) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello " + name + "! This is a basic service response.";
}
}
添加我们的日志配置文件log4j.properties:
log4j.rootCategory=INFO,CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
接下来编写我们的服务配置文件,因为我们需要启动三个服务生产者为后续服务扩展做准备,所以我们编写三个配置文件basic-service-provider.properties、 basic-service-provider1.properties和 basic-service-provider2.properties :
dubbo.application.name=basic-service-provider
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
dubbo.config-center.timeout=10000
dubbo.application.name=basic-service-provider
dubbo.protocol.name=dubbo
dubbo.protocol.port=20881
dubbo.config-center.timeout=10000
dubbo.application.name=basic-service-provider
dubbo.protocol.name=dubbo
dubbo.protocol.port=20882
dubbo.config-center.timeout=10000
对应的我们有三个配置类去加载对应的配置文件:
package com.rubin.dubbo.basic.provider.config;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@EnableDubbo(scanBasePackages = "com.rubin.dubbo.basic.provider")
@PropertySource("classpath:/basic-service-provider.properties")
@ComponentScan(basePackages = "com.rubin.dubbo.basic.provider.service")
public class BasicServiceProviderConfig {
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
return registryConfig;
}
}
package com.rubin.dubbo.basic.provider.config;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@EnableDubbo(scanBasePackages = "com.rubin.dubbo.basic.provider")
@PropertySource("classpath:/basic-service-provider1.properties")
@ComponentScan(basePackages = "com.rubin.dubbo.basic.provider.service")
public class BasicServiceProvider1Config {
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
return registryConfig;
}
}
package com.rubin.dubbo.basic.provider.config;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@EnableDubbo(scanBasePackages = "com.rubin.dubbo.basic.provider")
@PropertySource("classpath:/basic-service-provider2.properties")
@ComponentScan(basePackages = "com.rubin.dubbo.basic.provider.service")
public class BasicServiceProvider2Config {
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
return registryConfig;
}
}
再编写三个服务启动类我们的服务提供者就编写完成了:
package com.rubin.dubbo.basic.provider;
import com.rubin.dubbo.basic.provider.config.BasicServiceProviderConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.IOException;
public class BasicServiceProviderBootstrap {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BasicServiceProviderConfig.class);
applicationContext.start();
System.out.println("The basic provider is ready.Please enter any key to exit.");
System.in.read();
}
}
package com.rubin.dubbo.basic.provider;
import com.rubin.dubbo.basic.provider.config.BasicServiceProvider1Config;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.IOException;
public class BasicServiceProvider1Bootstrap {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BasicServiceProvider1Config.class);
applicationContext.start();
System.out.println("The basic provider1 is ready.Please enter any key to exit.");
System.in.read();
}
}
package com.rubin.dubbo.basic.provider;
import com.rubin.dubbo.basic.provider.config.BasicServiceProvider2Config;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.IOException;
public class BasicServiceProvider2Bootstrap {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BasicServiceProvider2Config.class);
applicationContext.start();
System.out.println("The basic provider2 is ready.Please enter any key to exit.");
System.in.read();
}
}
依次启动我们三个服务提供者。
服务消费者
在basic-demo模块下创建我们的服务生产者子模块basic-service-consumer。Maven定义如下:
<?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">
<parent>
<artifactId>basic-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>basic-service-consumer</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>basic-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
添加我们的日志配置文件log4j.properties:
log4j.rootCategory=INFO,CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss.SSS} [%t] %-5p %c.%M\(%F:%L\) - %m%n
编写我们服务消费者的配置文件basic-service-consumer.properties:
dubbo.application.name=basic-service-consumer
dubbo.registry.address=zookeeper://127.0.0.1:2181
编写配置类:
package com.rubin.dubbo.basic.consumer.config;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@EnableDubbo(scanBasePackages = "com.rubin.dubbo.basic.consumer")
@PropertySource("classpath:/basic-service-consumer.properties")
@ComponentScan(value = { "com.rubin.dubbo.basic.consumer" })
public class BasicServiceConsumerConfig {
}
编写我们的服务代理类:
package com.rubin.dubbo.basic.consumer.proxy;
import com.rubin.dubbo.basic.api.IBasicService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Component;
@Component(value = "basicServiceProxy")
public class BasicServiceProxy implements IBasicService {
@Reference(check = false, loadbalance = "rubinLoadBalance", timeout = 10000, mock = "return hello")
private IBasicService iBasicService;
@Override
public String sayHello(String name) {
return iBasicService.sayHello(name);
}
}
最后编写我们的启动类启动并在控制台输入回车键查看调用情况:
package com.rubin.dubbo.basic.consumer;
import com.rubin.dubbo.basic.api.IBasicService;
import com.rubin.dubbo.basic.consumer.config.BasicServiceConsumerConfig;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class BasicServiceConsumerBootstrap {
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BasicServiceConsumerConfig.class);
applicationContext.start();
System.out.println("The basic consumer is ready.Please enter any key to make a remote call.");
IBasicService iBasicService = (IBasicService) applicationContext.getBean("basicServiceProxy");
while (true) {
System.in.read();
// 同步调用
System.out.println(iBasicService.sayHello(UUID.randomUUID().toString()));
}
}
}
SPI
SPI简介
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现。简单来说,它就是一种动态替换发现的机制。使用SPI机制的优势是实现解耦,使得第三方服务模块的装配控制逻辑与调用者的业务代码分离。
JDK中的SPI
Java中如果想要使用SPI功能,先提供标准服务接口,然后再提供相关接口实现和调用者。这样就可以通过SPI机制中约定好的信息进行查询相应的接口实现。
SPI遵循如下约定:
- 当服务提供者提供了接口的一种具体实现后,在META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名
- 接口实现类所在的jar包放在主程序的classpath中
- 主程序通过java.util.ServiceLoader动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
- SPI的实现类必须携带一个无参构造方法
示例
我们创建一个新的Maven工程叫做java-spi-demo。Maven坐标如下:
<?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.rubin</groupId>
<artifactId>java-spi-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
接下来创建三个子项目分别是java-spi-api、java-spi-service-provider和java-spi-service-consumer分别对应接口项目、服务扩展项目以及扩展服务使用项目。
java-spi-api
Maven坐标:
<?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">
<parent>
<artifactId>java-spi-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>java-spi-api</artifactId>
</project>
共用接口:
package com.rubin.java.spi.api;
public interface IJavaSpiService {
String sayHello();
}
java-spi-service-provider
Maven坐标:
<?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">
<parent>
<artifactId>java-spi-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>java-spi-service-provider</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>java-spi-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
服务扩展实现:
package com.rubin.java.spi.service.provider;
import com.rubin.java.spi.api.IJavaSpiService;
public class CatJavaSpiServiceImpl implements IJavaSpiService {
public String sayHello() {
return "miao~ miao~";
}
}
package com.rubin.java.spi.service.provider;
import com.rubin.java.spi.api.IJavaSpiService;
public class DogJavaSpiServiceImpl implements IJavaSpiService {
public String sayHello() {
return "wang! wang!";
}
}
编写配置文件:classpath:META-INF\services\com.rubin.java.spi.api.IJavaSpiService。文件内容如下:
com.rubin.java.spi.service.provider.CatJavaSpiServiceImpl
com.rubin.java.spi.service.provider.DogJavaSpiServiceImpl
java-spi-service-consumer
Maven坐标如下:
<?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">
<parent>
<artifactId>java-spi-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>java-spi-service-consumer</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>java-spi-service-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
使用扩展的代码如下:
package com.rubin.java.spi.service.consumer;
import com.rubin.java.spi.api.IJavaSpiService;
import java.util.ServiceLoader;
public class JavaSpiConsumerBootstrap {
public static void main(String[] args) {
final ServiceLoader<IJavaSpiService> javaSpiServices = ServiceLoader.load(IJavaSpiService.class);
for (IJavaSpiService javaSpiService : javaSpiServices) {
System.out.println(javaSpiService.getClass().getSimpleName() + ":" + javaSpiService.sayHello());
}
}
}
运行main方法我们查看控制台打印情况如下:
证明我们的自定义扩展类已经成功被加载了。
Dubbo中的SPI
Dubbo中大量的使用了SPI来作为扩展点,通过实现同一接口的前提下,可以进行定制自己的实现类。比如比较常见的协议,负载均衡,都可以通过SPI的方式进行定制化,自己扩展Dubbo中已经存在的所有已经实现好的扩展点。
下图中则是Dubbo中默认提供的负载均衡策略。
Dubbo中扩展点使用方式
我们创建一个Maven工程来演示一下Dubbo中扩展点的使用方式。首先创建Maven工程spi-demo,其Maven坐标如下:
<?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">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<artifactId>spi-demo</artifactId>
</project>
创建之后,我们还是分为三个子模块spi-api、spi-service-provider和spi-service-consumer来演示。
spi-api
Maven坐标如下:
<?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">
<parent>
<artifactId>spi-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spi-api</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
</dependencies>
</project>
公用接口如下:
package com.rubin.dubbo.spi.api;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
@SPI
public interface ISpiService {
String sayHello();
}
spi-service-provider
Maven坐标如下:
<?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">
<parent>
<artifactId>spi-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spi-service-provider</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>spi-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
服务实现类如下:
package com.rubin.dubbo.spi.service.provider;
import com.rubin.dubbo.spi.api.ISpiService;
import org.apache.dubbo.common.URL;
public class CatSpiServiceImpl implements ISpiService {
@Override
public String sayHello() {
return "miao~ miao~";
}
}
package com.rubin.dubbo.spi.service.provider;
import com.rubin.dubbo.spi.api.ISpiService;
import org.apache.dubbo.common.URL;
public class DogSpiServiceImpl implements ISpiService {
@Override
public String sayHello() {
return "wang! wang!";
}
}
扩展配置文件classpath:META-INF/dubbo/com.rubin.dubbo.spi.api.ISpiService如下:
cat=com.rubin.dubbo.spi.service.provider.CatSpiServiceImpl
dog=com.rubin.dubbo.spi.service.provider.DogSpiServiceImpl
spi-service-consumer
Maven坐标如下:
<?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">
<parent>
<artifactId>spi-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spi-service-consumer</artifactId>
<dependencies>
<dependency>
<groupId>com.rubin</groupId>
<artifactId>spi-service-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
测试方法定义如下:
package com.rubin.dubbo.spi.service.consumer;
import com.rubin.dubbo.spi.api.ISpiService;
import org.apache.dubbo.common.extension.ExtensionLoader;
import java.util.Set;
public class SpiConsumerBootstrap {
public static void main(String[] args) {
// 获取扩展加载器
ExtensionLoader<ISpiService> extensionLoader = ExtensionLoader.getExtensionLoader(ISpiService.class);
// 遍历所有的支持的扩展点 META-INF.dubbo
Set<String> extensions = extensionLoader.getSupportedExtensions();
for (String extension : extensions){
String result = extensionLoader.getExtension(extension).sayHello();
System.out.println(result);
}
}
}
运行main方法我们会看到如下打印结果:
看到这里我们可能有疑问:既然JDK已经支持了SPI,为什么Dubbo还要自己扩展呢?有以下三点原因:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源
- 如果有扩展点加载失败,则所有扩展点无法使用
- 提供了对扩展点包装的功能(Adaptive),并且还支持通过set的方式对其他的扩展点进行注入
也就是说,Dubbo在JDK的SPI机制上做了增强,屏蔽了一个扩展点失败影响其他扩展点的问题,也解决了多个扩展点实现动态选择扩展时现的问题。
Dubbo SPI中的Adaptive功能
Dubbo中的Adaptive功能,主要解决的问题是如何动态的选择具体的扩展点。通过 getAdaptiveExtension
统一对指定接口对应的所有扩展点进行封装,通过URL的方式对扩展点来进行动态选择。 (dubbo中所有的注册信息都是通过URL的形式进行处理的。)这里同样采用相同的方式进行实现。
我们对上一小节的示例做一个增强。首先,我们扩展我们的共用接口类:
package com.rubin.dubbo.spi.api;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
@SPI("cat")
public interface ISpiService {
String sayHello();
@Adaptive
String sayHello(URL url);
}
由上述代码我们可以看出,我们添加了一个sayHello
的重载方法,并用@Adaptive
注解标注,来表明这是一个动态选择增前实现的方法,选择的方式就是添加一个URL( org.apache.dubbo.common.URL
)类型的传参(注意,所有使用 @Adaptive
注解标注 的增强方法均要有URL类型的参数)。
@SPI
注解中的值标明URL文案中没有指定增强实现的话默认使用的增强实现。
我们在来扩展我们的实现类:
package com.rubin.dubbo.spi.service.provider;
import com.rubin.dubbo.spi.api.ISpiService;
import org.apache.dubbo.common.URL;
public class CatSpiServiceImpl implements ISpiService {
@Override
public String sayHello() {
return "miao~ miao~";
}
@Override
public String sayHello(URL url) {
return "miao~ miao~ url";
}
}
package com.rubin.dubbo.spi.service.provider;
import com.rubin.dubbo.spi.api.ISpiService;
import org.apache.dubbo.common.URL;
public class DogSpiServiceImpl implements ISpiService {
@Override
public String sayHello() {
return "wang! wang!";
}
@Override
public String sayHello(URL url) {
return "wang! wang! url";
}
}
最后,我们编写一个测试方法来测试一下:
package com.rubin.dubbo.spi.service.consumer;
import com.rubin.dubbo.spi.api.ISpiService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class SpiConsumerAdaptiveBootstrap {
public static void main(String[] args) {
URL url = URL.valueOf("test://localhost/hello?i.spi.service=");
// 获取扩展加载器
ExtensionLoader<ISpiService> extensionLoader = ExtensionLoader.getExtensionLoader(ISpiService.class);
// 遍历所有的支持的扩展点 META-INF.dubbo
final ISpiService adaptiveExtension = extensionLoader.getAdaptiveExtension();
String result = adaptiveExtension.sayHello(url);
System.out.println(result);
}
}
该测试方法需要注意以下几点:
- 因为在这里只是临时测试,所以为了保证URL规范,前面的信息均为测试值即可,关键的点在于
i.spi.service
参数,这个参数的值指定的就是具体的实现方式。关于为什么叫i.spi.service
,是因为这个接口的名称,其中后面的大写部分被Dubbo自动转码为 . 分割 - 通过
getAdaptiveExtension
来提供一个统一的类来对所有的扩展点提供支持(底层对所有的扩展点进行封装) - 调用时通过参数中增加
URL
对象来实现动态的扩展点使用 - 如果
URL
没有提供该参数,则该方法会使用默认在SPI
注解中声明的实现
我们的默认测试结果如下:
可以看到,是选择了cat的实现。原因就是我们在URL
里面未指定实现,自动读取了@SPI
注解里面的默认实现。我们在URL
中指定实现为dog的来测试一下:
可以看到,我们的实现已经切换到dog对应的扩展点实现了。
过滤器
与很多框架一样,Dubbo也存在拦截(过滤)机制,可以通过该机制在执行目标程序前后执行我们指定的代码。
Dubbo的Filter机制,是专门为服务提供方和服务消费方调用过程进行拦截设计的,每次远程方法执行,该拦截都会被执行。这样就为开发者提供了非常方便的扩展性,比如为dubbo接口实现ip白名单功能、监控功能 、日志记录等。
我们在dubbo-demo模块下创建一个子模块叫做spi-filter。其Maven坐标如下:
<?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">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spi-filter</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
</dependencies>
</project>
定义我们的过滤器实现类:
package com.rubin.dubbo.spi.filter;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcException;
@Activate(group = { CommonConstants.CONSUMER, CommonConstants.PROVIDER })
public class RubinFilter implements Filter {
public RubinFilter() {
System.out.println("加载自定义过滤器");
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long startTime = System.currentTimeMillis();
try {
// 执行方法
return invoker.invoke(invocation);
} finally {
System.out.println("invoke time:" + (System.currentTimeMillis() - startTime) + "毫秒");
}
}
}
这里需要注意的是,我们需要指定该过滤器是在消费者端生效还是服务提供者端生效。扩展好自定义过滤器之后,我们需要使用上面讲解了SPI机制将扩展点加载进容器中,即定义扩展点配置文件classpath:META-INF/dubbo/org.apache.dubbo.rpc.Filter:
rubinFilter=com.rubin.dubbo.spi.filter.RubinFilter
将该依赖引入到我们的basic-service-api模块的pom文件中,启动服务发起调用查看是否生效:
负载均衡策略
负载均衡(Load Balance), 其实就是将请求分摊到多个操作单元上进行执行,从而共同完成工作任务。负载均衡策略主要用于客户端存在多个提供者时进行选择某个提供者。在集群负载均衡时,Dubbo 提供了多种均衡策略(包括随机、轮询、最少活跃调用数、一致性Hash),缺省为random随机调用。配置负载均衡策略,既可以在服务提供者一方配置,也可以在服务消费者一方配置,如下:
//在服务消费者一方配置负载均衡策略
@Reference(check = false,loadbalance = "random")
//在服务提供者一方配置负载均衡
@Service(loadbalance = "random")
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
return "hello " + name;
}
}
我们也可以自己定义负载均衡器。我们下面通过一个示例来演示一下。
我们在dubbo-demo模块下创建一个子模块叫做spi-loadbalance。其Maven坐标如下:
<?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">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spi-loadbalance</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
</dependencies>
</project>
定义我们负载均衡策略的实现:
package com.rubin.dubbo.spi.loadbalance;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.cluster.LoadBalance;
import java.util.List;
public class RubinLoadBalance implements LoadBalance {
public RubinLoadBalance() {
System.out.println("加载自定义负载均衡器");
}
@Override
public <T> Invoker<T> select(List<Invoker<T>> list, URL url, Invocation invocation) throws RpcException {
System.out.println("进入自定义负载均衡器");
// 所有的服务提供者 按照IP + 端口排序 选择第一个
final Invoker<T> invoker = list.stream().sorted((i1, i2) -> {
final int ipCompare = i1.getUrl().getIp().compareTo(i2.getUrl().getIp());
if (ipCompare == 0) {
return Integer.compare(i1.getUrl().getPort(), i2.getUrl().getPort());
}
return ipCompare;
}).findFirst().get();
System.out.println("选择了:" + invoker.getUrl().getIp() + ":" + invoker.getUrl().getPort());
return invoker;
}
}
将扩展点添加到容器中,配置 classpath:META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance:
rubinLoadBalance=com.rubin.dubbo.spi.loadbalance.RubinLoadBalance
将该依赖引入到我们的basic-service-api模块的pom文件中,修改我们服务消费者的代理类如下(即指定我们的自定义负载均衡器):
package com.rubin.dubbo.basic.consumer.proxy;
import com.rubin.dubbo.basic.api.IBasicService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Component;
@Component(value = "basicServiceProxy")
public class BasicServiceProxy implements IBasicService {
@Reference(check = false, loadbalance = "rubinLoadBalance", timeout = 10000)
private IBasicService iBasicService;
@Override
public String sayHello(String name) {
// 同步调用
return iBasicService.sayHello(name);
}
}
启动服务发起调用。这里需要注意,我们需要启动两个及以上的服务提供者,否则负载均衡策略将不生效。预期结果如下:
异步调用
Dubbo不只提供了堵塞式的的同步调用,同时提供了异步调用的方式。这种方式主要应用于提供者接口响应耗时明显,消费者端可以利用调用接口的时间去做一些其他的接口调用,利用 Future 模式来异步等待和获取结果即可。这种方式可以大大的提升消费者端的利用率。 目前这种方式可以通过XML的方式进行引入。
我们在写基本示例的时候已经将我们服务实现延迟了两秒才返回结果。我们想要同步改成异步调用只需要在服务消费方配制如下:
package com.rubin.dubbo.basic.consumer.proxy;
import com.rubin.dubbo.basic.api.IBasicService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Component;
@Component(value = "basicServiceProxy")
public class BasicServiceProxy implements IBasicService {
@Reference(check = false, loadbalance = "rubinLoadBalance", timeout = 10000)
private IBasicService iBasicService;
@Reference(check = false, loadbalance = "rubinLoadBalance", async = true, timeout = 10000)
private IBasicService iAsyncBasicService;
@Override
public String sayHello(String name) {
// 同步调用
// return iBasicService.sayHello(name);
// 异步调用
return iAsyncBasicService.sayHello(name);
}
}
修改一下我们的消费者启动类如下:
package com.rubin.dubbo.basic.consumer;
import com.rubin.dubbo.basic.api.IBasicService;
import com.rubin.dubbo.basic.consumer.config.BasicServiceConsumerConfig;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class BasicServiceConsumerBootstrap {
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BasicServiceConsumerConfig.class);
applicationContext.start();
System.out.println("The basic consumer is ready.Please enter any key to make a remote call.");
IBasicService iBasicService = (IBasicService) applicationContext.getBean("basicServiceProxy");
while (true) {
System.in.read();
// 同步调用
// System.out.println(iBasicService.sayHello(UUID.randomUUID().toString()));
// 异步调用
String hello = iBasicService.sayHello(UUID.randomUUID().toString());
// 利用Future 模式来获取
Future<Object> future = RpcContext.getContext().getFuture();
System.out.println("result :" + hello);
System.out.println("future result:"+future.get());
}
}
}
启动消费者发起调用结果如下:
需要特别说明的是,该方式的使用,请确保Dubbo的版本在2.5.4及以后的版本使用。 原因在于在2.5.3及之前的版本使用的时候,会出现异步状态传递问题。比如我们的服务调用关系是 A -> B -> C , 这时候如果A向B发起了异步请求,在错误的版本时,B向C发起的请求也会连带的产生异步请求。这是因为在底层实现层面,他是通过 RPCContext 中的attachment 实现的。在A向B发起异步请求时,会在 attachment 中增加一个异步标示字段来表明异步等待结果。B在接受到A中的请求时,会通过该字段来判断是否是异步处理。但是由于值传递问题,B向C发起时同样会将该值进行传递,导致C误以为需要异步结果,导致返回空。这个问题在2.5.4及以后的版本进行了修正。
线程池
Dubbo在使用时,都是通过创建真实的业务线程池进行操作的。目前已知的线程池模型有两个和java中的相互对应:
- fix: 表示创建固定大小的线程池。也是Dubbo默认的使用方式,默认创建的执行线程数为200,并且是没有任何等待队列的。所以再极端的情况下可能会存在问题,比如某个操作大量执行时,可能存在堵塞的情况
- cache: 创建非固定大小的线程池,当线程不足时,会自动创建新的线程。但是使用这种的时候需要注意,如果突然有高TPS的请求过来,方法没有及时完成,则会造成大量的线程创建,对系统的CPU和负载都是压力,执行越多反而会拖慢整个系统
在真实的使用过程中可能会因为使用fix模式的线程池,导致具体某些业务场景因为线程池中的线程数量不足而产生错误,而很多业务研发是对这些无感知的,只有当出现错误的时候才会去查看告警或者通过客户反馈出现严重的问题才去查看,结果发现是线程池满了。所以可以在创建线程池的时,通过某些手段对这个线程池进行监控,这样就可以进行及时的扩缩容机器或者告警。下面的这个程序就是这样子的,会在创建线程池后进行对其监控,并且及时作出相应处理。
我们在dubbo-demo模块下创建一个子模块叫做spi-threadpool。其Maven坐标如下:
<?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">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spi-threadpool</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
</dependencies>
</project>
自定义一个监控线程池:
package com.rubin.dubbo.spi.threadpool;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.threadpool.support.fixed.FixedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ReportFixedThreadPool extends FixedThreadPool implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(ReportFixedThreadPool.class);
// 定义线程池使用的阀值
private static final double ALARM_PERCENT = 0.90;
private final Map<URL, ThreadPoolExecutor> THREAD_POOLS = new ConcurrentHashMap<>();
public ReportFixedThreadPool() {
// 每隔3秒打印线程使用情况
Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(this, 1, 3, TimeUnit.SECONDS);
}
@Override
public Executor getExecutor(URL url) {
Executor executor = super.getExecutor(url);
if (executor instanceof ThreadPoolExecutor) {
THREAD_POOLS.put(url, (ThreadPoolExecutor) executor);
}
return executor;
}
@Override
public void run() {
// 遍历线程池
for (Map.Entry<URL, ThreadPoolExecutor> entry : THREAD_POOLS.entrySet()) {
final URL url = entry.getKey();
final ThreadPoolExecutor executor = entry.getValue();
// 计算相关指标
final int activeCount = executor.getActiveCount();
final int poolSize = executor.getCorePoolSize();
double usedPercent = activeCount / (poolSize * 1.0);
LOGGER.info("线程池执行状态:[{}/{}:{}%]", activeCount, poolSize, usedPercent * 100);
if (usedPercent > ALARM_PERCENT) {
LOGGER.error("超出警戒线! host:{} 当前使用率是:{},URL:{}", url.getIp(), usedPercent * 100, url);
}
}
}
}
将扩展点添加到容器中,配置 classpath:META-INF/dubbo/org.apache.dubbo.common.threadpool.ThreadPool:
report=com.rubin.dubbo.spi.threadpool.ReportFixedThreadPool
将该依赖引入到我们的basic-service-api模块的pom文件中,在服务提供者的配置文件修改为:
dubbo.application.name=basic-service-provider
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
dubbo.config-center.timeout=10000
dubbo.provider.threadpool=report
启动服务提供者,会看到每隔3秒打印一下线程池情况。我们修改一下服务消费者的启动类,让其启动的时候并发产生1000次请求来测试一下线程池的监控情况:
package com.rubin.dubbo.basic.consumer;
import com.rubin.dubbo.basic.api.IBasicService;
import com.rubin.dubbo.basic.consumer.config.BasicServiceConsumerConfig;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class BasicServiceConsumerBootstrap {
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BasicServiceConsumerConfig.class);
applicationContext.start();
System.out.println("The basic consumer is ready.Please enter any key to make a remote call.");
IBasicService iBasicService = (IBasicService) applicationContext.getBean("basicServiceProxy");
while (true) {
System.in.read();
// 同步调用
// System.out.println(iBasicService.sayHello(UUID.randomUUID().toString()));
// 异步调用
// String hello = iBasicService.sayHello(UUID.randomUUID().toString());
// 利用Future 模式来获取
// Future<Object> future = RpcContext.getContext().getFuture();
// System.out.println("result :" + hello);
// System.out.println("future result:"+future.get());
// 并发执行 查看线程池监控打印情况
for (int i = 0; i < 1000; i++) {
iBasicService.sayHello(UUID.randomUUID().toString());
}
}
}
}
分别启动服务提供者和消费者,我们会看到提供者的控制台如下:
路由规则
路由是决定一次请求中需要发往目标机器的重要判断,通过对其控制可以决定请求的目标机器。我们可通过创建这样的规则来决定一个请求会交给哪些服务器去处理。
我们可以创建如下一个路由规则示例:
package com.rubin.dubbo.basic.consumer.router;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.registry.Registry;
import org.apache.dubbo.registry.RegistryFactory;
/**
* 路由规则示例,运行起来服务后调用本类main方法,去除本机服务提供者
*/
public class BasicServiceConsumerRouterBootstrap {
public static void main(String[] args) {
RegistryFactory
registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://127.0.0.1:2181"));
registry.register(URL.valueOf(
"condition://0.0.0.0/com.rubin.dubbo.basic.api.IBasicService?category=routers&force=true&dynamic=true&rule="
+ URL.encode("=> host != 172.50.3.30")));
}
}
我们运行其服务提供者和消费者之后,运行此类的main
方法之后,发起调用。我们会发现本机的服务提供者被屏蔽了,会抛出无服务的异常。
通过上面的程序,我们实际本质上就是通过在zookeeper中保存一个节点数据,来记录路由规则。消费者会通过监听这个服务的路径,来感知整个服务的路由规则配置,然后进行适配。这里主要介绍路由配置的参数。具体请参考文档, 这里只对关键的参数做说明。
- route:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填
- 0.0.0.0 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填
- com.rubin.dubbo.basic.api.IBasicService 表示只对指定服务生效,必填
- category=routers 表示该数据为动态配置类型,必填
- dynamic 是否为持久数据,当指定服务重启时是否继续生效。必填
- runtime 是否在设置规则时自动缓存规则,如果设置为true则会影响部分性能
- rule 是整个路由最关键的配置,用于配置路由规则。
… => …
在这里=>
前面的就是表示消费者方的匹配规则,可以不填(代表全部)。=>
后方则必须填写,表示当请求过来时,如果选择提供者的配置。官方这块儿也给出了详细的示例,可以按照那里来写。其中使用最多的便是host
参数。 必填
路由与上线系统结合
当公司到了一定的规模之后,一般都会有自己的上线系统,专门用于服务上线。方便后期进行维护和记录的追查。我们去想象这样的一个场景,一个Dubbo的提供者要准备进行上线,一般都提供多台提供者来同时在线上提供服务。这时候一个请求刚到达一个提供者,提供者却进行了关闭操作。那么此次请求就应该认定为失败了。所以基于这样的场景,我们可以通过路由的规则,把预发布(灰度)的机器进行从机器列表中移除。并且等待一定的时间,让其把现有的请求处理完成之后再进行关闭服务。同时,在启动时,同样需要等待一定的时间,以免因为尚未重启结束,就已经注册上去。等启动到达一定时间之后,再进行开启流量操作。
其实现思路如下:
- 利用ZooKeeper的路径感知能力,在服务准备进行重启之前将当前机器的IP地址和应用名写入ZooKeeper
- 服务消费者监听该目录,读取其中需要进行关闭的应用名和机器IP列表并且保存到内存中
- 当前请求过来时,判断是否是请求该应用,如果是请求重启应用,则将该提供者从服务列表中移除
我们在dubbo-demo模块下创建一个子模块叫做spi-router。其Maven坐标如下:
<?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">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.rubin</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spi-router</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
</dependency>
</dependencies>
</project>
我们先定义一个ZooKeeper的客户端工厂类来构建ZooKeeper的客户端实例:
package com.rubin.dubbo.spi.router;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class ZookeeperClientFactory {
private CuratorFramework client;
private static ZookeeperClientFactory INSTANCE;
public ZookeeperClientFactory(CuratorFramework client) {
this.client = client;
}
static {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
INSTANCE = new ZookeeperClientFactory(client);
client.start();
}
public static CuratorFramework client() {
return INSTANCE.client;
}
}
构建一个重启实例的监听器,用来保存正在重启的实例信息并提供添加重启实例和删除重启实例的方法:
package com.rubin.dubbo.spi.router;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ReadyRestartInstances implements PathChildrenCacheListener {
private static final Logger LOGGER = LoggerFactory.getLogger(ReadyRestartInstances.class);
private static final String LISTEN_PATH = "/rubin/dubbo/restart/instances";
private final CuratorFramework zkClient;
// 当节点变化时 给这个集合赋值 重启机器的信息列表
private volatile Set<String> restartInstances = new HashSet<>();
private ReadyRestartInstances(CuratorFramework zkClient) {
this.zkClient = zkClient;
}
public static ReadyRestartInstances create() {
final CuratorFramework zookeeperClient = ZookeeperClientFactory.client();
try {
// 检查监听路径是否存在
final Stat stat = zookeeperClient.checkExists().forPath(LISTEN_PATH);
// 如果监听路径不存在 则创建
if (stat == null) {
zookeeperClient.create().creatingParentsIfNeeded().forPath(LISTEN_PATH);
}
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("确保基础路径存在");
}
final ReadyRestartInstances instances = new ReadyRestartInstances(zookeeperClient);
// 创建一个NodeCache
PathChildrenCache nodeCache = new PathChildrenCache(zookeeperClient, LISTEN_PATH, false);
// 给节点缓存对象 加入监听
nodeCache.getListenable().addListener(instances);
try {
nodeCache.start();
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("启动路径监听失败");
}
return instances;
}
/**
* 返回应用名和主机拼接后的字符串
*/
private String buildApplicationAndInstanceString(String applicationName, String host) {
return applicationName + "_" + host;
}
/**
* 增加重启实例的配置信息方法
*/
public void addRestartingInstance(String applicationName, String host) throws Exception {
zkClient.create().creatingParentsIfNeeded()
.forPath(LISTEN_PATH + "/" + buildApplicationAndInstanceString(applicationName, host));
}
/**
* 删除重启实例的配置信息方法
*/
public void removeRestartingInstance(String applicationName, String host) throws Exception {
zkClient.delete().forPath(LISTEN_PATH + "/" + buildApplicationAndInstanceString(applicationName, host));
}
/**
* 判断节点信息 是否存在于 restartInstances
*/
public boolean hasRestartingInstance(String applicationName, String host) {
return restartInstances.contains(buildApplicationAndInstanceString(applicationName, host));
}
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent)
throws Exception {
// 查询出监听路径下 所有的目录配置信息
final List<String> restartingInstances = zkClient.getChildren().forPath(LISTEN_PATH);
// 给 restartInstances
if (CollectionUtils.isEmpty(restartingInstances)) {
restartInstances = Collections.emptySet();
} else {
restartInstances = new HashSet<>(restartingInstances);
}
}
}
再来定义我们的自定义路由类,来保证请求到来的时候,过滤掉正在重启的实例:
package com.rubin.dubbo.spi.router;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.cluster.Router;
import java.util.List;
import java.util.stream.Collectors;
public class RestartingInstanceRouter implements Router {
private final ReadyRestartInstances instances;
private final URL url;
public RestartingInstanceRouter(URL url) {
this.url = url;
instances = ReadyRestartInstances.create();
}
@Override
public URL getUrl() {
return url;
}
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
// 如果没有在重启列表中 才会加入到后续调用列表
return invokers.stream().filter(i -> !instances
.hasRestartingInstance(i.getUrl().getParameter("remote.application"), i.getUrl().getIp()))
.collect(Collectors.toList());
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean isForce() {
return true;
}
@Override
public int getPriority() {
return 0;
}
}
最后定义我们的路由工厂类扩展点:
package com.rubin.dubbo.spi.router;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.cluster.Router;
import org.apache.dubbo.rpc.cluster.RouterFactory;
@Activate
public class RestartingInstanceRouterFactory implements RouterFactory {
@Override
public Router getRouter(URL url) {
return new RestartingInstanceRouter(url);
}
}
将扩展点添加到容器中,配置 classpath:META-INF/dubbo/org.apache.dubbo.rpc.cluster.RouterFactory:
restartingInstanceRouterFactory=com.rubin.dubbo.spi.router.RestartingInstanceRouterFactory
将该依赖引入到我们的basic-service-api模块的pom文件中, 在服务消费者端添加一个测试方法:
package com.rubin.dubbo.basic.consumer.instance;
import com.rubin.dubbo.spi.router.ReadyRestartInstances;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.registry.Registry;
import org.apache.dubbo.registry.RegistryFactory;
/**
* 服务重启示例,运行起来服务后调用本类main方法,暂时去除本机服务提供者,30秒后自动加入服务提供者队列,模拟手动控制实例重启时的路由
*/
public class BasicServiceConsumerRestartInstanceBootstrap {
public static void main(String[] args) throws Exception {
ReadyRestartInstances.create().addRestartingInstance("basic-service-provider","172.50.3.30");
Thread.sleep(30000);
ReadyRestartInstances.create().removeRestartingInstance("basic-service-provider","172.50.3.30");
}
}
我们可以启动服务提供者和服务消费者。之后启动上述测试方法,会发现在30秒内我们是无法访问服务提供者,30秒之后就可以正常访问了。
服务动态降级
服务降级也就是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略的降低服务级别,以释放服务器资源,保证核心任务的正常运行。
而为什么要使用服务降级,这是防止分布式服务发生雪崩效应,什么是雪崩?就是蝴蝶效应,当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,样下去将导致整个分布式服务都瘫痪,这就是雪崩。
Dubbo也有很多方式来实现服务降级:
- 在 Dubbo 管理控制台配置服务降级
- 指定返回简单值或者null
- 使用java代码 动态写入配置中心
- 整合 hystrix
前三种的配置规则都一样,只是配置入口不一样。规则如下:
- 配置mock="return null"或者mock="return 1234"就是简单的返回null或者返回1234的字符串
- 配置mock="force:return null"表示无论服务提供者可用不可用,都强制服务消费方直接返回null而不发起调用
- 配置mock="fail:return null"表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响
使用java代码动态写入配置中心示例代码如下:
RegistryFactory registryFactory =
ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension()
;
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://IP:端
口"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?
category=configurators&dynamic=false&application=foo&mock=force:return+null"));
详情查看官方文档。
以上就是博文的全部内容。博文中所讲的均为我们工作中经常使用的地方,如果想要详细了解的话,建议查看官方文档。官方文档很详细也都是中文的。欢迎留言交流~~~
文章评论