在之前的文章中介绍过了基于Spring 4 + Dubbo 的注解配置。参见《改造Dubbo,使其能够兼容Spring 4注解配置》。
但是,在使用中会发现,Dubbo仅仅提供了两个注解:
com.alibaba.dubbo.config.annotation.Reference
com.alibaba.dubbo.config.annotation.Service
而且,这两个注解中仅仅包含了基本的配置项。导致有一些配置无法通过注解方式完成。比如:<dubbo:method>
,这个常用的配置,就没有提供对应的注解。
1.分析
在《改造Dubbo,使其能够兼容Spring 4注解配置》中,曾经分析过,Dubbo的注解完全是由AnnotationBean
来完成的。 那先来看看AnnotationBean
是怎么处理配置的:
- public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
-
- ...
-
- Field[] fields = bean.getClass().getDeclaredFields();
- for (Field field : fields) {
- try {
- if (! field.isAccessible()) {
- field.setAccessible(true);
- }
- Reference reference = field.getAnnotation(Reference.class);
- if (reference != null) {
- Object value = refer(reference, field.getType());
- if (value != null) {
- field.set(bean, value);
- }
- }
- } catch (Throwable e) {
- logger.error("Failed to init remote service reference at filed " + field.getName() + " in class " + bean.getClass().getName() + ", cause: " + e.getMessage(), e);
- }
- }
-
- ...
-
- }
其实AnnotationBean
中有好几处类似这样的代码。其实逻辑都大同小异:
- 在类、方法、属性上查找
Reference
、Service
注解; - 将注解转换成对应的
com.alibaba.dubbo.config.spring.ReferenceBean
或者com.alibaba.dubbo.config.spring.ServiceBean
那再来看看ReferenceBean
- public class ReferenceBean<T>
- extends ReferenceConfig<T>
- implements FactoryBean, ApplicationContextAware, InitializingBean, DisposableBean
这个是继承ReferenceConfig
,而这个ReferenceConfig
是带有Method配置信息的:
- public class ReferenceConfig<T> extends AbstractReferenceConfig {
-
- ...
- // 方法配置
- private List<MethodConfig> methods;
- ...
-
- }
但是注解处理过程中并没有对这个属性进行赋值处理,追踪一下AnnotationBean
,就会发现配置对象的处理是交由refer
方法完成的:
- private Object refer(Reference reference, Class<?> referenceClass) { //method.getParameterTypes()[0]
-
- ...
- String key = reference.group() + "/" + interfaceName + ":" + reference.version();
- ReferenceBean<?> referenceConfig = referenceConfigs.get(key);
- if (referenceConfig == null) {
- referenceConfig = new ReferenceBean<Object>(reference);
- ...
- }
- referenceConfigs.putIfAbsent(key, referenceConfig);
- referenceConfig = referenceConfigs.get(key);
- }
- return referenceConfig.get();
- }
ReferenceBean
的构造器中传入了注解对象,并最终交由com.alibaba.dubbo.config.AbstractConfig#appendAnnotation
方法处理:
- protected void appendAnnotation(Class<?> annotationClass, Object annotation) {
- Method[] methods = annotationClass.getMethods();
- for (Method method : methods) {
- if (method.getDeclaringClass() != Object.class
- && method.getReturnType() != void.class
- && method.getParameterTypes().length == 0
- && Modifier.isPublic(method.getModifiers())
- && ! Modifier.isStatic(method.getModifiers())) {
- try {
- String property = method.getName();
- if ("interfaceClass".equals(property) || "interfaceName".equals(property)) {
- property = "interface";
- }
- String setter = "set" + property.substring(0, 1).toUpperCase() + property.substring(1);
- Object value = method.invoke(annotation, new Object[0]);
- if (value != null && ! value.equals(method.getDefaultValue())) {
- Class<?> parameterType = ReflectUtils.getBoxedClass(method.getReturnType());
- if ("filter".equals(property) || "listener".equals(property)) {
- parameterType = String.class;
- value = StringUtils.join((String[]) value, ",");
- } else if ("parameters".equals(property)) {
- parameterType = Map.class;
- value = CollectionUtils.toStringMap((String[]) value);
- }
- try {
- Method setterMethod = getClass().getMethod(setter, new Class<?>[] { parameterType });
- setterMethod.invoke(this, new Object[] { value });
- } catch (NoSuchMethodException e) {
- // ignore
- }
- }
- } catch (Throwable e) {
- logger.error(e.getMessage(), e);
- }
- }
- }
- }
这个方法,也比较简单,就是将注解中的各个方法找出来,加上set
然后反射执行。
另一个BUG
分析这个方法,还会发现Dubbo中另一个Bug:if (value != null && ! value.equals(method.getDefaultValue())){}
只有当注解中的配置项的值与默认值不一致才会生效
比如:启动时检查 按Dubbo的文档中描述:
Dubbo缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止Spring初始化完成,以便上线时,能及早发现问题,默认check=true。
但是,com.alibaba.dubbo.config.annotation.Reference#check
默认值是false。
2.方案
所以,如果想通过注解完成这个动作,就需要自己动手解决了。 思路有以下几个:
- 扩展
com.alibaba.dubbo.config.annotation.Reference
注解。 - 完全使用自定义注解来完成。
在《改造Dubbo,使其能够兼容Spring 4注解配置》中,曾经分析过,Dubbo的注解完全是由AnnotationBean
来完成的。所以,以上两种方式,都是需要修改AnnotationBean
的处理逻辑。
而com.alibaba.dubbo.config.annotation.Reference
并没有加上@Inherited
,所以,要想不修改Dubbo源码来扩展Reference
是难以操作的。
如果在完全替换,使用自定义注解。那就会牵连着要改动ReferenceBean
,ReferenceConfig
。
所以,只好在保留Reference
体系的基础上,叠加上自定义处理过程了。
3.实施
自定义注解:
InterfaceMethods:
- package com.roc.dubbo.config.spring.annotation;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- /**
- * Created by roc on 2017/3/30.
- */
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.METHOD})
- public @interface InterfaceMethods {
- Method[] methods() default {};
- }
-
Method:
- package com.roc.dubbo.config.spring.annotation;
-
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
-
- /**
- * Dubbo方法配置
- * Created by roc on 2017/3/30.
- */
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Method {
-
- /**
- * @return 方法名
- */
- String name();
-
- /**
- * @return 负载均衡策略
- */
- String loadbalance() default "";
-
- /**
- * @return 远程服务调用重试次数
- */
- int retries() default -1;
-
- /**
- * @return 方法使用线程数限制
- */
- int executes() default 0;
-
- /**
- * @return 每服务消费者最大并发调用限制
- */
- int actives() default 0;
-
- /**
- * @return 是否过时
- */
- boolean deprecated() default false;
-
- /**
- * @return 是否需要开启stiky策略
- */
- boolean sticky() default false;
-
- /**
- * @return 是否需要返回
- */
- boolean isReturn() default true;
-
- /**
- * @return 方法调用超时时间(毫秒)
- */
- int timeout() default -1;
- }
继上次《改造Dubbo,使其能够兼容Spring 4注解配置》扩展的com.roc.dubbo.config.spring.AnnotationBean
的基础上,添加自定义注解的处理逻辑:
增加:getMethods方法
- private com.roc.dubbo.config.spring.annotation.Method[] getMethods(AccessibleObject accessibleObject) {
- InterfaceMethods interfaceMethods = accessibleObject.getAnnotation(InterfaceMethods.class);
- com.roc.dubbo.config.spring.annotation.Method[] methodArray = null;
- if (interfaceMethods != null) {
- methodArray = interfaceMethods.methods();
- }
- return methodArray;
- }
修改:refer方法
- private Object refer(Reference reference, Class<?> referenceClass, com.roc.dubbo.config.spring.annotation.Method[] methodArray) { // method.getParameterTypes()[0]
-
- .......
-
- String methodKey = "&";
- if (methodArray != null) {
- for (int i = 0; i < methodArray.length; i++) {
- methodKey += methodArray[i] + "&";
- }
- }
-
- String key = reference.group() + "/" + interfaceName + ":" + reference.version() + methodKey;
- ReferenceBean<?> referenceConfig = referenceConfigs.get(key);
- if (referenceConfig == null) {
- referenceConfig = new ReferenceBean<Object>(reference);
- //修复check默认值的BUG
- // referenceConfig.setCheck(reference.check());
- // >>>>>>>>>>>>>>>>>>>>>>>>
- if (void.class.equals(reference.interfaceClass()) && "".equals(reference.interfaceName())
- && referenceClass.isInterface()) {
- referenceConfig.setInterface(referenceClass);
- }
-
- if (methodArray != null && methodArray.length > 0) {
- List<MethodConfig> methodConfigList = new ArrayList<>();
- for (int i = 0; i < methodArray.length; i++) {
- com.roc.dubbo.config.spring.annotation.Method m = methodArray[i];
- MethodConfig mc = new MethodConfig();
- mc.setName(m.name());
- mc.setDeprecated(m.deprecated());
- mc.setExecutes(m.executes());
- if (m.retries() >= 0) {
- mc.setRetries(m.retries());
- }
- if (StringUtils.isNotEmpty(m.loadbalance())) {
- mc.setLoadbalance(m.loadbalance());
- }
- if(m.timeout() >= 0){
- mc.setTimeout(m.timeout());
- }
- mc.setActives(m.actives());
- mc.setSticky(m.sticky());
- mc.setReturn(m.isReturn());
- methodConfigList.add(mc);
-
- }
- referenceConfig.setMethods(methodConfigList);
- }
-
- .........
- }
- referenceConfigs.putIfAbsent(key, referenceConfig);
- referenceConfig = referenceConfigs.get(key);
- }
- return referenceConfig.get();
- }
这样,在使用时需要同时使用Dubbo注解和自定义注解:
- @Reference
- @InterfaceMethods(methods = {@Method(name = "applyPay", retries = 0, timeout = 3000)})
- private PayService payCenter;
通过这样改动,即可在不改动Dubbo源码的基础上(不用对Dubbo重编译打包)加上自定义注解处理逻辑。
4 其他解题思路
上面通过自定义注解,并在AnnotationBean
中添加额外逻辑来处理。如果仅仅是为了加上方法超时,重试等配置参数,仍然感觉比较繁杂。
其实,Dubbo的服务注册、引用处理最终表现形式就是就是注册中心的URL。 例如:
consumer://127.0.0.1/com.roc.pay.service.PayService?application=egege_web&applyPay.timeout=4000&category=consumers&check=false&default.check=false&default.group=TEST&dubbo=2.5.3&interface=com.roc.pay.service.PayService&methods=applyBatchPay,applyPay&organization=com.egege&owner=egege&pid=1303&revision=0.1.1&side=consumer×tamp=1490867465568
URL中对于applyPay
方法的超时配置表现为:applyPay.timeout=4000
这样就说明各种配置,最终都是转换成了URL的参数。
所以,我们其实可以通过Dubbo注解中的parameters
属性来直接操作URL:
- @Reference(parameters = {"applyPay.timeout","4000"})
- private PayService payCenter;
这样即可。