今天我们来聊聊Java远程服务的解决方案。Java分布式远程服务的解决方案,近几年在互联网应用越来越普及。我们简单分析下,形成这种格局的背景。
从无到有开发一个产品的时候,如果技术框架没有积累,那么代码的实现会比较随意,很多时候前端web层耦合了很多后端DAL层的代码。接下来,随着产品越来越多,每个产品的技术实现都会有很多重复代码。这就给后期的维护和升级带来了不便(比如针对某个服务做缓存优化或者日志处理,代价会非常高)。服务模块化呼之欲出!
服务模块化,就意味着代码的实现架构不再是Web层与DAL层的简单关系了。很多相似的业务会抽象为一个分布式服务,Java语言支持多种远程服务的实现,像EJB、 WebService、 RMI、Hessian等等。下面我们通过一个具体的例子来简述这些技术的使用以及在实践中如何权衡各种技术的适用场景。
用例:提供一个分布式的动物中心服务,提供猴子的名字。
1. RMI
RMI是Java提供的分布式应用API,远程方法调用RPC的实现。它的宗旨是,某个JVM下的对象可以调用其他JVM下的远程对象。RMI的底层实现是构建于TCP协议之上,将远程对象绑定具体的端口号,监听客户端的请求;客户端与远程对象的通信当中,依赖于预定义的接口,即RMI会生成一个本地Stub代理类,每次客户端调用远程对象的时候,Stub代理类会初始化参数、启动远程连接、将参数进行编组(marshal),通过网络传输送往服务器端,并对返回的结果进行反编组(unmarshal)。对于客户端调用方来讲,RMI隐藏了对象序列化和网络传输的实现细节。
图一:RMI的调用机制[1]
图一描述了RMI调用的大体步骤:首先RMI Server会通过请求RMIRegistry(远程对象联机注册服务)绑定一个远程对象,对象的元数据信息放在一个已有的Web Server上面;然后RMI Client会发送请求到RMIRegistry获取远程对象的地址,并远程调用该对象的方法。
下面我们使用RMI来实现之前所描述的用例。
- 接口类IAnimalService.java
import java.rmi.Remote; import java.rmi.RemoteException; public interface IAnimalService extends Remote { String getMonkeyName() throws RemoteException; }
- 实现类AnimalServiceImp.java
import java.rmi.RemoteException; import java.rmi.registry.Registry; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject; public class AnimalServiceImp implements IAnimalService { public AnimalServiceImp() { } @Override public String getMonkeyName() throws RemoteException { return "I'm Jacky"; } }
- 服务端AnimalServer.java
import java.rmi.RemoteException; import java.rmi.registry.Registry; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject; try { final int port = 8009; //绑定的端口号 final String host = "127.0.0.1"; //本机作为服务host final String serviceName = "animalService"; //服务名称 IAnimalService obj = new AnimalServiceImp(); IAnimalService stub = (IAnimalService) UnicastRemoteObject.exportObject(obj, port); //端口绑定远程对象 Registry registry = LocateRegistry.getRegistry(); registry.unbind(serviceName); registry.bind(serviceName, stub); //注册服务地址 System.out.println("Server Start..."); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); }
- 客户端Client.java
import java.rmi.RemoteException; import java.rmi.registry.Registry; import java.rmi.registry.LocateRegistry; Registry registry = null; final String host = "127.0.0.1"; final String serviceName = "animalService"; //服务名称 try { registry = LocateRegistry.getRegistry(host); //获取远程对象联机注册 //获取动态代理类 IAnimalService stub = (IAnimalService) registry.lookup(serviceName); //远程调用 String name = stub.getMonkeyName(); System.out.println("monkey name: " + name); } catch (Exception e) { e.printStackTrace(); }
- 部署RMI:编译上述代码、启动RMIRegistry、运行服务端的代码(AnimalServer.java)
- 客户端调用RMI:运行客户端代码(Client.java)
使用RMI的利弊:
- 优势:面向对象的远程服务模型;基于TCP协议上的服务,执行速度快。
- 劣势:不能跨语言;每个远程对象都要绑定端口,不易维护;不支持分布式事务JTA
早在Applet时期,Applet+RMI是Java业内广泛推崇的方式来实现分布式计算。笔者认为RMI框架对于安全性、事务、可扩展性的支持非常有限,进而限制了其发展。
2. EJB
EJB是之前Sun公司推出的基于面向对象的服务器端组件模型。它旨在成为一个可移植的、可扩展的、事务处理的、带有安全策略的分布式解决方案。
图二:EJB在J2EE解决方案中的角色[2]
EJB的核心有三个部分:会话Bean、实体Bean、消息Bean。EJB3.0对组件模型做了很多简化,降低了开发以及配置的复杂度。本节讨论的都已EJB3.0为准。
图三:EJB3.0架构图[3]
如图三描述的那样会话Bean主要负责将业务逻辑抽象出来,会话Bean分为有状态Bean和无状态Bean,有状态Bean记录客户端的信息,无状态Bean反之。实体Bean负责持久层ORMapping的工作,EJB3.0对实体Bean做了很大的调整,提供了持久化API(JPA),简化了开发和配置。消息Bean主要用来处理JMS中间件接受的客户端消息,即JMS队列的消费者,本质上它是一个异步的无状态会话Bean。
对于本文的用例来说,最适合使用无状态的会话Bean,下面我们来看下具体的实现。
- 接口类AnimalBeanLocal.java
import javax.ejb.Remote; @Remote public interface AnimalBeanLocal { String getMonkeyName(); }
- 无状态会话Bean AnimalBean.java
import javax.ejb.Stateless; /** * Session Bean implementation class AnimalBean */ @Stateless public class AnimalBean implements AnimalBeanLocal { /** * Default constructor. */ public AnimalBean() { } public String getMonkeyName() { return "I'm Jacky"; } }
- 客户端Client.java
import javax.naming.InitialContext; //经由JNDI命明和目录服务获取EJB Properties props = new Properties(); props.setProperty("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory"); props.setProperty("java.naming.provider.url", "localhost:1099"); props.setProperty("java.naming.factory.url.pkgs", "org.jboss.naming"); try { InitialContext ctx = new InitialContext(props); AnimalBeanLocal proxy; proxy = (AnimalBeanLocal) ctx.lookup("AnimalBean/remote"); System.out.println(helloworld.getMonkeyName()); } catch (Exception e) { e.printStackTrace(); }
- 部署EJB:启动JBOSS,并将EJB组件注册进JNDI服务
- 客户端调用EJB:运行客户端代码
使用EJB的利弊:
- 优势:可扩展性好,安全性强,支持分布式事务处理。
- 劣势:不能跨语言;配置相对复杂,不同J2EE容器之间很难做无缝迁移。
EJB是被诟病最多的分布式解决方案,主要原因是EJB配置复杂而且不同容器迁移起来困难。尽管EJB3.0做了很多的简化,配置起来还是相对笨重。对于学习曲线如此陡峭的技术来说,并不是企业放心采用的解决方案。
3. Web Service
Web Service是一组分布式应用模型的规范, 它定义了网络传输协议和访问服务的数据格式。该模型隐藏了技术的实现细节,旨在提供松散耦合、跨平台、跨语言的服务组件。
图四:Web Service架构图[4]
图四描述了SOAP协议实现的Web Service模型(本节讨论都以SOAP协议实现为准),首先客户端通过UDDI(发现整合平台)找到对应的Web Service,下载对应WSDL文件,生成本地代理类,继而请求Web Service服务。UDDI的概念一直被弱化,因为客户端一般都知道Web Service的地址。
接下来我们使用Web Service来实现本文的用例。本节使用的Web Service第三方库是CXF(http://cxf.apache.org/),规范使用的是JAX-WS。
- 接口类IAnimalService.java
import javax.jws.WebService; @WebService public interface IAnimalService { public String getMonkeyName(); }
- 实现类AnimalServiceImp.java
import javax.jws.WebService; @WebService(endpointInterface = "IAnimalService", serviceName = "AnimalService") public class AnimalServiceImp implements IAnimalService { @Override public String getMonkeyName() { return "I'm Jacky"; } }
- 服务端Server.java
import javax.xml.ws.Endpoint; IAnimalService serviceInstance = new AnimalServiceImp(); final String address = "http://localhost:9000/animalService"; //服务名称 Endpoint.publish(address, serviceInstance); //绑定并发布服务
- 客户端Client.java(无需手动下载WSDL文件,动态调用Web Service)
import org.apache.cxf.interceptor.LoggingInInterceptor; import org.apache.cxf.interceptor.LoggingOutInterceptor; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(); factory.getInInterceptors().add(new LoggingInInterceptor()); //日志输入拦截器 factory.getOutInterceptors().add(new LoggingOutInterceptor()); //日志输出拦截器 factory.setServiceClass(IAnimalService.class); factory.setAddress("http://localhost:9000/animalService"); IAnimalService client = (IAnimalService) factory.create(); System.out.println(client.getMonkeyName());
使用Web Service的利弊:
- 优势:跨语言、跨平台,SOA思想的实现;安全性高;可以用来兼容legacy系统的功能
- 劣势:性能相对差,不支持两阶段事务
Web Service使用的范围非常广,比如SalesForces(http:// www.salesforce.com),世界上最大的在线CRM提供商, 它的产品卖给使用不同技术平台的企业(.Net, Java, Ruby),SalesForces云计算的数据接口是以Web Service的方式发布的[8];Web Service另一个适用场景是,企业很多时候会有新老系统做数据交互,而新老系统使用的技术平台不一致,Web Service是个不错的选择。
4. Hessian
Hessian(http://hessian.caucho.com)是一种轻量级的Web Service, 采用的是二进制的RPC协议。
图五:Hessian架构图[5]
如图五所示,Hessian可以形容是一种基于二进制协议提供RMI功能的组件。
接下来我们使用Hessian来实现本文的用例。
- 接口类IAnimalService.java
public interface IAnimalService { public String getMonkeyName(); }
- 实现类AnimalServiceImp.java
public class AnimalServiceImp implements IAnimalService { @Override public String getMonkeyName() { return "I'm Jacky"; } }
- 服务端容器Tomcat配置Web.xml(不需要单独编写Servlet代码)
<servlet> <servlet-name>AnimalService</servlet-name> <servlet-class>com.caucho.hessian.server.HessianServlet</servlet-class> <init-param> <param-name>home-class</param-name> <param-value>com.demo.AnimalServiceImp</param-value> </init-param> <init-param> <param-name>home-api</param-name> <param-value>com.demo.IAnimalService</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>AnimalService</servlet-name> <url-pattern>/service/animalService</url-pattern> </servlet-mapping> </servlet>
- 客户端Client.java
final String url = "http://localhost:8080/service/animalService"; HessianProxyFactory factory = new HessianProxyFactory(); IAnimalService proxy = (IAnimalService) factory.create(IAnimalService.class, url); System.out.println(proxy.getMonkeyName());
使用Hessian的利弊:
- 优势:使用简单,速度快;跨语言,跨平台;可以用来兼容legacy系统的功能。
- 劣势:安全性的支持不够强,不支持两阶段事务。
通过上面的例子我们可以看出,Hessian使用起来非常简单简单,而且性能评测结果显示Hessian高于基于XML协议的RPC技术(http://daniel.gredler.net/2008/01/07/java-remoting-protocol-benchmarks/)。笔者认为在局域网内Hessian取代WebService是可行的,谁愿意花时间去研究相对笨重的Web Service框架,而且运行相率又很一般呢。大家可能想问,Hessian到底快在哪呢?有两点,首先Hessian采用的是二进制的RPC协议,其次Hessian的序列化速度也比Java本身序列化要快。因而选择Hessian作为解决方案的企业也越来越多。
5. NIO(Mina/Netty)
Java NIO可以理解为我们常说的非阻塞IO(异步IO),这个概念在高并发、多线程的环境里面尤为适用。NIO的基本原理是选择器来处理IO请求,将每个请求做标识,塞入处理队列;每个客户端请求进入睡眠,等待唤醒。
图六:异步IO工作原理[6]
图六展示了异步IO的工作原理,很显然异步IO在高并发的情况下可以节省系统很多资源(对比阻塞IO,异步IO不需要开启同等数量的服务线程)。
接下来我们使用异步IO来实现本文的用例,第三方库使用的是Netty。
- 接口类IAnimalService.java, Request.java
public interface IAnimalService extends Serializable { public String getMoneyName(); } public class Request implements Serializable { /** * 序列号 */ private static final long serialVersionUID = 3701941641993894303L;@SuppressWarnings("rawtypes") private Class service; //接口类 private String method; //调用方法名称 private Object[] paras; //调用方法参数 private String version; //服务版本 /** * @return the service */@SuppressWarnings("rawtypes") public Class getService() { return service; } /** * @param service the service to set */ public void setService(Class service) { this.service = service; } /** * @return the method */ public String getMethod() { return method; } /** * @param method the method to set */ public void setMethod(String method) { this.method = method; } /** * @return the paras */ public Object[] getParas() { return paras; } /** * @param paras the paras to set */ public void setParas(Object[] paras) { this.paras = paras; } /** * @return the version */ public String getVersion() { return version; } /** * @param version the version to set */ public void setVersion(String version) { this.version = version; } }
- 实现类AnimalServiceImp.java
public class AnimalServiceImp implements IAnimalService, Serializable { /** * 序列号 */ private static final long serialVersionUID = -160535222600556362L;@Override public String getMoneyName() { return "I'am Jackey"; } }
- 服务器端Server.java
final int port = 9990; ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); bootstrap.setPipelineFactory(new ChannelPipelineFactory() { public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeLine = Channels.pipeline(new SimpleChannelUpstreamHandler() { @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { //监听消息到达 Request obj = (request) e.getMessage(); if (obj.getService().equals(IAnimalService.class)) { Method targetMethod = obj.getService().getMethod(obj.getMethod(), new Class[0]); Object result = targetMethod.invoke(new AnimalServiceImp(), obj.getParas()); e.getChannel().write(result); } } }); pipeLine.addFirst("encoder", new ObjectEncoder()); //对象编码器 pipeLine.addFirst("decoder", new ObjectDecoder()); //对象解码器 return pipeLine; } }); bootstrap.bind(new InetSocketAddress(port)); //启动服务并绑定端口
- 客户端代码Client.java
ClientBootstrap client = new ClientBootstrap(new NioClientSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); client.setPipelineFactory(new ChannelPipelineFactory() { public ChannelPipeline getPipeline() throws Exception { ChannelPipeline pipeLine = Channels.pipeline(new SimpleChannelUpstreamHandler() { @Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) { //创建连接发送请求 Request r = new Request(); r.setVersion("1.0.0"); //设置版本 r.setService(IAnimalService.class); //设置服务类型 r.setMethod("getMoneyName"); //调用服务方法名称 r.setParas(null); //参数 e.getChannel().write(r); } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception{ //监听消息到达 System.out.println(e.getMessage().toString()); } }); pipeLine.addFirst("encoder", new ObjectEncoder()); //对象编码器 pipeLine.addFirst("decoder", new ObjectDecoder()); //对象解码器 return pipeLine; } }); client.setOption("tcpNoDelay", true); client.setOption("keepAlive", true); ChannelFuture future = client.connect(new InetSocketAddress("127.0.0.1", 9990)); future.getChannel().getCloseFuture().awaitUninterruptibly(); client.releaseExternalResources(); //释放外部资源
上述代码的实现稍有复杂,主要的结构是客户端将请求对象编码并发送管道,服务端将接受的字节流解码为对象,调用相应的方法并将结果返还至客户端。感兴趣的读者可以查看Netty官网(http://www.jboss.org/netty)来了解详情。
中国最大的互联网公司之一,淘宝,内部使用的服务框架HSF就采用了这种方式(采用的第三方NIO库是Mina)[7] 。笔者认为使用NIO这种方式来做分布式应用的优劣也是非常明显的:
- 优点:基于TCP通信,效率上高于HTTP的方式,非阻塞IO应对高并发绰绰有余。根据具体的需要制定数据传输的格式,可扩展性强。
- 缺点:不能跨语言,无法穿透防火墙。
结论
对企业来讲,Java Remoting采取何种方案没有一个特定的标准。根据笔者的经验,业务特点以及数据吞吐量决定了技术的选择方向。比如第三方数据接口,重点考虑的是跨平台、跨语言、支持高并发、保证安全;而局域网内的分布式服务,重点考虑的是高性能、稳定性、可伸缩性。
作者
李湃,上海交通大学计算机硕士毕业,5年互联网的行业经验,现就职于国内某互联网公司,喜欢开源技术,对于Java企业架构、分布式技术、高性能高可靠软件设计有极大的热情,希望能对国内社区有所贡献。博客地址:http://haperkelu2011.iteye.com/