`
herman_liu76
  • 浏览: 96788 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

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

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

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

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



   本文先介绍一下碰到的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)重名的话就会触发这个校验机制,抛异常。

protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
if (!this.registry.containsBeanDefinition(beanName)) {
return true;
}
BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
if (originatingDef != null) {
existingDef = originatingDef;
}
if (isCompatible(beanDefinition, existingDef)) {
return false;
}
throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
}


   网上查到的解决方案都是取别名。但是普通的类可以写别名,可不知道如何介入到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的。我也这么用了。

    话说前面介绍的几个介入的接口没有它啊,查了一下:
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

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


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

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

	public void afterPropertiesSet() throws Exception {
		// TODO Auto-generated method stub
		System.out.println("begin get the proxy object");
		InvocationHandler invocationHandler=new InvocationHandler(){
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				// TODO Auto-generated method stub
				System.out.println("method invoke:"+method.getName());
				System.out.println("method invoke:"+args[0].toString());
				Client client=ClientCenter.getAClient("192.168.117.35", 9166, "85f035103434343402655fff888", "h4343888",null);
				MiddleMsg msg = new MiddleMsg(method.getName(), args[0].toString());
				//下面是发同步消息
				MiddleMsg resultMsg=client.sendMsgSync(msg);
				System.out.println("【收到消息:】"+resultMsg.getBody());
				ResultObject result=new ResultObject();
				Map map=new HashMap();
				map.put("data",resultMsg.getBody());
				result.setAttachment(map);
//				return method.invoke(this, args);
				return result;
			}
		};
		invoker=invocationHandler;
		ref =(T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[] { interfaceClass }, invocationHandler);
		System.out.println("has create the Proxy Bean");
	}



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,并设置接口与实现是谁。

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


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



	public void afterPropertiesSet() throws Exception {
		initMiddlerWareStart(actionName,MsgHandler.class);//启动TCP监听
	}

	public void initMiddlerWareStart(String msgName, Class clazz) {
		System.out.println("启动服务器,在9166端口监听tcp");
		try {
			MsgServiceHandlerRegister register = MsgServiceHandlerRegister.getRegister();
			// 注册事件处理类
			register.addMsgServiceHandler(msgName, clazz);
			new Thread(new Runnable() {
				@Override
				public void run() {
					// TODO Auto-generated method stub
					try {
						java.util.List<ClientApp> clientAppList = new java.util.ArrayList<ClientApp>();
						ClientApp eachClient = new ClientApp();// 通讯层用的客户对象,校验客户端					eachClient.setAppKey("85f035102cb411e8b971002655fff888");						eachClient.setAppSecret("homer888");
						clientAppList.add(eachClient);
						ServerInit.init(9166, clientAppList);
					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
						// isMiddlewearStarted=false;
					}
				}
			}).start();
			System.out.println("启动消息服务成功!端口:" + 9166);
		} catch (Exception e) {
//			isMiddlewearStarted = false;
			e.printStackTrace();
			System.out.println("启动消息服务失败!异常:" + e.toString());
		}

	}



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


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



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。

public class MsgHandler implements MsgServiceHandler {
	public static Map<String, InvokerHolder> invoderMap = new java.util.HashMap<String, InvokerHolder>();
	@Override
	public MiddleMsg handleMsgEvent(MsgEvent dm, MiddleMsg msg) {
		// TODO Auto-generated method stub

		System.out.println("invoderMap.size():"+invoderMap.size());
		Iterator<Entry<String, InvokerHolder>> it=invoderMap.entrySet().iterator();
		
		while(it.hasNext()){
			Entry aa= it.next();
			System.out.println("map:"+aa.getKey()+"|"+aa.getValue());
		}
		
		String action = msg.getHeader().getAction();
		String msgBody=msg.getBody().toString();
		try {
			System.out.println("action:"+action);
			System.out.println("msgBody:"+msgBody);
			//从请求参数中,找到invoker对象并设置好值。类型啥都舍弃了。
			InvokerHolder ref = invoderMap.get(action);
			Object[] arguments=new Object[]{msgBody};//参数值
			ResultObject resultObject = (ResultObject) ref.doInvoke(arguments);
			String body = "" + resultObject.getStatus() + resultObject.getStatusMsg();
			msg.setBody(body);

		} catch (Exception e) {
			e.printStackTrace();
		}
		return msg;
	}
}



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

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


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



四、总结

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

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

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

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



  • 大小: 23.3 KB
  • 大小: 55.1 KB
  • 大小: 99 KB
  • 大小: 843.2 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics