模仿dubbo的与spring无缝集成的RPC演示框架

    Dubbo以前也看过些源码,正好同事写了一个基于netty的通讯架构,想自己试试模仿dubbo,使用此通讯架构写一个RPC框架学习一下。根据百度百科定义:Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和Spring框架无缝集成。我的目标仅是实现一个与spring集成的rpc调用框架。 

    初看了一下dubbo的解析XML标签,平时工作太忙了,写什么自定义标签解析也很麻烦,就没进展了。直到有一天,因为项目中处理mybatis的mapper接口冲突问题,为了解决问题,看了一下mybatis的一点源码,丰富了对与spring整合的技巧。感觉可以结合dubbo与mybatis与spring的整合技巧,用很少的时间,很少的代码写一个RPC框架了。 

  核心功能:客户端动态代理,把接口类,方法,参数类,参数值发过去,服务端按这个找到spring中可用的接口实现,再通过反射调用得到返回值,再传回客户端。 
 

   本文先介绍一下碰到的mybatis冲突问题与解决,再介绍对这个仿dubbo框架的构思,再介绍如何实现客户端与服务端的代码的。最后总结一下,特别是对druid,dubbo学习后的使用体会与实践。由于时间仓促,学艺不精,里面有错误欢迎指正。 

一、mybatis使得中碰到的问题 

    最近的一个应用中,同一个数据库使用了两套mybatis的dao层,一套是老系统遗留的,一套是新做的公共maven包。由于自动生成,只是包不同,mapper接口名都一样,所以类似com.a.userMapper.java与com.b.userMapper.java这样的接口类共存,结果发现spring启动冲突了。 

    先介绍一下IOC容器中规则,从name,或者是别名,或者id,一定只能得到一个bean; 
    从type可以得到一个bean,或者是一组bean。 
    在加载bean的时候,默认有个校验机制,SpringMVC中bean的加载,是采用类似 键值对(key/value)的映射方式存储的,而当中的(key)键,默认是用类名来作为键的(如果不取别名的话)。这样如果不同包路径下的两个组件(controller/service)重名的话就会触发这个校验机制,抛异常。 

Java代码 

  1. protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {  
  2. if (!this.registry.containsBeanDefinition(beanName)) {  
  3. return true;  
  4. }  
  5. BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);  
  6. BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();  
  7. if (originatingDef != null) {  
  8. existingDef = originatingDef;  
  9. }  
  10. if (isCompatible(beanDefinition, existingDef)) {  
  11. return false;  
  12. }  
  13. throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +  
  14. "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +  
  15. "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");  
  16. }  



   网上查到的解决方案都是取别名。但是普通的类可以写别名,可不知道如何介入到mapper接口的实现类中呢?人家是动态生成的。后来看了点文章,spring的IOC容器提供了好几个接口,允许你加入你的控制。按介入的先后顺序,总结如下: 

   1. BeanNameGenerator:beandefination是IOC中最核心的对象了。在生成beandefination时,可以有BeanNameGenerator介入,提供一个产生名字的规则。解决mybatis的那个冲突就是靠这个了。 

   2.BeanFactoryPostProcessor: Spring中BeanFactoryPostProcessor和下面介绍的BeanPostProcessor都是Spring初始化bean时对外暴露的扩展点。PostProcessor部分表示这是一个后置处理接口,相应还有前置处理接口。这里Factory的处理(工厂)肯定比bean产生的早,所以前者早于后者。BeanFactoryPostProcessor确实是在beandefination准备好之后可以介入的,它可以修改beanDefination。 

    3.BeanPostProcessor:它是对从beandefination实例化后的Bean做进一步的操作。比如Aware一些外部的东西,容器事件监听啊什么的。 

    4.最后还有一个InitializingBean接口,它的afterpropetySet接口我经常用。就是当bean生成了,感知aware外部也好了,一切都准备好了。那可能做些初始化的工作了。比如我之前在此方法中会启动通讯组件,或者微核心框架,并把自己引用的实现了微核心中接口的属性类传进去,以便让这些通讯组件或者微核心框架正常工作。都会放在这个接口的方法中实现,spring会调用这个接口方法的。 

    既然知道问题是名字冲突,冲突的同名接口的beanDefination都无法生成,所以只能想办法修改bean名字了,要是可以用包全名肯定不冲突。在spring的配置中,扫描注解对象中可以配置一个BeanNameGenerator,但mabatis中怎么办?只好看源码,从spring-mybatis配置中看到这个类MapperScannerConfigurer,于是点进去看了一个源码,一个熟悉的对象出现了,就是BeanNameGenerator,不正是切入点嘛,于是我在配置的属性中加入自定义的namegenarator类,再用实现BeanFactoryPostProcessor类来打印出所有的beandefination,果然不冲突了,都是包全名。这样冲突解决了,如果一个service中使用上面两个原来冲突的mapper接口,eclipse会提示选择一个,或者写全名。 

    处理好冲突,正好想到mybatis也是根据一个提供的接口,动态代理实现数据库操作嘛。dubbo的客户端也是这样啊,为何mybatis不用xml定义标签呢?那正好学习一下mybatis的方式。 

    mybatis通过MapperScannerConfigurer扫描配置的mapper接口与xml文件,再加入sqlsession,产生一个个beanDefination,定义里放置的是实现了beanFactory的类,再由这个类的getObject来动态产生了一个实现接口的类。事实上,当beanDefination中如果放了一个beanFactory的类,就会被spring调用产生另外你getObject产生的类,而不是通常new出来的类。就正是可以做手脚的地方。dubbo是通过自定义的标签解析,来产生同样的beanDefination,里面放的也是实现了beanFactory的类。看来是异曲同工,最终目标就是产生这样的beanDefination。好吧,于是我选择mybatis的方式来写自己的miniDubbo了,放弃dubbo的标签定义与解析。 

    mybatis的那个配置类MapperScannerConfigurer是实现了postProcessBeanDefinitionRegistry接口,在这个过程中扫描那些DAO与XML并产生一个个beanDefination的。速途同归,不同的方式都是要产生beanDefination。这个方式方便,我也这么用了。 

    话说前面介绍的几个介入的接口没有它啊,查了一下: 
public interface BeanDefinitionRegistryPostProcessor 
extends BeanFactoryPostProcessor。好吧,它是BeanFactoryPostProcessor的子接口。介入的位置正好是在beanDefination产生后。 


二、客户端的设计 

    
    客户端的功能就是找到所有的接口,产生出一个个实现类,实现类功能不是执行方法调用,因为没有真正的功能实现类。只是通过代理的实现类,把获取到的接口的名子,方法的名字,参数类型,参数值等传给服务器,服务器当然找到服务器上实现相同接口的真正的实现类,调用后,得到值,再由服务器传递回来。客户端把传递过来的值返回调用者。 
    调用者是透明调用,不知道其后面是远程调用,与本地调用的感觉是一样一样的。都可以是autowired的接口。 

    要实现对任何接口的适用,动态代理是少不了的。除非你一个接口写一个静态代理。动态代理中最主要是一个invocation的产生,把在invoke方法中,把参数类型,值都发出去。 

设计以下类: 
1. Configurer类,它配置在spring-context.xml中,模仿mybatis,配置好接口。咱不学习dubbo的自定义标准产生beanDefination的方式了。它用于加载配置的接口,产生bean定义并放入springIOC中。它 implements BeanDefinitionRegistryPostProcessor。在postProcessBeanDefinitionRegistry方法中new 出一个个Beandefination,里面放置一个 2 

Java代码 

  1. <bean id="configurer" class="dubbura.spring.Configurer">  
  2.     <property name="beanName" value="msgSendI"></property>  
  3. </bean>  



2.ReferenceBean类,正是上面的beanDefination中放入的类。ReferenceBean<T>  implements FactoryBean接口。在getObject()方法getObject()中返回真正的代理类T。每一个接口产生一个bean定义,每个bean定义中放的就是这样一个类。 

  ReferenceBean中的动态代理类怎么生成呢?在ReferenceBean类的afterPropertiesSet()中可以生成啊。 
 

Java代码 

  1. public void afterPropertiesSet() throws Exception {  
  2.     // TODO Auto-generated method stub  
  3.     System.out.println("begin get the proxy object");  
  4.     InvocationHandler invocationHandler=new InvocationHandler(){  
  5.         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  6.             // TODO Auto-generated method stub  
  7.             System.out.println("method invoke:"+method.getName());  
  8.             System.out.println("method invoke:"+args[0].toString());  
  9.             Client client=ClientCenter.getAClient("192.168.117.35", 9166, "85f035103434343402655fff888", "h4343888",null);  
  10.             MiddleMsg msg = new MiddleMsg(method.getName(), args[0].toString());  
  11.             //下面是发同步消息  
  12.             MiddleMsg resultMsg=client.sendMsgSync(msg);  
  13.             System.out.println("【收到消息:】"+resultMsg.getBody());  
  14.             ResultObject result=new ResultObject();  
  15.             Map map=new HashMap();  
  16.             map.put("data",resultMsg.getBody());  
  17.             result.setAttachment(map);  
  18. /               return method.invoke(this, args);  
  19.             return result;  
  20.         }  
  21.     };  
  22.     invoker=invocationHandler;  
  23.     ref =(T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[] { interfaceClass }, invocationHandler);  
  24.     System.out.println("has create the Proxy Bean");  
  25. }  




3.还要有一个接口。客户端只有这个接口。 

4.主要的类就3个,其它还有一个controller,将会autowired这个接口用于测试。 




三、服务端的设计 

    服务端的功能,首先收到信息的是通讯服务端。我们知道基于netty的tcp通讯框架主要是处理channelHandler。客户端其实把调用的接口,方法,参数类型与值传过来,你也可以简单传过来一些String,关键是找到服务端的spring IOC中的实现类。实现真正调用。想到实现任何的接口方法,调用实现类,那肯定反射调用了。method.invoke()。 

    我用的这个通讯组件传的是方法名,参数是jason对象的字符串。服务端根据方法名,找到实现channelHandler(msgHandler)的一个注册进来的类,用这个类处理消息并返回的,返回的意思就是写回netty通道给客户端。是根据方法,并不是根据接口名,找到处理类的。接口名与方法名不是一个级别的。dubbo好象有一个请求结构体,封装了类、方法、参数类型与值,我不搞那么复杂了。 
    客户端发过来的消息是有方法名的,再找到处理类。我干脆不管什么方法,都用同一个handler吧,在handler中再找找map存放方法与对应处理类。收到任何消息,就用同一个处理类,它的方法会从消息体内含有的方法名查找这个方法要用的实现类。我如何建的这个map,会放置好方法名对应的接口实现类呢? 

    这个接口实现类是spring管理的常见的接口实现类。如何把自己放入另一个map中呢?也不是所有实现类都要放MAP中,有些不需要暴露出来给通讯层又的如何区分呢?所以自己实现注册进MAP并不好,还是学习dubbo方式吧(当然可以用自己的annotation,在beanDefinaton产生后介入处理),从spring配置中拿到信息,这样再从spring中拿到实现类,构建这样一个过渡类吧。想想在mybatis中,每个动态代理类必然要有一个数据库连接,连接是IOC中的对象,所以都要拿到容器中的连接对象。 
    要从方法反射调用类,干脆定义一个invocor对象作为过渡类吧,它持有接口实现类的对象,还有访求参数对象数组与参数值对象数组。这样,前面说的MAP中就是方法名与持有实现类的invoker类了。 

    总体的实现策略,与客户端一样,也是一个配置类开始的,再产生一个个beanDefination,里面放置一个serviceBean类持有配置参数,再它的afterporpetyset中,产生一个invoker类,并从容器中找到实现类给它,参数也给它,就完整了。再把它们注册到通讯层处理的静态map表中。 

   另外通讯层的tcp服务在哪启动呢?要么在配置类的afterpropetySet中,也可以在serviceBean的afterpropetySet方法中启动。反正启动一次,在整个spring容器启动中完成就行了,serviceBean比较多,可能会重复。就选择在配置类config中启动吧。 


1.config类,用于产生serviceBean的定义,并启动服务。它配置在spring-context.xml中,模仿mybatis,并设置接口与实现是谁。 
 

Java代码 

  1. <bean id="configurer" class="dubburaver.spring.Configurer">  
  2.         <property name="interfaceName" value="MsgSendI"></property>  
  3.         <property name="actionName" value="spush_notification"></property>  
  4.     </bean>  


 

Java代码 

  1.     public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {  
  2.         // TODO Auto-generated method stub  
  3.         System.out.println("begin......");  
  4.         BeanDefinition beanDefinitionref = registry.getBeanDefinition("msgSendImpl");  
  5.           
  6.         int a=registry.getBeanDefinitionCount();  
  7.         System.out.println(a);  
  8. //      BeanDefinition candidate=null;  
  9.         RootBeanDefinition beanDefinition = new RootBeanDefinition();  
  10.         beanDefinition.setBeanClass(ServiceBean.class);  
  11.         beanDefinition.setBeanClassName(ServiceBean.class.getName());  
  12.         beanDefinition.getPropertyValues().addPropertyValue("interfaceName", actionName);  
  13. //      definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));//mybatis的方式:运行时加载对象  
  14.         beanDefinition.getPropertyValues().addPropertyValue("ref", new BeanDefinitionHolder(beanDefinitionref, "msgSendI"));//dubbo的方式:定义中引用定义。  
  15.           
  16.         registry.registerBeanDefinition(ServiceBean.class.getName(), beanDefinition);  
  17.         int aa=registry.getBeanDefinitionCount();  
  18.         System.out.println(aa);  
  19.     }  



 

Java代码 

  1. public void afterPropertiesSet() throws Exception {  
  2.     initMiddlerWareStart(actionName,MsgHandler.class);//启动TCP监听  
  3. }  
  4.   
  5. public void initMiddlerWareStart(String msgName, Class clazz) {  
  6.     System.out.println("启动服务器,在9166端口监听tcp");  
  7.     try {  
  8.         MsgServiceHandlerRegister register = MsgServiceHandlerRegister.getRegister();  
  9.         // 注册事件处理类  
  10.         register.addMsgServiceHandler(msgName, clazz);  
  11.         new Thread(new Runnable() {  
  12.             @Override  
  13.             public void run() {  
  14.                 // TODO Auto-generated method stub  
  15.                 try {  
  16.                     java.util.List<ClientApp> clientAppList = new java.util.ArrayList<ClientApp>();  
  17.                     ClientApp eachClient = new ClientApp();// 通讯层用的客户对象,校验客户端                   eachClient.setAppKey("85f035102cb411e8b971002655fff888");                       eachClient.setAppSecret("homer888");  
  18.                     clientAppList.add(eachClient);  
  19.                     ServerInit.init(9166, clientAppList);  
  20.                 } catch (Exception e) {  
  21.                     // TODO Auto-generated catch block  
  22.                     e.printStackTrace();  
  23.                     // isMiddlewearStarted=false;  
  24.                 }  
  25.             }  
  26.         }).start();  
  27.         System.out.println("启动消息服务成功!端口:" + 9166);  
  28.     } catch (Exception e) {  
  29. /           isMiddlewearStarted = false;  
  30.         e.printStackTrace();  
  31.         System.out.println("启动消息服务失败!异常:" + e.toString());  
  32.     }  
  33.   
  34. }  




2.ServiceBean.java. 上面的代码中beanDefinition.setBeanClass(ServiceBean.class);这句是用的它。它的目标是产生invoker对象,并把真正实现类的定义置给它。最后它会把invoker对象注册到通讯处理类的map中,供选用。 

 

Java代码 

  1. public void afterPropertiesSet() throws Exception {  
  2.     System.out.println("【serviceBean】afterPropertiesSet...");  
  3.     System.out.println("【serviceBean】interfaceName..."+interfaceName);  
  4.     InvokerHolder invokerHolder=new InvokerHolder();  
  5.     invokerHolder.methodName=interfaceName;  
  6.     invokerHolder.parameterTypes=new Class[]{String.class};  
  7.     invokerHolder.proxy=ref;//上面的代码中有置这个值,正被的实现类。在spring的IOC中。  
  8.     System.out.println("【serviceBean】ref..."+(ref==null?"is null":ref));  
  9.     MsgHandler.invoderMap.put(interfaceName, invokerHolder);//放入MAP中。  
  10. }  




3. InvokerHolder类,它是被serviceBean生成的,serviceBean生了它,并传递给它实现类后,serviceBean也就没啥用了。这个InvokerHolder类就是根据参数值,进行反射调用。等通讯层找到它,用通讯层拿到的值设置好这个对象的值,就可以调用了。 

public class InvokerHolder { 
Object proxy; 
String methodName; 
Class<?>[] parameterTypes; 
Object[] arguments; 
    protected Object doInvoke(Object[] arguments) throws Exception 
    { 
    Method method = proxy.getClass().getMethod(methodName, parameterTypes); 
    return method.invoke(proxy, arguments); 
     } 


4.MsgHandler类,这个是低层通讯用的,从通讯中拿到需要的数据,找到invoker,并调用,再处理返回值写入nettyChannel。 
 

Java代码 

  1. public class MsgHandler implements MsgServiceHandler {  
  2.     public static Map<String, InvokerHolder> invoderMap = new java.util.HashMap<String, InvokerHolder>();  
  3.     @Override  
  4.     public MiddleMsg handleMsgEvent(MsgEvent dm, MiddleMsg msg) {  
  5.         // TODO Auto-generated method stub  
  6.   
  7.         System.out.println("invoderMap.size():"+invoderMap.size());  
  8.         Iterator<Entry<String, InvokerHolder>> it=invoderMap.entrySet().iterator();  
  9.           
  10.         while(it.hasNext()){  
  11.             Entry aa= it.next();  
  12.             System.out.println("map:"+aa.getKey()+"|"+aa.getValue());  
  13.         }  
  14.           
  15.         String action = msg.getHeader().getAction();  
  16.         String msgBody=msg.getBody().toString();  
  17.         try {  
  18.             System.out.println("action:"+action);  
  19.             System.out.println("msgBody:"+msgBody);  
  20.             //从请求参数中,找到invoker对象并设置好值。类型啥都舍弃了。  
  21.             InvokerHolder ref = invoderMap.get(action);  
  22.             Object[] arguments=new Object[]{msgBody};//参数值  
  23.             ResultObject resultObject = (ResultObject) ref.doInvoke(arguments);  
  24.             String body = "" + resultObject.getStatus() + resultObject.getStatusMsg();  
  25.             msg.setBody(body);  
  26.   
  27.         } catch (Exception e) {  
  28.             e.printStackTrace();  
  29.         }  
  30.         return msg;  
  31.     }  
  32. }  




5. 接口类与接口实现类,这个就不说明了。客户端接口与服务端接口应该是同一个包的。客户端没有实现,服务端有。 

   下图为服务端工程,类不是很多,上面都介绍过: 
 

   下图为客户端运行结果,客户端controller中autowired了接口,服务端invoker了调用。只有最后一句“我从服务端实现了这个接口”是接口方法的返回值。 
 


四、总结 

    上述功能已经在测试中很快实现了。以前看过些源码,但真正写出来,写之前还是思考不少东西。代码虽然不多,平时就要有空想想,看资料还是花了些时间的。碰巧平时处理项目中的mybatis问题中发现了另一种处理方式,所以快速就写出来。 

    之前看过些dubbo与druid的源码,但直到近来在项目中使用,才真正感觉到源码的价值了。不久前写一个业务数据内存中的处理,用到了druid中的连接池的线程管理方式。写另一业务依次处理中,用到了我前面一个文章写的过滤链模式。dubbo中的主要的代理与技术目前还没有在实际中用过,但对于类的设计与关系处理更透彻了,看其它代码也很快。有些人喜欢看java的源码,但我建议看这两个项目的源码,这是更系统化的解决问题的代码,而不仅是什么arrayList,Hashmap里面一些技巧的学习。有点象设计模式与具体代码技巧的区别。 

    如果你写基于netty的通讯架构,可以模仿dubbo协议。如果用到集群,用到SPI,用到策略,用到与zookeeper, redis连接,如果让系统集成多种实现需要写抽象层的代码,也可以学习它的思路。 

    现在dubbo与spring cloud是微服务的两个主流,不知道spring cloud中有啥可以借鉴的,过一阵有空用用它。 

猜你喜欢

转载自blog.csdn.net/herriman/article/details/81283549
今日推荐