前言
随着阿里巴巴开源的高性能分布式 RPC 框架 Dubbo 正式进入 Apache 孵化器,Dubbo 又火了一把。本文作为 Dubbo 系列开端,先教大家使用 Dubbo 搭建一个简单的分布式系统,因为要研究一个东西的原理,必须先能把环境搭建起来,并且会使用它。
在这个系统里面会包含服务提供者,服务消费者,服务注册中心(本 Chat 使用 ZooKeeper),管理控制台(Dubbo-Admin),监控平台(Dubbo-Monitor),麻雀虽小,却五脏俱全。
通过本 Chat 你将能学到(文章内有 Demo 源码):
五大组件关系
服务注册中心的搭建
本文我们讲解 Apache ZooKeeper 的搭建。
解压该包后,如下图:
可知 zk 在端口 2181 进行监听,至此服务注册中心搭建完毕。
服务提供方与服务消费方搭建
本文 Demo 结构介绍
首先讲解下本文使用的 Demo 的结构,Demo 使用 Maven 聚合功能,里面有三个模块,目录如下:
其中 SDK 里面的接口定义源码如下:
public interface UserServiceBo { String sayHello(String name);
String sayHello2(String name);
String testPojo(Person person);
}
在 SDK 模块执行 mvn clean install
命令会安装该模块的 Jar 到本地仓库,要想在其他模块引入该 Jar,必须要先执行这个安装步骤。
基于 Spring 配置的服务提供方与消费方搭建
Provider 模块为服务提供者,作用是注册提供的服务到 zk,并使用 Netty 服务监听服务消费端的链接。里面 TestProvider 和 provider.xml 组成了基于 XML 方式的服务提供,UserServiceImpl 为服务实现类。
public class UserServiceImpl implements UserServiceBo{ @Override
public String sayHello(String name) { //让当前当前线程休眠2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} return name;
} @Override
public String sayHello2(String name) { //让当前当前线程休眠2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} return name;
} @Override
public String testPojo(Person person) { return JSON.toJSONString(person);
}
}
<?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: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/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="dubboProvider" />
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 启用monitor模块 -->
<dubbo:monitor protocol="registry" />
<bean id="userService" class="com.test.UserServiceImpl" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.test.UserServiceBo" ref="userService"
group="dubbo" version="1.0.0" timeout="3000"/>
</beans>
log4j.rootLogger=INFO,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
public class TestProvider {
public static void main(String[] arg) throws InterruptedException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:provider.xml"); //挂起当前线程,如果没有改行代码,服务提供者进程会消亡,服务消费者就发现不了提供者了
Thread.currentThread().join();
}
}
说明当前服务已经注册了 ZooKeeper 了。
4.3 基于 Dubbo API 方式的服务提供方与消费方搭建
Consumer 模块为服务消费方,服务消费端主要是从 zk 获取自己需要的服务提供者的 ip 列表,然后根据路由规则选择一个 ip 进行远程调用。里面 TestConsumer 和 consumer.xml 组成了基于 XML 方式的服务调用。
<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: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/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="dubboConsumer" />
<!-- 使用multicast广播注册中心暴露发现服务地址 -->
<dubbo:registry protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />
<!-- 启动monitor-->
<dubbo:monitor protocol="registry" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="userService" interface="com.test.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000"/>
</beans>
public class TestConsumer { public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( new String[] { "classpath:consumer.xml" }); final UserServiceBo demoService = (UserServiceBo) context.getBean("userService");
System.out.println(demoService.sayHello("哈哈哈"));
}
}
说明服务消费端已经正常调用了服务提供方的服务了。
注:至此一个经典的含有服务提供者,服务消费者,服务注册中心的简单分布式系统搭建完毕了。
基于 Dubbo API 方式的服务提供方与消费方搭建
其中 Provider 模块为服务提供者,里面 TestProviderApi 是基于 Dubbo API 的服务提供,UserServiceImpl 为服务实现类。
public class TestProviderApi { public static void main(String[] arg) throws InterruptedException { //(4.3.1-1)等价于 <bean id="userService" class="com.test.UserServiceImpl" />
UserServiceBo userService = new UserServiceImpl(); //(4.3.1-2)等价于 <dubbo:application name="dubboProvider" />
ApplicationConfig application = new ApplicationConfig();
application.setName("dubboProvider"); //(4.3.1-3)等价于 <dubbo:registry address="zookeeper://127.0.0.1:2181" />
RegistryConfig registry = new RegistryConfig();
registry.setAddress("127.0.0.1:2181");
registry.setProtocol("zookeeper"); // (4.3.1-4)等价于 <dubbo:protocol name="dubbo" port="20880" />
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(20880); //4.3.1-5)等价于 <dubbo:monitor protocol="registry" />
MonitorConfig monitorConfig = new MonitorConfig();
monitorConfig.setProtocol("registry"); //4.3.1-6)等价于 <dubbo:service interface="com.test.UserServiceBo" ref="userService"
//group="dubbo" version="1.0.0" timeout="3000"/>
ServiceConfig<UserServiceBo> service = new ServiceConfig<UserServiceBo>(); // 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
service.setApplication(application);
service.setMonitor(monitorConfig);
service.setRegistry(registry); // 多个注册中心可以用setRegistries()
service.setProtocol(protocol); // 多个协议可以用setProtocols()
service.setInterface(UserServiceBo.class);
service.setRef(userService);
service.setVersion("1.0.0");
service.setGroup("dubbo");
service.setTimeout(3000);
service.export(); //4.3.1-8) 挂起当前线程
Thread.currentThread().join();
}
}
其中 Consumer 模块为服务消费者,里面 TestConsumerApi 是基于 Dubbo API 方式的服务调用。
public class TestConsumerApi { public static void main(String[] args) throws InterruptedException { // 等价于 <dubbo:application name="dubboConsumer" />
ApplicationConfig application = new ApplicationConfig();
application.setName("dubboConsumer"); // 等价于 <dubbo:registry protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />
RegistryConfig registry = new RegistryConfig();
registry.setAddress("127.0.0.1:2181");
registry.setProtocol("zookeeper"); //等价于 <dubbo:monitor protocol="registry" />
MonitorConfig monitorConfig = new MonitorConfig();
monitorConfig.setProtocol("registry"); //等价于<dubbo:reference id="userService" interface="com.test.UserServiceBo"
//group="dubbo" version="1.0.0" timeout="3000" />
ReferenceConfig<UserServiceBo> reference = new ReferenceConfig<UserServiceBo>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface(UserServiceBo.class);
reference.setVersion("1.0.0");
reference.setGroup("dubbo");
reference.setTimeout(3000);
reference.setInjvm(false);
reference.setMonitor(monitorConfig);
UserServiceBo userService = reference.get();
System.out.println(userService.sayHello("哈哈哈"));
Thread.currentThread().join();
}
}
服务消费端泛化调用
前面我们讲解基于 Spring 和基于 Dubbo API 方式搭建一个简单的分布式系统时候服务消费端是引入了一个 SDK 二方包的,里面存放了服务提供端提供的所有接口类,之所以需要引入接口类是因为服务消费端一般是基于接口使用 JDK 代理实现远程调用的。
泛化接口调用方式主要用于服务消费端没有 API 接口类及模型类元(比如入参和出参的 POJO 类)的情况下使用;这时候参数及返回值中由于没有对应的 POJO 类,所以所有 POJO 均转换为 Map 表示。使用泛化调用时候服务消费模块不在需要引入 SDK 二方包。
下面基于 Dubbo API 来实现异步调用,在 Consumer 模块里面 TestConsumerApiGeneric 是泛化调用的方式,代码如下:
public class TestConsumerApiGeneric { public static void main(String[] args) throws IOException { // 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("dubboConsumer"); // 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("127.0.0.1:2181");
registry.setProtocol("zookeeper"); // 泛型参数设置为GenericService
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
reference.setApplication(application);
reference.setRegistry(registry);
reference.setVersion("1.0.0");
reference.setGroup("dubbo");
reference.setTimeout(3000); //设置为泛化
reference.setInterface("com.test.UserServiceBo");
reference.setGeneric(true); //用com.alibaba.dubbo.rpc.service.GenericService替代所有接口引用
GenericService userService = reference.get(); //
// 基本类型以及Date,List,Map等不需要转换,直接调用,如果返回值为POJO也将自动转成Map
Object result = userService.$invoke("sayHello", new String[] {"java.lang.String"}, new Object[] {"哈哈哈"});
System.out.println(JSON.json(result));//POJO参数转换为mapMap<String, Object> map = new HashMap<String, Object>();
map.put("class", "com.test.PersonImpl");
map.put("name", "jiaduo");
map.put("password", "password");
result = userService.$invoke("testPojo", new String[] { "com.test.Person" }, new Object[] { map });
System.out.println((result));
}
}
这里由于 sayHello 的参数是 String,没有很好的体现参数转换为 Map,下面我们具体来说下 POJO 参数转换 Map 的含义。
比如服务提供者提供的一个接口的 testPojo(Person person) 方法的参数为如下 POJO:
package com.test;public class PersonImpl implements Person {private String name;private String password;public String getName() {return name;
}public void setName(String name) {this.name = name;
}public String getPassword() {return password;
}public void setPassword(String password) {this.password = password;
}
}
则 POJO 数据:
Person person = new PersonImpl();
person.setName("jiaduo");
person.setPassword("password");
正常情况下调用接口是使用:
servicePerson.testPojo(person);
泛化调用下需要首先转换 person 为 Map 后如下表示:
Map<String, Object> map = new HashMap<String, Object>();// 注意:如果参数类型是接口,或者List等丢失泛型,可通过class属性指定类型。map.put("class", "com.test.PersonImpl");
map.put("name", "jiaduo");
map.put("password", "password");
然后使用下面方法进行泛化调用:
servicePerson.$invoke("testPojo", new String[]
{"com.test.Person"}, new Object[]{map});
泛化调用通常用于框架集成,比如:实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现,而不需要依赖服务实现方提供的接口类以及接口的入参和出参的 POJO 类。
服务消费端异步调用
无论前面我们讲解的正常调用还是泛化调用也好,都是进行同步调用的,也就是服务消费方发起一个远程调用后,调用线程要被阻塞挂起,直到服务提供方返回。
本节来讲解下服务消费端异步调用,异步调用是指服务消费方发起一个远程调用后,不等服务提供方返回结果,调用方法就返回了,也就是当前线程不会被阻塞,这就允许调用方同时调用多个远程方法。