dubbo 注解的使用

使用非常简单,下面贴出关键部分

//provider的配置文件
<?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.xsd          
    http://code.alibabatech.com/schema/dubbo          
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd">  
    <!-- 提供方应用信息,用于计算依赖关系 -->  
    <dubbo:application name="provider-app"  />  
    <!-- 使用zookeeper注册中心暴露服务地址 -->  
    <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" />
    <!-- 用dubbo协议在20880端口暴露服务 -->  
    <dubbo:protocol name="dubbo" port="20880" />
 
    <dubbo:annotation package="com.i.springboot.controller" />
</beans>
//服务接口类
@com.alibaba.dubbo.config.annotation.Service
public class PostService {
    ………………省略……………………
}
 
//consumer端配置
<?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.xsd          
    http://code.alibabatech.com/schema/dubbo          
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd">  
    <!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->  
    <dubbo:application name="consumer-app"/>  
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />
    <dubbo:annotation package="com.i.springboot.controller"/>
</beans>  
 
//服务引用
@Controller
@RequestMapping("/post")
@SessionAttributes("currentUser")
public class PostController {
    //注解使用dubbo服务端服务
    @Reference
    PostService postService;
    ……………省略……………
}

以上如果只是用 spring 的容器,而不使用 springmvc 进行结合使用时是不会出现引用为空的问题的;但是如果不了解 spring 和 springmvc 加载配置文件和初始化 bean 的流程,则极有可能出现 postService 为 Null 的情况,错误配置如下

我 src/main/resources 下面有 spring-consumer.xml、spring-mvc.xm 两个配置文件,spring-mvc.xml 和 web.xml 配置部分如下

<?xml version="1.0" encoding="UTF-8"?>    
<beans xmlns="http://www.springframework.org/schema/beans"   
    xmlns:context="http://www.springframework.org/schema/context"   
    xmlns:mvc="http://www.springframework.org/schema/mvc"  
    xmlns:p="http://www.springframework.org/schema/p"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    
    xsi:schemaLocation="http://www.springframework.org/schema/beans   
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
    http://www.springframework.org/schema/context  
    http://www.springframework.org/schema/context/spring-context.xsd  
    http://www.springframework.org/schema/mvc   
    http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> 

    <!-- 自动扫描controller包下的所有类,使其认为spring mvc的控制器 -->
    <context:component-scan base-package="com.i.springboot.controller" />
    ……………………省略…………………………
</beans>

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        classpath*:/spring-*.xml
    </param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

初步一看完全没有问题,spring 启动的时候会扫描 controller 包,然后初始化我的 postService 服务类,一切想的都是那么美好,程序一运行,一个大大的空指针异常抛出,然后网上、qq 上一堆乱问,终不得解,生无可恋,下面一步步解决问题
开始之前提出如下疑问

  1. spring 是什么时候跟 dubbo 勾搭在一起的
  2. 为什么 PostService 上面加上 dubbo 的 Service 注解,服务类就会被加载
  3. 为什么 dubbo 官方文件所说的通过 Reference 引用服务却为 NULL

dubbo 和 spring 结合

到底 dubbo 是怎么和 spring 组合的呢,先看如下

<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.xsd          

    http://code.alibabatech.com/schema/dubbo          
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

注意到 xmlns:dubbo=”http://code.alibabatech.com/schema/dubbo” 了吗,仔细想想 spring 怎么解析 xml 配置文件的,NamespaceHandlerNamespaceHandlerSupportBeanDefinitionParser,spring 提供的一种 SPI 规范,dubbo 定义了自己的 schema、namespacehandler、beandefinitionparser

/**
 * DubboNamespaceHandler
 * 
 * @author william.liangf
 * @export
 */
public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
    }

}

DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions,xml 配置文件就这样开始解析,具体不做描述,主要看 dubbo 是如何搭上 spring 的车的,解析过程咱们重点看 AnnotationsBean 的解析,也就是 dubbo 的 DubboBeanDefinitionParser

dubbo 的 service、reference 是如何初始化的

DubboBeanDefinitionParser 的 parse 方法被调用后,dubbo 定义的几个大的标签 application、registry、provider、consumer、annotation 等都会被初始化,并包装成 RootBeanDefinition 在 spring 的 bean 容器中

@SuppressWarnings("unchecked")
    private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClass(beanClass);
        beanDefinition.setLazyInit(false);
       ……………………………省略具体解析代码…………………………………
        return beanDefinition;
    }

重点看 annotation 实例化过程,也是 dubbo 注解的关键,进入此类,看到一长串的类

public class AnnotationBean extends AbstractConfig implements DisposableBean, BeanFactoryPostProcessor, BeanPostProcessor, ApplicationContextAware {

不得不提下 spring 的 processer 和 aware 两个 SPI 点,设计的非常巧妙,完全符合设计六大原则之一的**开闭原则**

//任选一种都支持process和aware两种方式
<context:annotation-config/>
<context:component-scan base-package="xx.xx.xx" />

dubbo 充分利用了 spring 提供的机制进行 service 的初始化和 reference 的实例化的

service 的实例化过程

第一步

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {
        if (annotationPackage == null || annotationPackage.length() == 0) {
            return;
        }
        if (beanFactory instanceof BeanDefinitionRegistry) {
            try {
                // init scanner
                Class<?> scannerClass = ReflectUtils.forName("org.springframework.context.annotation.ClassPathBeanDefinitionScanner");
                Object scanner = scannerClass.getConstructor(new Class<?>[] {BeanDefinitionRegistry.class, boolean.class}).newInstance(new Object[] {(BeanDefinitionRegistry) beanFactory, true});
                // add filter
                Class<?> filterClass = ReflectUtils.forName("org.springframework.core.type.filter.AnnotationTypeFilter");
                Object filter = filterClass.getConstructor(Class.class).newInstance(Service.class);
                Method addIncludeFilter = scannerClass.getMethod("addIncludeFilter", ReflectUtils.forName("org.springframework.core.type.filter.TypeFilter"));
                addIncludeFilter.invoke(scanner, filter);
                // scan packages
                String[] packages = Constants.COMMA_SPLIT_PATTERN.split(annotationPackage);
                Method scan = scannerClass.getMethod("scan", new Class<?>[]{String[].class});
                scan.invoke(scanner, new Object[] {packages});
            } catch (Throwable e) {
                // spring 2.0
            }
        }
    }

实例化一个 ClassPathBeanDefinitionScanner 类,通过反射实现有参构造的初始化,将注册 bean 的 (BeanDefinitionRegistry) beanFactory 作为参数传递给实例,最后反射调用 scanner 的 scan 方法,将 service 注解的实例增加至 spring 容器中
第二步

public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        //判断是不是dubbo需要处理的bean类,如果是则继续进行处理,不是则不做任何处理
        if (! isMatchPackage(bean)) {
            return bean;
        }
        Service service = bean.getClass().getAnnotation(Service.class);
        if (service != null) {
            ServiceBean<Object> serviceConfig = new ServiceBean<Object>(service);
            if (void.class.equals(service.interfaceClass())
                    && "".equals(service.interfaceName())) {
                if (bean.getClass().getInterfaces().length > 0) {
                    serviceConfig.setInterface(bean.getClass().getInterfaces()[0]);
                } else {
                    throw new IllegalStateException("Failed to export remote service class " + bean.getClass().getName() + ", cause: The @Service undefined interfaceClass or interfaceName, and the service class unimplemented any interfaces.");
                }
            }
            if (applicationContext != null) {
                serviceConfig.setApplicationContext(applicationContext);
                if (service.registry() != null && service.registry().length > 0) {
                    List<RegistryConfig> registryConfigs = new ArrayList<RegistryConfig>();
                    for (String registryId : service.registry()) {
                        if (registryId != null && registryId.length() > 0) {
                            registryConfigs.add((RegistryConfig)applicationContext.getBean(registryId, RegistryConfig.class));
                        }
                    }
                    serviceConfig.setRegistries(registryConfigs);
                }
                if (service.provider() != null && service.provider().length() > 0) {
                    serviceConfig.setProvider((ProviderConfig)applicationContext.getBean(service.provider(),ProviderConfig.class));
                }
                if (service.monitor() != null && service.monitor().length() > 0) {
                    serviceConfig.setMonitor((MonitorConfig)applicationContext.getBean(service.monitor(), MonitorConfig.class));
                }
                if (service.application() != null && service.application().length() > 0) {
                    serviceConfig.setApplication((ApplicationConfig)applicationContext.getBean(service.application(), ApplicationConfig.class));
                }
                if (service.module() != null && service.module().length() > 0) {
                    serviceConfig.setModule((ModuleConfig)applicationContext.getBean(service.module(), ModuleConfig.class));
                }
                if (service.provider() != null && service.provider().length() > 0) {
                    serviceConfig.setProvider((ProviderConfig)applicationContext.getBean(service.provider(), ProviderConfig.class));
                } else {

                }
                if (service.protocol() != null && service.protocol().length > 0) {
                    List<ProtocolConfig> protocolConfigs = new ArrayList<ProtocolConfig>();
                    for (String protocolId : service.registry()) {
                        if (protocolId != null && protocolId.length() > 0) {
                            protocolConfigs.add((ProtocolConfig)applicationContext.getBean(protocolId, ProtocolConfig.class));
                        }
                    }
                    serviceConfig.setProtocols(protocolConfigs);
                }
                try {
                    serviceConfig.afterPropertiesSet();
                } catch (RuntimeException e) {
                    throw (RuntimeException) e;
                } catch (Exception e) {
                    throw new IllegalStateException(e.getMessage(), e);
                }
            }
            //
            serviceConfig.setRef(bean);
            serviceConfigs.add(serviceConfig);
            //通过注册中心暴露dubbo的服务
            serviceConfig.export();
        }
        //返还spring容器,有点类似于装饰
        return bean;
    }

至此,dubbo 通过 service 注解实现 spring 容器管理完毕

reference 实例过程

reference 和 service 最大的不同是,reference 注解生成的实例就没有交给 spring 容器去管理,而只是作为 spring 管理 bean 的一个属性赋值操作,通过反射来实现,代码如下

public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        //和service一样
        if (! isMatchPackage(bean)) {
            return bean;
        }
        Method[] methods = bean.getClass().getMethods();
        for (Method method : methods) {
            String name = method.getName();
            if (name.length() > 3 && name.startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())
                    && ! Modifier.isStatic(method.getModifiers())) {
                try {
                    Reference reference = method.getAnnotation(Reference.class);
                    if (reference != null) {
                        Object value = refer(reference, method.getParameterTypes()[0]);
                        if (value != null) {
                            method.invoke(bean, new Object[] {  });
                        }
                    }
                } catch (Throwable e) {
                    logger.error("Failed to init remote service reference at method " + name + " in class " + bean.getClass().getName() + ", cause: " + e.getMessage(), e);
                }
            }
        }
        Field[] fields = bean.getClass().getDeclaredFields();
        for (Field field : fields) {
            try {
                if (! field.isAccessible()) {
                    field.setAccessible(true);
                }
                //本项目使用的是PostController,field就是controller中的postService,refer方法则是通过连接注册中心,检测服务是否存在,当然如果配置中check为false就不会现在进行检测
                Reference reference = field.getAnnotation(Reference.class);
                if (reference != null) {
                    //refer方法有兴趣可以自己看,牵扯到zk和netty
                    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);
            }
        }
        //将controller中有reference标示的字段赋值后返回,并没有将字段类实例注入spring容器,确实也没有必要
        return bean;
    }

    private Object refer(Reference reference, Class<?> referenceClass) { //method.getParameterTypes()[0]
        String interfaceName;
        if (! "".equals(reference.interfaceName())) {
            interfaceName = reference.interfaceName();
        ……………………省略……………………
        return referenceConfig.get();
    }

至此 reference 标注的实例也初始化完成,service 和 referece 返回的都是 dubbo 的代理类 com.alibaba.dubbo.common.bytecode.Proxy,用到了 jdk 的 Proxy、InvocationHandler 来生成代理类(没有 javassist 的情况),再次声明,reference 标注的对象不会被 spring 容器管理,是无法通过 factory.getBean 获取的

Null 出现的原因

首先得知道 spring 容器初始化过程

public class ContextLoaderListener extends ContextLoader implements ServletContextListener

/**
 * Initialize the root web application context.
 */
@Override
public void contextInitialized(ServletContextEvent event) {
    initWebApplicationContext(event.getServletContext());
}

我配置了监听,会读取 spring-*.xml 配置文件,会初始化一个 xmlwebapplicationcontext 也就是应用的 rootContext 顶级容器,这个容器在 serverletcontext 上下文中,等监听初始化完毕后,我们配置的 dispatcherservlet 开始初始化

/**
     * Overridden method of {@link HttpServletBean}, invoked after any bean properties
     * have been set. Creates this servlet's WebApplicationContext.
     */
    @Override
    protected final void initServletBean() throws ServletException {
        try {
            //初始化mvc的容器
            this.webApplicationContext = initWebApplicationContext();
            initFrameworkServlet();
        }
    }

protected WebApplicationContext initWebApplicationContext() {
        //获取监听初始化的顶级容器
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;

        if (this.webApplicationContext != null) {
            // A context instance was injected at construction time -> use it
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent -> set
                        // the root application context (if any; may be null) as the parent
                        cwac.setParent(rootContext);
                    }
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        if (wac == null) {
            // No context instance was injected at construction time -> see if one
            // has been registered in the servlet context. If one exists, it is assumed
            // that the parent context (if any) has already been set and that the
            // user has performed any initialization such as setting the context id
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            // No context instance is defined for this servlet -> create a local one
            //初始化XmlWebApplicationContext开始,它会读取配置文件spring-mvc.xml
            wac = createWebApplicationContext(rootContext);
        }

        if (!this.refreshEventReceived) {
            // Either the context is not a ConfigurableApplicationContext with refresh
            // support or the context injected at construction time had already been
            // refreshed -> trigger initial onRefresh manually here.
            onRefresh(wac);
        }

        if (this.publishContext) {
            // Publish the context as a servlet context attribute.
            String attrName = getServletContextAttributeName();
            //将mvc容器放入servletcontext上下文中
            getServletContext().setAttribute(attrName, wac);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                        "' as ServletContext attribute with name [" + attrName + "]");
            }
        }

        return wac;
    }

监听初始化的容器读取了所有的配置文件,并初始化了 controller 类,同时将 dubbo 注解 reference 的实例 set 给了 controller,而后 servlet 初始化过程中又再一次读取了 spring-mvc.xml 配置文件,同时也对 controller 进行了初始化,但是与顶级容器初始化不同的是,它没有加载 dubbo 实现的 DubboNamespaceHandler,也就是说 reference 实例化的过程都没有进行,因此在 mvc 容器中的 controller 是没有注入 reference 标注的实例的,因此出现 NULL 的情况

总结

遇到这样的问题是一种幸运,也是一种不幸,幸运的是通过 debug 看源码的过程增加了对 spring 的了解以及框架的优秀设计,不幸的是对于 spring 一些细节的地方还不够了解;遇到问题不放弃、不抛弃,加油