远程调用方式就是尽可能地使系统间的通信和系统内一样,让使用者感觉调用远程同调用本地一样,但其实并没有办法做到完全透明,例如由于远程调用带来的网络问题、超时问题、序列化/反序列化问题、调试复杂的问题等,在远程调用时要注意对这些问题的处理。
1. 基于Java自身技术实现远程调用
在Java中实现远程调用方式的技术主要有RMI和WebService两种,下面分别来看看基于这两种技术如何实现远程调用方式的系统间通信。
RMI
RMI(Remote Method Invocation)是Java用于实现透明远程调用的重要机制。在远程调用中,客户端仅有服务器端提供的接口。通过此接口实现对远程服务器端的调用。
远程调用基于网络通信来实现,RMI同样如此,其机制如图所示:
Sun JDK 6.0以前版本中的RMI实现均是基于TCP/IP+BIO方式的,RMI服务器端通过启动RMI注册对象在一个端口上监听对外提供的接口,其实现实例以字符串的方式绑定到RMI注册对象上。RMI客户端通过proxy的方式代理了对服务器端接口的访问,RMI客户端将要访问的服务器端对象字符串、方法和参数封装成一个对象,序列化成流后通过TCP/IP+BIO传输到RMI服务器端。RMI服务器端接收到客户端的请求对象后,解析其中的对象字符串、方法及参数,通过对象字符串从RMI注册对象上找到提供业务功能的实例,之后结合要访问的方法来反射获取到方法实例对象,传入参数完成对服务器端对象实例的调用,返回的结果则序列化为流以TCP/IP+BIO方式返回给客户端,客户端在接收到此流后反序列化为对象,并返回给调用者。
RMI要求服务器端的接口继承Remote接口,接口上的每种方法必须抛出RemoteException,服务器端业务类通过
实现此接口提供业务功能,然后通过调用UnicastRemoteObject.exportObject来将此对象绑定到某端口上,最
后将此对象注册到本地的LocateRegistry上,此时形成一个字符串对应于对象实例的映射关系。基于RMI实现示
例中的服务器端代码如下。
//服务端对外提供接口 public interface Business extends Remote{ public String echo(String message) throws RemoteException; } 服务器端实现此接口的类 public class BusinessImpl implements Business{ public String echo(String message) throws RemoteException { if("quit".equalsIgnoreCase(message)){ System.err.println("Server will be shut down"); System.exit(0); } System.err.println("Message from client: " + message); return "Server response " + message; } } 基于RMI的服务器端 public class Server { public static void main(String[] args) throws RemoteException { int port = 9527; String name = "BusinessDemo"; System.err.println("Server start up..."); Business business = new BusinessImpl(); UnicastRemoteObject.exportObject(business, port); Registry registry = LocateRegistry.createRegistry(1111); registry.rebind(name, business); } }
RMI的客户端首先通过LocateRegistry.getRegistry来获取Registry对象,然后通过Registry.lookup
字符串获取要调用的服务器端接口的实例对象,最后以接口的方式透明地调用远程对象的方法。按照
以上描述,基于RMI实现客户端的关键代码如下:
public class Client { public static void main(String[] args) throws RemoteException, NotBoundException { System.err.println("Client start up..."); String name = "BusinessDemo"; Registry registry = LocateRegistry.getRegistry("localhost", 1111); Business business = (Business) registry.lookup(name); business.echo("no zuo no die"); //business.echo("quit"); } }
从上面示例代码来看,基于RMI实现的客户端和服务端较之基于TCP/IP+NIO等实现的客户端和服务端简单很多,代码可维护性也高很多。
WebService
WebService是一种跨语言的系统间交互标准。在这个标准中,对外提供功能的一方以HTTP的方式提供服务,该服务采用WSDL(Web Service Description Language)描述,在这个文件中描述服务所使用的协议、所期望的参数、返回的参数格式等。调用端和服务端通过SOAP(Simple Object Access Protocol)方式来进行交互。
在Java中使用WebService须首先将服务器端的服务根据描述生成相应的WSDL文件,并将应用及此WSDL文件放入HTTP服务器中,借助Java辅助工具根据WSDL文件生成客户端stub代码。此代码的作用是将产生的对象请求信息封装为标准的SOAP格式数据,并发送请求到服务器端,服务器端在接收到SOAP格式数据时进行转化,反射调用相应的Java类,过程如图所示:
Java SE6中集成了WebService,因此可以直接实现该方式的远程调用,服务器端通过@WebService来标记对外暴露的WebService实现类,通过调用Endpoint.publish将此WebService实现发布到指定的HTTP地址上。客户端通过wsimport来访问相应地址的wsdl文件,从而生成调用服务器端的辅助类,应用即可通过调用此类来实现远程调用了。基于WebService实现示例中的服务器端代码如下:
对外暴漏的接口:
public interface Business { public String echo(String message); }
服务器端的实现类,通过@WebService来指定对外提供的WebService的名称和客户端生成的类名及包名:
@WebService(name="Business",serviceName="BusinessService", targetNamespace="http://WebService.chapter1.book/client") @SOAPBinding(style = SOAPBinding.Style.RPC) public class BusinessImpl implements Business{ public String echo(String message) { if("quit".equalsIgnoreCase(message)){ System.err.println("Server will be shut down..."); System.exit(0); } System.err.println("Message from client: " + message); return "Server response: " + message; } }
发布WebService的类:
public class Server { public static void main(String[] args) { Endpoint.publish("http://localhost:9527/BusinessService", new BusinessImpl()); System.err.println("Server has been started"); } }
客户端通过JDK bin目录下的wsimport命令来生成辅助调用代码,执行如下命令生成辅助代码:
wsimport -keep http://localhost:9527/BusinessService?wsdl
执行后,在当前目录下会生成book/chapter1/WebService/client/Business.java和book/chapter1/ WebService/client/BusinessService.java的代码,基于这两个生成的代码编写客户端的关键代码如下:
public class Client { public static void main(String[] args) { BusinessService businessService = new BusinessService(); Business business = businessService.getBusinessPort(); business.echo("no zuo no die"); System.err.println("Client's message has been sended"); } }
WebService传输的数据协议采用SOAP,SOAP对于复杂的对象结构比较难支持,其好处是能够支持跨语言。
无论是采用RMI还是WebService,都封装了网络通信的细节,因此使用起来会较为简单,但如果想对通信细节做一些调优或控制,也会比较麻烦。
2. 基于开源框架实现远程调用
SpringRMI
Spring RMI是Spring Remoting中的一个子框架,基于Spring RMI可以很简单地就实现RMI方式的Java远程调用, Spring RMI的工作原理如图所示:
基于Spring RMI实现示例中的服务器端代码如下:
对外提供的接口及服务器端的实现类同上(无需任何注解标注的那一版)。
Spring描述文件:
<bean id="businessService" class="com.lanxum.springrmi.BusinessImpl" /> <bean id="rmiBusinessService" class="org.springframework.remoting.rmi.RmiServiceExporter"> <property name="service" ref="businessService" /> <property name="serviceName" value="BusinessService" /> <property name="serviceInterface" value="com.lanxum.springrmi.Business" /> </bean>
启动Spring容器,并让外部能够以RMI的方式访问BusinessImpl:
public static void main(String[] args) { new ClassPathXmlApplicationContext("com/lanxum/springrmi/server.xml"); System.err.println("Server has been started..."); }
客户端代码如下:
Spring描述文件:
<!-- spring Rmi 客户端是通过 RmiProxyFactoryBean 和它的父类来完成 查找远程对象 生成代理对象 方法调用 http://blog.csdn.net/java2000_wl/article/details/7408249 --> <bean id="businessService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://localhost/BusinessService" /> <property name="serviceInterface" value="com.lanxum.springrmi.Business"></property> </bean>
客户端通过启动Spring容器,获取相应的Spring bean后即可以RMI方式调用远程对象了:
ApplicationContext ctx = new ClassPathXmlApplicationContext("com/lanxum/springrmi/client.xml"); Business business = (Business) ctx.getBean("businessService"); // error create the bean Business business.echo("no zuo no die..."); System.err.println("Client message has been sended...");
CXF
CXF是Apache的顶级项目,也是目前Java社区中用来实现WebService流行的一个开源框架(尤其是收编了xfire后)。基于CXF可以非常简单地以WebService的方式来实现Java甚至是跨语言的远程调用,CXF的工作原理如图所示:
CXF对于WebService的服务器端并没做多少封装,它仍然采用目前Java SE本身的WebService方式,只是提供了一个JaxWsServerFactoryBean类,从而可以在WebService被调用时增加一些拦截器的处理。客户端方面CXF则增加了封装,以便能够直接以接口的方式来调用远程的WebService,简化了调用WebService的复杂性,CXF提供的类为JaxWsProxy-FactoryBean,通过此类将WebService的接口类以及WebService的地址放入,即可获取对应接口的代理类了,基于CXF实现示例中的服务器端代码如下:
接口类:
@WebService public interface Business { public String echo(String message); }
业务实现类:
@WebService(serviceName="BusinessService", endpointInterface="com.lanxum.ws.cxf.Business") public class BusinessImpl implements Business{ public String echo(String message) { if("quit".equalsIgnoreCase(message)){ System.err.println("Server will be shutdown!"); System.exit(0); } System.err.println("Message from client: " + message); return "Server response: " + message; } }
调用CXF类完成WebService的发布:
public static void main(String[] args) { Business service = new BusinessImpl(); JaxWsServerFactoryBean svrFactory = new JaxWsServerFactoryBean(); svrFactory.setServiceClass(Business.class); svrFactory.setAddress("http://localhost:9527/business"); svrFactory.setServiceBean(service); svrFactory.create(); }
客户端由于CXF提供了良好的封装,调用WebService就比直接用Java的方式简单多了,关键代码如下:
JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(); factory.setServiceClass(Business.class); factory.setAddress("http://localhost:9527/business"); Business business = (Business) factory.create(); business.echo("no zuo no die...");
类似CXF这样的WebService框架在开源界中还有不少,例如Axis。这些开源WebService框架除了对Java WebService
的使用方式进行封装,提升了易用性外,还增加了多种协议来调用WebService,例如TCP/IP和JMS等。
基于Spring httpInvoker及Hessian的远程调用:
http://blog.csdn.net/cenwenchu79/article/details/1879688
http://www.cnblogs.com/7mile/archive/2013/04/24/3038535.html
Apache Thrift框架
SCA
SCA(Service Component Architecture)是SOA的一具体实现规范,服务遵照SOA以接口方式对外提供,发布服务首先要求系统本身已经有相应的接口实现。为了减少对系统实现的侵入,通过XML定义Component映射到系统本身的接口实现上。SCA支持了多种映射方式,并允许自行扩展,因此系统可采用Java、Spring Bean等多种方式来实现接口。在定义了Component后,即可将Component实现的接口以服务的方式发布。基于Component的复用,同时吸收了IOC的思想,Component之间的组装是通过SCA框架来实现的。基于业务组件的模块化开发的设计思想,SCA的Component和OSGI的Bundle一样其实是对Java封装的一种贯彻,它有需要Import的服务引用,需要Export的服务,很重要一点就是它对于Component,service,reference的实现都没有作限制,这类对象只需要有个接口定义和实现描述即可,当前规范中支持的接口定义可以是java接口也可以是wsdl文件(最终也是转换成为java接口),实现的话那么更加广泛java,webservice,rmi,jms,脚本语言等等,同时提供了扩展接口,所以只要你想的到,就可以实现的了。然后多个Component组装成为一个Composite(对于业务开发来说,也就是一个业务的Model)。
SCA框架有Caucho的Tuscany,Top的服务框架HSF就有运用SCA规范所实现。
在大型的应用中,分布式的概念不仅仅是上面的跨JVM或跨机器的系统间通信,还会涵盖如集群(故障转移、消息容错、leader选举,集群管理等, Zookeeper)、负载均衡、分布式缓存、分布式文件系统等多方面的内容。