背景
一般我们在使用dubbo进行服务调用时,都会将服务提供者的api进行打jar包提供给消费者,这样消费者可以直接引用,但是考虑到如下两种情况。
- 如果dubbo的使用者不是java客户端?
- 服务如果具有网关层,网关转发请求到后端如果依赖后端jar,网关发布频繁,稳定性怎么保证? 为了解决上面的问题,dubbo设计了泛化能力,简单来说就是客户端可以不依赖生产者api。
使用介绍
我们先介绍个简单的例子,之后再来说下原理和实现。
生产者配置
//xml
<dubbo:service interface="com.poizon.study.api.service.HelloService" ref="helloService"/>
//服务实现类
@Service
public class HelloServiceImpl implements HelloService {
@Override
public WishRequest sayHelloToDubbo(String name) throws IOException {
WishRequest wishRequest = new WishRequest();
wishRequest.setMoney(1L);
wishRequest.setMsg("hello to dubbo");
return wishRequest;
}
@Override
public String sayHappyNewYear(WishRequest wish) { return "thank you!"; }
}
服务消费者
//xml
<dubbo:reference generic="true" id="helloService" interface="com.poizon.study.api.service.HelloService" />
public static void main(String[] args) throws IOException, InterruptedException {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("dubbo-consumer.xml");
ctx.start();
GenericService helloService = ctx.getBean("helloService", GenericService.class);
Object sayHelloToDubbo = helloService.$invoke("sayHelloToDubbo", new String[]{"java.lang.String"}, new Object[]{"jack"});
System.out.println(sayHelloToDubbo);
//print:{msg=hello to dubbo, money=1, class=com.poizon.study.api.service.request.WishRequest, age=null}
}
消费者代码中xml的interface 配置的string 接口名称在消费端是不存在的如果安装了idea会报红,我们无需理会,这里唯一的区别在
generic=true参数,main函数中将bean都转换为GenericService对象,然后通过他的$invoke($invokeAsync) 去调用我们期望调用的方法名,第二个参数为参数列表,第三个参数为传参,自定义对象可以通过map的形式。
原理分析
调用成功了,dubbo实现泛化主要通过两个filter(之前文章有分析过),我们这边单独把这两个过滤器拿出来分析下。
- GenericFilter(生产者侧)
- GenericImplFilter(消费者侧配置了
generic=true才会加载)
GenericImplFilter
//org.apache.dubbo.rpc.filter.GenericImplFilter#invoke
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String generic = invoker.getUrl().getParameter(GENERIC_KEY);
if ((invocation.getMethodName().equals($INVOKE) || invocation.getMethodName().equals($INVOKE_ASYNC))
&& invocation.getArguments() != null
&& invocation.getArguments().length == 3
&& ProtocolUtils.isGeneric(generic)) {
Object[] args = (Object[]) invocation.getArguments()[2];
if (ProtocolUtils.isJavaGenericSerialization(generic)) {
for (Object arg : args) {
if (!(byte[].class == arg.getClass())) {
error(generic, byte[].class.getName(), arg.getClass().getName());
}
}
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
for (Object arg : args) {
if (!(arg instanceof JavaBeanDescriptor)) {
error(generic, JavaBeanDescriptor.class.getName(), arg.getClass().getName());
}
}
}
invocation.setAttachment(
GENERIC_KEY, invoker.getUrl().getParameter(GENERIC_KEY));//1
}
return invoker.invoke(invocation);
}
过滤器的作用主要是判断客服端序列化类型,注释1处将序列化标识通过attachment(dubbo附加信息参数) 传递给了生产者。
GenericFilter
//org.apache.dubbo.rpc.filter.GenericFilter#invoke
public Result invoke(Invoker<?> invoker, Invocation inv) {
if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
&& inv.getArguments() != null
&& inv.getArguments().length == 3
&& !GenericService.class.isAssignableFrom(invoker.getInterface())) {//1
String name = ((String) inv.getArguments()[0]).trim();
String[] types = (String[]) inv.getArguments()[1];
Object[] args = (Object[]) inv.getArguments()[2];
try {
Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
Class<?>[] params = method.getParameterTypes();
if (args == null) {
args = new Object[params.length];
}
String generic = inv.getAttachment(GENERIC_KEY);
if (StringUtils.isBlank(generic)) {
generic = RpcContext.getContext().getAttachment(GENERIC_KEY);
}
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)) {//2
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
//...
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
//...
} else if (ProtocolUtils.isProtobufGenericSerialization(generic)) {
//...
}
return invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));//3
} catch (ClassNotFoundException e) {
throw new RpcException(e.getMessage(), e);
}
}
return invoker.invoke(inv);
}
消费者filter中,注释1判断入参方法名是否为$invoke 类名,参数个数等,然后通过这些参数创建出后续需要的method对象,注释2处是默认的序列化方式,注释3处创建新的RpcInvocation(此时方法名已经修改为我们调用$invoke传过来的第一个参数既 sayHelloToDubbo)对象作为调用链的参数。
思考?
我们将消费者调用的方法名改成一个实际不存在的名字 sayHelloToDubbo2
public static void main(String[] args) throws IOException, InterruptedException {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("dubbo-consumer.xml");
ctx.start();
GenericService helloService = ctx.getBean("helloService", GenericService.class);
Object sayHelloToDubbo = helloService.$invoke("sayHelloToDubbo2", new String[]{"java.lang.String"}, new Object[]{"jack"});
System.out.println(sayHelloToDubbo);
Exception in thread "main" org.apache.dubbo.rpc.RpcException:
Failed to invoke the method sayHelloToDubbo2 in the
service org.apache.dubbo.rpc.service.GenericService.
Tried 3 times of the providers [192.168.2.172:20880] (1/1) from the registry localhost:2181 on the consumer 192.168.2.172 using the dubbo version 2.7.3. Last error is: Failed to invoke remote method: $invoke, provider: dubbo://192.168.2.172:20880/com.poizon.study.api.service.HelloService?anyhost=true&application=dubbo-provider&bean.name=com.poizon.study.api.service.HelloService&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=true&interface=com.poizon.study.api.service.HelloService&lazy=false&logger=slf4j&methods=sayHappyNewYear,sayHelloToDubbo&pid=81910®ister=true®ister.ip=192.168.2.172&release=2.7.3&remote.application=dubbo-provider&side=consumer&sticky=false×tamp=1612017946014, cause: org.apache.dubbo.remoting.RemotingException: org.apache.dubbo.rpc.RpcException: com.poizon.study.api.service.HelloService.sayHelloToDubbo2(java.lang.String)
org.apache.dubbo.rpc.RpcException: com.poizon.study.api.service.HelloService.sayHelloToDubbo2(java.lang.String)
at org.apache.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:143)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:82)
at org.apache.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38)
//....
报错是意料之中的,但是笔者想表达的是Tried 3 times 这个错误,那么消费者为什么要调用三次呢?大家可以在idea中验证下,如果有兴趣把参数种类也换下比如用string 代替 integer 看看是否错误还是Tried 3 times,那么这种错误我们能否避免(可以考虑下RpcException.isBiz())?
总结
泛化目前在springboot + dubbo 生态中,网关使用还是很频繁的大家有必要深入学习。