今天我们来聊聊 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 是个不错的选择。
引用
[1] http://www.tcs.uj.edu.pl/~krawczyk/programowanie_w_sieci_internet/wyklad/wyklad5-rmi/rmi/slajd1.html
[2] http://www.ibm.com/developerworks/cn/websphere/library/bestpractices/enterprise_javabean.html
[3] http://publib.boulder.ibm.com/infocenter/radhelp/v7r5/topic/com.ibm.jee5.doc/topics/cejb3vejb21.html
[4] http://www.emeraldinsight.com/journals.htm?articleid=862014&show=abstract
作者简介
李湃,上海交通大学计算机硕士毕业,5 年互联网的行业经验,现就职于国内某互联网公司,喜欢开源技术,对于Java 企业架构、分布式技术、高性能高可靠软件设计有极大的热情,希望能对国内社区有所贡献。博客地址: http://haperkelu2011.iteye.com/
感谢崔康对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论