EJB方法调用框架

  作者:板桥banq

<<Java实用系统开发指南>>

本章将介绍一种EJB方法调用框架,它适合不同种类的远程客户端。这些客户端不仅指通常的瘦客户端如浏览器,也包括一些所谓的“肥客户端”,即使用Java的Application、Applet和J2ME等技术编制而成的客户端。
这些客户端通过本框架和EJB服务(EJB Services)实现交互,从而可以快速有效地开发出一个强大的、可分布的、可伸缩的企业应用或专用系统。

7.1  框架概况

J2EE多层结构根据客户端形式的不同,分为两种主要形式,客户端基于浏览器的B/S多层结构和基于肥客户端的C/S多层结构。后者在一些专业专用领域有一定的应用市场,特别是Java无线技术的发展,J2ME作为一个拥有功能强大的手机终端系统正显现其特有的魅力。
使用肥客户端技术可以减轻J2EE服务器系统的操作开销,降低客户端和服务器端的通信次数,提供交互层次,改善数据的确认速度,缺点是需要用户专门下载安装。

7.1.1  远程调用技术背景

C/S多层结构首先需要解决的问题是:肥客户端和服务器之间的远程通信和调用。远程调用的概念由来已久,Java RMI (Remote Method Invocation 远程方法调用)是远程方法调用的简称,它大大增强了Java开发分布式应用的能力。
所谓远程方法调用,就是在一台机器上调用另外一台机器上对象的方法。正如前面章节所讨论的,客户端调用EJB一般是通过RMI方式进行的。这样,EJB能以分布式运行在多台服务器之间,对于客户端的调用是透明的,几乎没有什么区别。
RMI所提供的远程方法调用是有一定限制的,即“远程”并不意味着非常远,一般是指在同一个防火墙内的区域。因为实际应用中,网络防火墙因为安全原因,对大多数协议或端口都进行了封锁,在这种情况下,防火墙以外的客户端如果希望通过RMI调用EJB服务,那几乎是不可能的事情,因此,需要寻找一个能穿透大多数防火墙的协议,将远程客户端调用导入防火墙内部,再通过RMI调用EJB服务。
目前,HTTP协议大概是惟一的防火墙友好的协议。其实,Web Service的SOAP也是一种基于HTTP协议的协议。
这样,针对B/S多层结构,因为一般Web服务器和EJB服务器是位于同一个防火墙内,属于一个局域网;因此,Web服务器作为客户端可以通过RMI实现EJB服务的远程方法调用;而针对C/S多层结构,将客户端的调用通过HTTP协议序列化后导入防火墙的Web服务器内,再通过Web服务器实现EJB服务的RMI调用。

7.1.2  框架结构

在图7-1中,肥客户端主要负责处理视图显示、事件控制响应、发出HTTP请求。Servlet Proxy专门用于接受HTTP请求,通过Business Proxy调用EJB Service。
如果客户端是浏览器这样的瘦客户端,那么通过RMI就可以直接调用EJB了。不过,在本框架系统中,调用EJB是通过方法反射机制来实现的,而不是通常的EJB调用方式,其优点将在以后章节中分析。
图7-1的虚线包括的部分是EJB方法调用框架。可以发现,通过使用该框架系统,实现了EJB和客户端之间的解耦,两者各自变成一个相当独立的子系统,利于开发和维护。
对于Web开发人员,无需掌握太多EJB相关知识,只要获知EJB Service提供的功能方法,就可以相对独立地开发Servlet/JSP;也无需在Web层进行EJB出错捕获等琐碎工作;可以在没有EJB容器或数据服务器情况下测试Web层。
对于EJB开发人员,也无需了解Strut等Web技术,EJB改变或部署变化时,不必变动Web层代码。
由于该框架系统对EJB Service实现了缓存机制,大大提高了系统响应性能,如果再结合具体数据缓存机制,整个系统的并发处理性能将提高很多。
在前面章节中也讨论了一种接口框架系统的设计,本框架是在其基础上再进行深入和抽象。同时兼顾到远程肥客户端的调用情况,更加提高了系统的可重用性以及可伸缩性,加快了系统开发流程。


7.2  框架设计

一般在Web层调用EJB方法如下:
EJBFacadeLocal eJBFacadeLocal = eJBFacadeHomeLocal.create();
eJBFacadeLocal.eJBmethod();
上述代码中,通过JNDI获得的EJB的Home接口,使用create方法创建一个EJBLocalObject或EJBObject,客户端再通过EJBLocalObject调用EJB的具体方法,如createUser()、insert()、update()等方法。
同样,对于在物理位置上远离服务器的肥客户端,我们也希望以类似的方式实现。但是,EJBLocalObject或EJBObject并不能通过网络传送到远程肥客户端。那么,只能将远程客户端的EJB调用在服务器端进行实现,然后将处理结果送回远程肥客户端。
远程客户端调用EJB代码如下:
EJBFacadeLocal eJBFacadeLocal = (EJBFacadeLocal)serviceFactory.getService();
eJBFacadeLocal.eJBmethod(Object param);
远程客户端从serviceFactory获得一个EJB Service,然后调用该Service的方法eJBmethod()。如果将方法eJBmethod和方法参数Object param通过HTTP协议传递到服务器端,由服务器端EJB相应的方法处理后,再将结果送回,这样就完成了远程客户端对EJB的虚拟调用。
上述过程非常类似Web Service的JAX-RPC实现,了解本框架的RPC实现过程,也将有助于理解JAX-RPC实现原理。

7.2.1  代理(Proxy)模式

在GOF设计模式中对代理模式的定义是:为其他对象提供一种代理,以控制对这个对象的访问。代理模式可以强迫客户端对一个对象的方法调用间接通过代理类进行。
通常代理模式有以下几种:访问代理(Access Proxy)、虚拟代理和远程代理等。
访问代理是对访问服务或数据对象实现安全权限控制,如Jive论坛中的代理模式就属于这种。
虚拟代理可实现对象不同方式的初始化,具体有两种方式:即时的和懒惰的,懒惰初始化是相对即时初始化而言,它不是在这个对象生成时立即进行初始化工作,而是只在被访问使用时的才进行初始化。
远程代理是用于屏蔽或保护客户端离开远距离的原始对象,当客户端访问一个对象需要经过漫长的网络调用或大数据读写时,可以通过在本地设置中间代理来代替远程的原始对象。


以Java代码为例,假设有一个接口为BaseInterface,定义行为myMethod,如下:
public interface BaseIF{
  public Object myMethod();
}
有一个原始类OriginClass是BaseIF的具体实现:
public class OriginClass implements BaseIF{
  public Object myMethod (){
       return " hello , It is me! ";
  }
}
在正常情况下,客户端调用原始类OriginClass如下:
BaseIF instance =  new OriginClass();
String result = (String )instance. myMethod ();       //实现方法操作
System.out.println(result);                       //将会输出 hello , It is me!
但是,如果这个原始类OriginClass处于远程,需要通过Internet或很长的网络调用才能访问到,那么有必要使用一个代理来代理这个原始类OriginClass,代理类如下:
public class ProxyClass implements BaseIF{
public Object myMethod (){
      //通过网络协议调用远程的OriginClass
     BaseIF instance =  getRemoteOrigin ();
     Return instance.myMethod ();
  }
  …
}
ProxyClass类也是接口BaseIF的实现,在myMethod方法中,是通过getRemoteOrigin()获得OriginClass实例。在getRemoteOrigin ()中封装的是远程网络调用细节代码。
客户端调用基本没有变化:
BaseIF instance =  new Proxy Class();
String = (String)instance.myMethod ();
System.out.println(result);               //将会输出 hello , It is me!
客户端调用ProxyClass的方法myMethod,实际就是调用了远程OriginClass的myMethod方法,Proxy类只是一个中间代理类,类似“二传手”。
如果不使用代理类ProxyClass,那么就要在客户端代码中直接实现对远程OriginClass的调用,这样会导致客户端代码在修改或拓展时非常不方便。很显然,使用代理ProxyClass可以封装这些网络调用细节,使得客户端代码变得非常简洁。
使用代理模式可以解决实际编程中很多问题,但是它的缺点是ProxyClass和OriginClass还是比较紧密地耦合在一起,使用动态代理就可以克服这个缺点。

7.2.2  动态代理

动态代理(Dynamic Proxy)是JDK 1.3以后推出的新API,它是代理模式的一种实现。动态代理利用Java的反射(Reflect)机制,可以在运行时刻将一个对象实例的方法调用分派到另外一个对象实例的调用,注意是运行时刻而不是在源代码编译时,动态代理模式可以在运行时刻创建继承某个接口的类型安全的代理对象,而无需在代码编译时编译这些代理类代码。
也就是说,如果使用了动态代理,前面讨论的代理模式示例中的ProxyClass类就不需要了。但是,ProxyClass类方法myMethod中的网络调用细节代码是如何在动态代理中实现的呢?
在动态代理中,对原始类的方法(如myMethod)调用将变成对一个叫invocation handler 类的方法调用,原始类的方法被编码进入java.lang.reflect的Method对象,方法参数则被包含进入一组对象数组。也就是说,ProxyClass类中的网络调用细节代码必须放置在invocation handler 类的方法中实现。
动态代理API由两大部分组成,首先需要一个接口定义:
public interface BaseIF{
  public void myMethod ();
}
第二步是实现invocation handler,如下:
public Class DynamicProxyClass implements java.lang.reflect.InvocationHandler
{
  Object obj;

  public DynamicProxyClass(Object obj)
  { this.obj = obj; }

  public Object invoke(Object proxy, Method m, Object[] args) throws Throwable
  {
     if (m.getName().equals(“myMethod”)){
      //通过网络协议调用远程的OriginClass
      BaseIF instance =  getRemoteOrigin ();
     return instance.myMethod ();
    }
   }
   …
 }
DynamicProxyClass中主要部分是invoke方法,在这个方法中,通过getRemoteOrigin获得远程OriginClass实例。
客户端调用代码如下:
DynamicProxyClass dynamicProxyClass = new DynamicProxyClass(obj)
BaseIF instance = (BaseIF)
java.lang.reflect.Proxy.newProxyInstance(dynamicProxyClass.getClass().getClassLoader(),
                                  Class[] { BaseIF.class },
                                  dynamicProxyClass);
String = (String)instance.myMethod ();
System.out.println(result);               //将会输出 hello , It is me!
上述代码显得繁琐一点,第一行是产生DynamicProxyClass实例,然后通过java.lang.reflect.Proxy的newProxyInstance产生代理实例,这个代理实例是在运行时动态产生的。
当通过instance.myMethod ()调用时,系统将直接执行DynamicProxyClass的invoke方法,invoke方法有两个重要输入参数Method m和Object[] args,系统自动将方法myMethod编码进入参数m,如果myMethod有参数,如myMethod(String param),那么方法参数param将被包含进入args中。
动态代理有一些HOOK(钩子),如果能理解了这些钩子的来龙去脉,那么就比较容易使用动态代理API了。
使用动态代理API要注意以下几点。
代理接口(如BaseIF.class)必须是一个接口,不能是普通类或抽象类。
Proxy.newProxyInstance的输入参数中,代理接口数组中不能包含相同的代理接口,即不能出现Class[] { BaseIF.class, BaseIF.class }。
所有的代理接口必须对于Proxy.newProxyInstance输入参数ClassLoader是可见的,即ClassLoader必须能够调用到代理接口。如dynamicProxyClass.getClass().getClass Loader()必须能够调用到BaseIF.class。
代理接口不能有冲突的方法名,如,不能有这样的两个方法:有同样的输入参数,但是返回的是不同类型。
在本框架设计中,将通过动态代理API实现远程客户端访问EJB。按照动态代理的使用规则,首先需要定义一个接口,而EJB的remote接口或local接口正好符合这个要求,下一步是如何从远程客户端发出请求数据以及获得EJB的处理结果。

7.2.3  反射(Reflection)和方法调用

上节的Proxy.newProxyInstance属于Java反射(Reflection)的一种实现,除此之外,反射还有字段Field、方法Method等实现,Java反射机制是一种提供了类安全的,支持类和对象自省机制的API。通过反射机制,可以做到以下几点:
(1)创建新的类的实例和新的数组。如下列代码并非使用new来创建一个新的对象:
String className = "com.jdon.bussinessproxy.web.ServiceFactoryImp";
Class c = Class.forName(className);
ServiceServerFactory factory = (ServiceServerFactory) c.newInstance();
上例中,通过Class.forName获得一个Class实例,使用Class.newInstance直接生成了对象实例,类似直接使用new获得对象实例:
ServiceFactory serviceFactoryImp = new ServiceFactoryImp();
(2)访问并修改对象和类的字段。例如,能直接获得 Java 类中各成员的名称并显示出来:
import junit.framework.*;
import java.lang.reflect.*;
public class TestReflectField extends TestCase {
  public int oneField = 1;
  public void testMthod() throws Throwable {
     Class c = this.getClass();
     Field fld = c.getField("oneField");
     System.out.println("oneField = " + fld.getInt(this));  //结果oneField = 1
     fld.setInt(this, 99); //改变oneField字段值为99
     System.out.println("oneField = " + fld.getInt(this));  //结果oneField = 99
  }
}
在例子中,通过Class实例getField方法来获得或者修改运行中对象的字段值。
(3)直接调用对象或类的方法。如下列代码:
import junit.framework.*;
import java.lang.reflect.*;

public class TestReflectField extends TestCase {
  public int update(int x, int y) {
    return x + y;
  }

  public void testMthod() throws Throwable {
    Class c = this.getClass();

    Class types[] = new Class[2];
    types[0] = Integer.TYPE;
    types[1] = Integer.TYPE;

    Method m = c.getMethod("update", types);

    Object inputParams[] = new Object[2];
    inputParams[0] = new Integer(1);
    inputParams[1] = new Integer(99);

    Integer obj = (Integer) m.invoke(this, inputParams);
    System.out.println(obj.intValue());          //输出结果是 100
  }
}
上述代码功能实现类似于下列一行语句:
this.update(1, 99);
通过使用反射的Method,从另外一个角度实现了方法调用,虽然代码显得繁琐一点,但在一些特殊情况下使用却非常方便,有很强的解耦性和简易性。
使用反射的Method语句Method m = c.getMethod("update", types),可以灵活地在运行时刻指定方法的名称和方法参数类型。例如可以通过Method m = c.getMethod("add", types) 调用add方法。这点非常适合本框架系统的远程方法调用。
客户端通过上节的动态代理机制直接调用远程的方法,调用远程方法的关键代码是在DynamicProxyClass的invoke方法中:
if (m.getName().equals(“myMethod”)){
    //通过网络协议调用远程的OriginClass
    BaseIF instance =  getRemoteOrigin ();
   return instance.myMethod ();
}
其中getRemoteOrigin ()的实现是关键。getRemoteOrigin ()实际是将方法和参数Method m和Object[] args通过协议传送到远程服务器,服务器获得客户端调用某个EJB的方法名、参数类型以及参数数值后,可以通过方法反射机制直接调用该EJB的方法。
这样,通过客户端的动态代理机制和服务器端的方法反射机制,就可以完成远程客户端对服务器EJB Service的访问。那么,如何将远程客户端调用的EJB方法名、参数类型和参数数值传送到服务器端?


7.2.4  HTTP协议和对象序列化

HTTP协议是一种请求/应答式的协议。以浏览器为例,当浏览器发送一个请求,在后台建立一个连接到服务器80端口的基于Socket的TCP连接,服务器处理后,返回该请求的应答。HTTP协议最新的版本是HTTP/1.1,HTTP/1.1由 RFC 2616 定义。
HTTP协议主要分请求和响应两大部分。
请求部分:请求行、请求头和请求正文,当一个浏览器访问http://localhost/ProxyServle网址时,浏览器将发出如下HTTP请求:
POST /ProxyServlet HTTP/1.1
Accept: text/plain; text/html
Accept-Language: en-gb
Connection: Keep-Alive
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
Content-Length: 33
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip,deflate,compress,identity

param1=1234&param2=6789&param=jdon.com
其中第一行是请求行,定义了请求方法POST或GET等方法以及URI资源;第二行开始是请求头,提供一些有关客户端环境和请求正文的帮助信息;请求头结束后,是一个空白行(只有CRLF符号的行),是请求头和正文之间的分界线。
HTTP响应分3个部分:状态行、响应头部和响应正文。示例如下:
HTTP/1.1 200 OK
Server: Apache/1.3.12 (Unix) mod_perl/1.24
Date: Fri, 20 Jul 2003 19:31:10 GMT
Content-Type: text/html
Last-Modified: Fri, 20 Jul 2003 18:34:06 GMT
Content-Length: 150
Connection: close

<html>
<head><title>welcome</title></head><body>
You are Welcome
</body></html>
状态行中包括状态码,如第一行中200表示成功,响应头部也包含一些例如服务器类型、日期时间、内容类型和长度等信息。
HTTP协议有如下特点。
简单快速,由于HTTP协议简单,使得HTTP两端的处理程序规模较小,处理速度比较快;相比Web Service的SOAP协议,HTTP协议无需携带复杂的XML格式文本,因而更加轻便,通信速度也很快。
HTTP可以传输任意类型的对象,传输类型由Content-Type加以标记,利用这个特点可以实现本框架系统的对象序列化传输,将对象序列化成字节进行传输。
每次请求/响应结束后,基于Socket的TCP连接就断开,属于非长连接,优点是可以节省输出时间,抗网络干扰强;每次连接结束后不留下任何状态,需要应用系统进行HTTPSession等状态保存。
HTTP协议可以穿透大多数安全防火墙,在远程通信方面,此点要优于Java的RMI。
HTTP/1.1还提供了身份认证、状态管理和Cache缓存等机制,特别是身份认证,结合J2EE提供的基于HTTP的安全认证机制,可以很方便地实现远程客户端和J2EE服务器的安全通信机制(更进一步可以采取HTTPS)。
综合以上特点,在本框架系统中,可以采取HTTP协议实现客户端和服务器通信的统一协议。
下面将讨论如何使用HTTP,协议携带数据对象。
HTTP协议实际上是由一个个数据字节组成的,如果需要HTTP协议携带一个对象,那么就必须将这个对象编码转换成数据字节流保存在HTTP协议的正文部分,将对象编码变成字节流的过程就是对象序列化;当服务器端接受到序列化的字节流后,需要再将之反转为对象,这就是对象的反序列化。
将对象从字节流中读出或者写入有两个主要类: ObjectOutputStream与ObjectInputStream 。ObjectOutputStream 提供将对象写入输出流的writeObject方法, ObjectInputStream提供从输入流中读出对象的readObject方法。注意使用这些方法的对象是可被序列化的。
例如,将一个对象序列化到一个HTTP连接代码如下:
ObjectOutputStream oos = new ObjectOutputStream(conn.getOutputStream());
oos.writeObject(myObject);
oos.close();
首先从一个HTTP连接中获取一个OutputStream,然后使用ObjectOutputStream将对象myObject序列化到这个HTTP连接中。
相反,从HTTP字节流中读取对象,代码如下:
ObjectInputStream ois = new ObjectInputStream(conn.getInputStream());
myObject = ois.readObject();
ois.close();
对象是否可以被序列化将也是非常重要的一个环节,能够被序列化的那些类必须继承实现java.io.Serializable 接口(或继承实现Externalizable 接口)。
在Java中下列一些类型和对象都可被序列化:原始变量如int float型、数组、Collection等,对于JDK中一些类无法确定是否可被序列化时,使用JDK提供工具serialver就可以检查。


7.2.5  框架设计图

结合前面系列章节的分析,针对本框架系统的设计需求,在图7-3中展示了如何结合HTTP协议实现方法的远程调用。


要实行一个方法的调用,比如下列语句:
EJBFacadeLocal eJBFacadeLocal = (EJBFacadeLocal)serviceFactory.getService();
eJBFacadeLocal.eJBmethod();          //远程方法调用
eJBmethod是接口eJBFacadeLocal的一个方法。eJBFacadeLocal通过动态代理获得的实例、eJBmethod方法和方法参数则被整合进入动态代理API的invocation handler中,因此需要在invocation handler的invoke方法中实现远程调用。
在上节动态代理中,这段代码示例是:
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable
  {
     if (m.getName().equals(“myMethod”)){
      //通过网络协议调用远程的OriginClass
      BaseIF instance =  getRemoteOrigin ();
     return instance.myMethod ();
    }
   }
其中getRemoteOrigin是实现远程调用的方法。在这个方法中将通过HTTP协议和对象序列化技术来实现远程调用,将eJBmethod方法和参数传送到服务器上,通过服务器的Servlet Proxy递交由EJB实现处理,处理结果再经过HTTP协议传回客户端。
由于eJBmethod方法和它的方法参数已经被整合进动态代理的Method m和Object[] args,因此真正需要被序列化的对象是动态代理的Method m和Object[] args。
运行serialver –java.lang.reflect.Method,检查发现java.lang.reflect.Method是不能被序列化的,因此,只能进一步分解Method。
研究服务器端的EJB方法调用发现,其实,EJB方法调用也并一定完全需要整个Method,它只需要Method 实例的两个值方法名称m.getName()和参数类型m.getParameterTypes()。
因此,可以将远程客户端调用EJB方法的方法名、参数类型和参数数值等3个参数序列化传送到服务器端。
图7-3只是显示了在肥客户端/服务器体系下本框架的技术实现图,为了形成一个灵活可伸缩的框架,本系统还需要考虑浏览器/服务器架构下的应用。
在浏览器/服务器架构下,Web层相对EJB来说,是本地客户端,可以通过RMI直接实现EJB的方法调用。在这种情况下,本框架非常类似于前面章节中讨论的Web和EJB接口框架系统,本框架与该接口框架相比,有下列几个优点。
在原来的接口框架系统下调用EJB时,需要一段代码首先实现EJB Home的JNDI寻找定位,这在本框架系统中已经不必要了,避免了过多的EJB相关对象。
在Web层通过动态代理直接调用EJB方法实现了表现层和核心逻辑业务层的完全解耦,可以分别独立开发以及维护,表现层无需处理一些EJB出错信息,如RemoteException等。
由于在动态代理中使用了缓存机制,提高了EJB调用性能。

7.2.6  HTTPSession和缓存机制

尽管在J2SE 1.4以后版本中,Java的反射机制性能得到了很大提高。但是在实际运行中,还是需要通过缓存Cache来进一步提高性能。
考察本系统有两处需要实现缓存:
一个是Proxy.newProxyInstance生成的动态对象实例,如果每次都使用这条反射语句获得动态对象实例,显然会影响速度,可以在第一次生成动态对象实例后将其保存起来,下次需要该对象实例时从缓存中获取。
另外一处是EJBLocalObject或EJBObject实例的生成,当EJB创建后,调用该EJB的客户端拥有一个指向EJBLocalObject或EJBObject的引用。
对于无状态Bean,每个无状态Home接口只有一个EJBLocalObject或EJBObject, EJBLocalObject或EJBObject被调用该无状态Bean的所有用户共享,容器负责创建或销毁无状态Bean的实例。
当每次通过调用无状态Bean的Home接口create()方法时,客户端会获得和上次同样的EJBLocalObject或EJBObject,对无状态Bean,将EJBLocalObject或EJBObject引用放入缓存是可以的。
对于有状态Bean,需要将EJBLocalObject或EJBObject引用加入HTTPSession缓存,因为一个客户端可能会在其Session周期内反复调用有状态Bean。
在实际操作中,无论是无状态Bean或有状态Bean调用,其实都可以使用HTTPSession来保存EJBLocalObject或EJBObject的引用,这样就不必客户端自己声明它调用的Session Bean是有状态还是无状态。
另外,在系统的开发和维护中,有状态Bean和无状态Bean都可能相互转换和变化,使用HTTPSession作为缓存就可以让系统对于这种变化有很强的适应性。
使用HTTPSession作为缓存也是有缺点的,它消耗了一定的内存空间,这也是缓存机制的缺点。

7.2.7  基于HTTP的安全机制

以上讨论基本解决了整个框架的流程设计以及相关性能问题,本节讨论的是安全验证问题,当肥客户端从远程访问J2EE服务器时,必须对这种访问进行用户名和口令验证。
从前面章节“用户安全管理系统”中已经知道,对用户名和口令进行拦截验证有两种主要方式:一种是使用容器的安全验证机制,如基于HTTP的验证登录机制;还有一种是自己实现用户名和口令的验证。可以根据实际情况进行两种方式的选用。
首先简要地讨论一下第二种实现方式:开发者自己实施用户名和口令的验证。这种方式实现起来比较复杂,需要分两步:
(1)实现对用户第一次登录的信息进行验证,通过对照数据库(或LDAP)中的用户登录名和密码,检查用户输入和数据库保存是否一致,如果一致,表示验证通过。
(2)需要将用户验证成功后的登录信息传达给EJB安全机制,假设使用的是JBoss服务器,那么需要执行如下代码:
//以login构造一个principal
Principal principal = new DefaultPrincipal(login);
Subject subject = new Subject();
//将构造的principal加入subject
subject.getPrincipals().add(principal);
//使用JBoss提供的ClientLoginModule登录模块
//该模块只是将用户的登录信息告诉EJB容器,不实行验证
LoginModule loginModule = new ClientLoginModule();

Hashtable options = new Hashtable(2);
//每个线程代表一个用户登录,每个用户有自己的基本空间
options.put("multi-thread", "true");   

//将登录名和密码传送给Jboss
options.put("password-stacking", "useFirstPass");
Hashtable sharedState = new Hashtable(2);
sharedState.put("javax.security.auth.login.name", login);
sharedState.put("javax.security.auth.login.password", password);

//实行初始化
loginModule.initialize(subject, null, sharedState, options);
loginModule.login();
相比上述复杂的实现过程,使用第一种方式,也就是容器的安全登录验证机制则要简单得多,“用户安全管理系统”中也是采取这种机制,这个机制实现需要简单的两步。
(1)在服务器的Web.xml配置中表明是基于HTTP的基本认证,如下:
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
其他关于LoginModule的配置参考“用户安全管理系统”的章节。
(2)在客户端将登录信息编码进入HTTP协议。
根据RFC2617,必须在HTTP协议的请求头部以<username>:<password>的形式提供验证信息,username和password分别表示base64加密的用户名和密码。具体形式如下:
Authorization: Basic <username>:<password>
为了测试基于HTTP客户端/服务器连接以及安全验证,建立一个服务器端Servlet如下:
public class TestServlet extends HttpServlet {

  //Initialize global variables
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
  }

  //Process the HTTP Post request
  public void doPost(HttpServletRequest request, HttpServletResponse
                     response) throws ServletException, IOException {

    String j_username = null;
    String j_password = null;
    try {
      j_username = request.getParameter("j_username");
      j_password = request.getParameter("j_password");
    } catch (Exception e) {
      e.printStackTrace();
    }
    response.setContentType("text/html");
    PrintWriter out = new PrintWriter(response.getOutputStream());
    out.println("<html>");
    out.println("<head><title>j_username</title></head>");
    out.println("<body> j_username=");
    out.println(j_username);
    out.println("<p> j_password=");
        out.println(j_password);
    out.println("</body></html>");
    out.close();
  }

  //Get Servlet information
  public String getServletInfo() {
    return "servlettest.aservlet Information";
  }
}
这个Servlet接受到客户端参数后将以HTML格式输出结果,再看客户端代码:
public class TestClient {
  public static void main(String[] args) {
    try {
      String url = "http://localhost:8080/test/TestServlet";
      //装入参数
      Hashtable param = new Hashtable();
      param.put("j_username", "banq");
      param.put("j_password", "1970513");

      HttpURLConnection conn =
           (HttpURLConnection) (new URL(url).openConnection());
      conn.setRequestMethod("POST");
      conn.setFollowRedirects(true);
      conn.setDoInput(true);
      conn.setDoOutput(true);
      conn.setUseCaches(false);
      conn.setRequestProperty("Content-type",
                              "application/x-www-form-urlencoded");
      //设置HTTP请求验证头部
      String password = "banq:555";
      String encoded = "Basic "+ Base64.encode(password.getBytes("UTF-8"));
      conn.setRequestProperty( "Authorization", encoded );

      // 模拟表单提交,将需要的Post参数写入HTTP连接
      PrintWriter out =
          new PrintWriter(new OutputStreamWriter(conn.getOutputStream()));
      String paramString = "";
      //以 key=param& key=param& key=param…形式写入参数
      for (Enumeration e = param.keys(); e.hasMoreElements(); ) {
        String key = (String) e.nextElement();
        String value = (String) param.get(key);
        // no harm for an extra & at the end of the parameter list
        paramString += key + "=" + URLEncoder.encode(value) + "&";
      }
      paramString = paramString.substring(0, paramString.length() - 1);
      out.println(paramString);
      out.close();
     
      System.out.println (“status = ” = conn.getResponseCode());
  
      // 读取服务器的响应信息
      BufferedReader br =
           new BufferedReader(new InputStreamReader(conn.getInputStream()));
      String line = null;
      while ( (line = br.readLine()) != null) {
        System.out.println(line);
      }
      br.close();
      conn.disconnect();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
该测试示例运行后,应该在客户端收到如下结果:
status = 200
<html>
<head><title>j_username</title></head>
<body> j_username=banq
<p> j_password=1970513
</body></html>
这是未启动J2EE服务器的安全验证机制情况下的结果,如果按照“用户安全管理系统”介绍的那样,启动了JBoss的安全验证机制,那么只需要改变String password = "banq:555"中用户和密码的值。如果是错误用户名和密码,那么将得到错误状态码: status = 401 表示该请求未得到授权验证。
比较上述两种权限验证体系:开发者自己实施验证和依赖容器实施验证,它们各有优缺点,前者可适应不同J2EE平台,但是依赖具体的数据库实施,和具体应用程序耦合性较强;后者实现虽然比较依赖J2EE的系统配置,但是使得程序代码更加简洁,是框架型产品推荐用法。本框架设计也将采取后者方式。

7.3  类的详细设计和实现

本框架比较复杂的是远程客户端对EJB方法调用。在这些具体流调用环节中,将要使用动态代理模式、通过HTTP实现对象序列化和反序列化、基于HTTP的安全验证以及EJB反射方法等技术。
以肥客户端/服务器体系为例,肥客户端通过本框架系统获得EJB的代理实例后,启动EJB 的方法调用,动态代理机制将调用的方法名、参数类型和参数数值进行打包序列化,向远程J2EE服务器发出HTTP的请求。
在图7-4中,Client作为本框架系统的应用客户端,主要是调用动态代理工厂ServiceClientFactory,ServiceClientFactory封装了所有远程调用细节。ServiceFactoryImp作为ServiceClientFactory的实现子类,实际上主要功能实现还是依赖动态代理API中的RemoteInvocationHandler的invoke方法,而RemoteInvocationHandler又将远程调用实现委托给了HTTP客户端HTTPClient,通过HTTPClient与远程J2EE服务器的HTTP Servlet发生交互。在HTTPClient中,根据需要发出HTTPRequest请求,从服务器获得的响应是HTTPResponse。

7.3.1  基本业务对象

在图7-4中,框架的应用客户端直接调用ServiceClientFactory,获得所要调用EJB的动态代理,以前面章节的用户安全管理系统为例,要获得当前用户资料的客户端代码如下:
//获得EJB定义实例
EJBDefinition SecurityFacade = new EJBDefinition(
      "java:comp/env/ejb/EJBContollerLocal",
      "com.jdon.security.auth.ejb.SecurityFacadeLocal");

//获得ServiceClientFactory单态类
ServiceClientFactory serviceFactory = ServiceClientFactory.getInstance();

//获得EJB SecurityFacadeLocal动态代理实例
SecurityFacadeLocal securityFacadeLocal =
          (SecurityFacadeLocal) serviceFactory.getService(SecurityFacade);

//直接调用远程EJB SecurityFacadeLocal的方法getUser()
User user = securityFacadeLocal.getUser();
上面是一个框架应用客户端调用本框架的具体代码,通过这段代码,这个客户端应该能获得远程服务器中当前用户资料。
在上面客户端代码中需要将有关EJB的定义信息传送到服务器端,因此本框架系统的第一个基本业务对象(Domain Object)就是EJB的定义信息,它主要是保存着EJB的两个主要数据JNDI名和EJB本地接口名。它在客户端生成后,将通过本框架系统一直传送到J2EE服务器的EJB调用层。
定义EJBDefinition的代码如下(程序7-1):
程序7-1
public class EJBDefinition implements Serializable {
  private String _jndiName;
  private String _local;
  public EJBDefinition(String p_jndiName, String p_localClassName) {
    _jndiName = p_jndiName;
    _local = p_localClassName;
  }
  public Class getEJBInterfaceClass() {
        return getLocalClass();
  }
  public Class getLocalClass() {
    try {
      return Class.forName(_local);
    } catch (ClassNotFoundException ex) {
      throw new RuntimeException("Unable to load the class : " + _local);
    }
  }
  public String getLocalName() {    return _local;  }
  public String getJndiName() {    return _jndiName;  }

}
除了EJBDefinition之外,客户端调用的EJB方法名称、参数类型以及参数数值都应该属于基本业务对象,因为它们的实现是使用动态代理API,因此将在下面各个环节中阐述到。

 

下页