dubbo的介绍大家自行百度,下面以一个demo作为dubbo学习的开始
DUBBO 入门例子 - 一个服务一个实现
dubbo版本2.5.8
注册中心使用zookeeper,版本3.4
代码结构如下
api是接口服务定义
consumer是服务消费者
provider是服务生产者
api代码如下
public interface DemoService {
String sayHi(String name);
}
只是用来定义服务接口,代码很简单
provider代码如下
接口实现类
public class ProviderService implements DemoService {
@Override
public String sayHi(String name) {
return "provider " + name;
}
}
启动类
public class Provider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"provider.xml"});
context.start();
System.in.read();
}
}
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://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="hello-world-app" />
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="demoService" />
<!-- 和本地bean一样实现服务 -->
<bean id="demoService" class="com.tiger.dubbo.provider.ProviderService" />
</beans>
pom文件如下,consumer类似
<?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>dubboooo</artifactId>
<groupId>tiger</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>provider</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.8</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<dependency>
<groupId>tiger</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
consumer代码如下
public class Consumer {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"consumer.xml"});
context.start();
DemoService service = (DemoService) context.getBean("demoService");
System.out.println(service.sayHi("wawa"));
}
}
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://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="consumer-of-helloworld-app" />
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="demoService" interface="com.tiger.dubbo.api.DemoService" />
</beans>
依次启动zookeeper、provider、consumer,consumer控制台显示“provider wawa”则成功。
例子 - 一个服务多个实现
以上是dubbo最简单的demo,下面在demo的基础上做一些简答的修改。
上面的代码是provider提供接口实现,然后由consumer消费。如果同一个接口有多个实现consumer是如何区分的?
增加新的接口实现
public class SecondProviderService implements DemoService {
@Override
public String sayHi(String name) {
return "second " + name;
}
}
在provider.xml文件中暴露新的实现
<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="demoService" />
<bean id="demoService" class="com.tiger.dubbo.provider.ProviderService" />
<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="secService" />
<bean id="secService" class="com.tiger.dubbo.provider.SecondProviderService" />
现在的问题就是consumer如何区分两个实现。
dubbo是通过分组概念来区分的,在service配置文件中增加group参数,区分每个实现,修改后代码如下
<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="demoService" group="first" />
<bean id="demoService" class="com.tiger.dubbo.provider.ProviderService" />
<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="secService" group="second"/>
<bean id="secService" class="com.tiger.dubbo.provider.SecondProviderService" />
通过group参数为first和second来区分两个实现,consumer调用代码不变,只是在配置文件中需要指定消费的是哪个实现
<dubbo:reference id="demoService" interface="com.tiger.dubbo.api.DemoService" group="first" />
运行结果这里就不写了,大家可以自己试试.
学习
在学习dubbo过程中,经常有“我要学习什么的想法”,经常有看完一部分不知道后面要干嘛的时候。总的来说感觉自己缺少“dubbo能干什么?”的明确答案,所以在学习的时候就缺乏学习路线,这也跟自己长期的学习习惯有关:我要学习A技能,上网搜索 “A技能教程”或者“从零开始学A”。这样自己的学习完全依赖他人的总结,所以导致学习的成果同样依赖博客的质量,这就是高耦合啊同学们!!!我的学习质量为什么要依赖他人的学习成果。希望从今天开始改变自己的学习方式,从dubbo开始.
RPC
dubbo是一个RPC框架,在学习这个框架之前我们需要先明白什么是RPC,为什么需要RPC框架,怎么实现RPC框架(what why how)。
RPC是Remote Procedure Call,字面意思就是“远程过程调用”,翻译为“远程程序调用”可能更符合我们程序员。它的功能就是可以在A机器上访问B机器上的方法。
那么为什么需要RPC,随着业务量扩大,单机系统拆分成集群,为了维护性和高可用性的系统服务化,都需要RPC来实现。那么现在对RPC有了大致了解,也知道为什么要用RPC了,但是RPC框架是什么鬼,它有啥用?那么这就要从如何实现RPC讲起了。
RPC框架结构
我们先来想想如何实现一次远程调用,既然是远程调用,首先肯定要处理网络问题,我们可以使用现有协议比如http,或者我们自定义协议;网络问题解决后就要传输数据了,客户端调用服务端方法,需要知道对方地址(ip),调用的方法地址(接口名称,方法名称),参数等。传输这些数据之前就需要对数据进行序列化操作,相对的接收数据后需要反序列化。盗用大佬的一副图片
以及大佬说的:
1)服务消费方(client)调用以本地调用方式调用服务;
2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
3)client stub找到服务地址,并将消息发送到服务端;
4)server stub收到消息后进行解码;
5)server stub根据解码结果调用本地的服务;
6)本地服务执行并将结果返回给server stub;
7)server stub将返回结果打包成消息并发送至消费方;
8)client stub接收到消息,并进行解码;
9)服务消费方得到最终结果。
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。
后面我会按照大佬的逻辑实现一个简单rpc框架。
简单通信
首先是两个项目通过socket的通信,相信大家之前都写过类似的轮子,这里就不贴代码;
特定服务调用
既然两个服务可以通过网络互相通信,那么我们完全可以在客户端发送需要调用的服务类信息、方法信息、参数等。
调用的服务
public interface DemoService {
String sayHi(String name);
}
客户端调用代码如下
public class ClientApp {
public static void main(String[] args) {
try {
RPCClient.start();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
DemoService service = ServiceFactory.createService("demo");
System.out.println(service.sayHi("haha"));
}
}
start是启动了网络访问;后面我们需要假装给接口一个实现类,然后由这个实现类执行目标方法。
根据我们前面的理论,我们需要一个代理类来假装实现类,完成访问远端方法的责任,所以这里我们使用一个静态代理实现(为了简单)。
public class DemoServiceProxy implements DemoService {
public String sayHi(String name){
String msg = "com.tiger.dubbo.api.DemoService-sayHi-java.lang.String-" + name;
try {
return (String) RPCClient.sendMsg(msg);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
代理类非常简单;msg中的信息是传给服务端的,服务端会解析信息中的内容做相应的处理,这里我们简单处理,定义参数间用“-”隔开,第一个为服务类名称,第二个为方法名,第三个为参数类,第四个为参数值(对,只支持一个参数),这就相当于一个简单的协议。
服务端这边也很简单,根据协议解析信息,并做处理代码如下:
public static Object handleMsg(String msg)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
InstantiationException, InvocationTargetException {
String[] msgArr = msg.split("-");
String klassName = msgArr[0];
if ("com.tiger.dubbo.api.DemoService".equals(klassName))
klassName = "com.togo.service.DemoServiceImpl";
Class klass = Class.forName(klassName);
Class param = Class.forName(msgArr[2]);
Method method = klass.getMethod(msgArr[1], param);
return method.invoke(klass.newInstance(), msgArr[3]);
}
看到我的实现类获取方式有没有某个部位突然发紧。Orz
根据我们的协议,一些信息,并执行对应方法,将信息返回。
至此有翔以来最吊的rpc完成了~~撒花~~
好吧~继续完善,并对照dubbo等框架学习~作者还是很认真的 ~.~
信息
在上面我们已经实现了一个基本完美的rpc框架,但是我们不能骄傲,需要不断完善。
首先我们来处理下消息传递的问题,之前是以‘-’分割字符串,只能传递一个参数。现在我们把它修改为json格式传递。
这里json选择使用阿里的fastjson,啊,真是强强联和~~
maven:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
定义传输类Message
public class Message {
/**
* 服务类名称
*/
private String klassName;
/**
* 服务别名
*/
private String alias;
/**
* 方法名称
*/
private String methodName;
/**
* 方法参数类型
*/
private Class<?>[] parameterKlassNameArrays;
/**
* 方法参数值
*/
private Object[] parameterArrays;
}
客户端设置好参数后格式化为json格式传输
JSONObject.toJSONString(message)
然后服务端接收到后解析
Message message = JSONObject.parseObject(msg, Message.class);
然后按照对应字段继续执行。
啊~破费
等等,服务端匹配服务还是写死名称的,嗯,有点low,不是,有点不合适~~
服务实现自动加载
我们可以使用注解标注需要加载的实现类,代码如下
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
String name() default "";
}
@Service
public class DemoServiceImpl implements DemoService {
public String sayHi(String name) {
return "hi " + name;
}
}
那现在就需要扫描到这个类,并把这个类保存下来。
从根路径开始扫描
String root = URLDecoder.decode(RPCServer.class.getResource("/").getPath(), String.valueOf(Charset.defaultCharset()));
private static void scan(String root) {
System.out.println("start scan");
File file = new File(root);
allFiles(file, root);
loadImpl();
}
private static void allFiles(File file, String root) {
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files == null)
return;
for (File f : files) {
if (f.isDirectory())
allFiles(f, root);
else {
String path = f.getAbsolutePath();
Context.INSTANCE.addFile(handlePathToClass(path, root));
}
}
}
}
private static void allFiles(File file, String root) {
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files == null)
return;
for (File f : files) {
if (f.isDirectory())
allFiles(f, root);
else {
String path = f.getAbsolutePath();
Context.INSTANCE.addFile(handlePathToClass(path, root));
}
}
}
}
整体思路就是从根路径开始,如果碰到目录则搜索目录下文件,然后把所有的文件路径记录下来。
handlePathToClass是用来处理路径为类全限定名
private static String handlePathToClass(String path, String root) {
path = path.substring(root.length());
path = path.replace('/', '.');
return path.substring(0, path.length() - ".class".length());
}
这里的Context是我们创建的上下文,用于记录扫描信息(代码在git上看吧,不贴了)。
扫描完所有类之后,我们对扫描结果进行处理,将其中标注了servic注解的类放到Context中。
到这里服务端的东西就差不多了。再来看看客户端
动态代理
目前客户端使用一个静态代理来代理接口,静态的缺点很明显,就是。。。不高端,作为一个高大上的框架,怎么能用静态的,必须动态的。
使用Java自带的动态代理实现目标接口的代理类,在代理类中实现远程调用服务端服务逻辑,然后神不知鬼不觉的把结果返回给客户端。
主要代码如下
public class RemoteProxy<T> implements InvocationHandler {
private Class<T> klass;
private String alias;
public RemoteProxy(Class<T> klass, String alias) {
this.klass = klass;
this.alias = alias;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Message message = new Message();
message.setKlassName(klass.getName());
message.setAlias(alias);
message.setMethodName(method.getName());
message.setParameterKlassNameArrays(method.getParameterTypes());
message.setParameterArrays(args);
return RPCClient.sendMsg(JSONObject.toJSONString(message));
}
}
public class ServiceFactory {
public static <T> T createService(Class<T> klass, String alias) {
if (klass == null)
return null;
RemoteProxy<T> rp = new RemoteProxy<T>(klass, alias);
Object subject = Proxy.newProxyInstance(rp.getClass().getClassLoader(), new Class[]{klass}, rp);
return (T) subject;
}
}
然后客户端就可以非常简单的远程调用了
DemoService service = ServiceFactory.createService(DemoService.class, "multi");
System.out.println(service.sayHi("haha"));
一个服务多个实现
这种需求还是比较常见的,客户端指定需要使用的服务别名就可以使用不同的服务。在我们的Service注解中有name字段,该字段就标注了该服务的别名。
@Service(name = "multi")
public class MultiDemoServiceImpl implements DemoService {
@Override
public String sayHi(String name) {
return "multi " + name;
}
}
然后在扫描类之后的加载服务阶段会根据name字段作为查找类的key的一部分。
Key key = new Key(c.getName());
Service service = (Service) klass.getDeclaredAnnotation(Service.class);
if (StringUtil.isNotEmpty(service.name())) {
key.setAlias(service.name());
}
Context.INSTANCE.addServiceImpl(key, path);
然后客户端只需要在调用的时候指定alias就好了。
鼓掌~.~