第三节 编写Stateless Session Bean container
在第二节我们编写了一个能够初始化Bean并调用Bean中方法的框架。这一节我们对框架略加修改来完成无状态的Session Bean容器。
我们来看看什么是无状态Session Bean:
在EJB规范中,无状态Session Bean不维护客户的会话状态。当客户调用无状态Session Bean的方法时,bean实例的成员变量可能包含bean的状态,但是这个状态到bean方法调用执行完后就不再被保存了。除了bean的方法被调用期间,所有无状态Session Bean对象都是等价的,这允许EJB container可以将没有方法被调用的无状态Session Bean指派给任一客户。 |
这就是说,当客户A调用完无状态的Session Bean 的方法后,客户B可以立即调用这个Session Bean 的任一方法。为什么要这样呢?这是为了节省CPU资源和内存资源。
我们将在一给定的时间内没有任何方法被调用的Bean对象,称为在这段时间内空闲的对象,简称空闲对象。
既然无状态Session Bean不保留方法执行后的状态,那么Home接口中的create()方法就没有必要带参数创建Bean了(EJB规范中也规定无状态Session bean的create()方法不能带参数)。
实质上对无状态的Session Bean而言,EJB规范中也没有必要规定ejbCreate()方法了,因为ejbCreate()方法同其他的方法相比没有什么特殊性了。 |
现在我们明白了无状态Session Bean的含义了,那么在第二节中调用Home接口的create()方法创建的Bean对象可以放到一个Bean Pool中。如果第二个客户要求创建Bean对象并且刚好Bean Pool中有空闲的Bean对象,我们就不用真的创建Bean对象了而直接利用Bean Pool中的空闲Bean对象。为了管理空闲对象,我们设计了BeanPool类。BeanPool类的定义如下:
package ejb.sessionbean.stateless;
package ejb.sessionbean.stateless;
import java.util.ArrayList; import java.util.List;
public class BeanPool {
private Class beanClass_; private List freeBeans_ = new ArrayList();
public BeanPool( Class beanClass ) { beanClass_ = beanClass; }
public Object getBean() throws InstantiationException, IllegalAccessException{ synchronized(freeBeans_) { if( freeBeans_.isEmpty() ) return beanClass_.newInstance(); else return freeBeans_.remove( 0 ); }
}
public void freeBean( Object bean ) { synchronized(freeBeans_) { freeBeans_.add( bean ); } } } |
在BeanPool类中,getBean()方法获得一个空闲的Bean对象,如果没有空闲的bean对象,就创建一个新的Bean对象并返回该对象。如果有空闲的Bean对象,就返回一个空闲的Bean对象。由于getBean()方法有了创建Bean对象的能力,原来的Home接口和Remote接口可以只调用BeanPool.getBean()方法。freeBean()方法将一个bean对象标示为空闲对象。
现在我们来看在RemoteInvocationHandler中如何调用BeanPool.getBean()和BeanPool.freeBean()来创建Bean对象或获取空闲Bean对象和释放执行完某一方法后的Bean对象。
package ejb.sessionbean.stateless;
import java.lang.reflect.*; import java.rmi.*;
public class RemoteInvocationHandler implements InvocationHandler {
private Class beanClass_; private BeanPool beanPool_;
public RemoteInvocationHandler ( Class beanClass, BeanPool beanPool ) { beanClass_ = beanClass; beanPool_ = beanPool; }
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable{ return invoke( proxy, method.getName(), method.getParameterTypes(), args ); }
public Object invoke( Object proxy, String methodName, Class[] paramTypes, Object[] args ) throws Throwable{ Object bean = null; Object ret = null; try { Method m = beanClass_.getMethod( methodName, paramTypes );
bean = beanPool_.getBean(); ret = m.invoke( bean, args ); }catch( Exception ex ) { throw new RemoteException( "Fail to find or execute method:" + methodName, ex ); }finally { if( bean != null ) beanPool_.freeBean( bean ); return ret; } } } |
在调用ret = m.invoke( bean, args )之前从beanPool_中创建一Bean对象或获取一空闲Bean对象。在ret = m.invoke( bean, args )之后,将bean对象标示为空闲对象放入beanPool_中。
这个RemoteInvocationHandler 同第二节的有些不同:
public Object invoke( Object proxy,
String methodName,
Class[] paramTypes,
Object[] args ) throws Throwable
增加这个方法的目的是为了将ejbCreate方法看作为与其他Bean方法一样的方法。
现在让我们看看无状态Session Bean的HomeInvocationHandler是如何定义的,下面是是HomeInvocationHandler的定义:
package ejb.sessionbean.stateless;
import java.lang.reflect.*; import java.rmi.*;
public class HomeInvocationHandler implements InvocationHandler { private Class remoteClass_; private Class beanClass_; private BeanPool beanPool_;
public HomeInvocationHandler( Class remoteClass, Class beanClass ) { remoteClass_ = remoteClass; beanClass_ = beanClass; beanPool_ = new BeanPool( beanClass ); }
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { try { RemoteInvocationHandler handler = new RemoteInvocationHandler( beanClass_, beanPool_ ); Object proxyObj = Proxy.newProxyInstance( proxy.getClass().getClassLoader(), new Class[]{remoteClass_ }, handler ); if( method.getName().equals( "create" ) ) { handler.invoke( proxyObj, "ejbCreate", method.getParameterTypes(), args ); } return proxyObj; }catch( Exception ex ) { throw new RemoteException( "fail to call the method:" + method, ex ); } } } |
在这个HomeInvocationHandler的invoke()方法中不再显示创建Bean对象了,Bean对象的创建已经RemoteInvocationHandler中隐含的实现了。由于要能够支持用户指定的ClassLoader,在HomeInvocationHandler的构造函数变为了:
public HomeInvocationHandler( Class remoteClass, Class beanClass )
当用户每调用一次Home接口的create()方法时,HomeInvocationHandler都要创建RemoteInvocationHandler和Remote接口的代理对象。如果在HomeInvocationHandler中只创建一个RemoteInvocationHandler和Remote接口的代理对象也能满足EJB中无状态Session bean的要求就好了。如果只有一个线程调用创建的Remote接口的代理对象的方法,不用改HomeInvocationHandler的实现就可以满足要求。现在我们主要考虑多个线程同时调用Remote接口的代理对象的方法的情况。当调用RemoteInvocationHandler.invoke()方法时,Bean对象可以从BeanPool中安全的存取(因为存取Bean时要取得同步锁),所以即使只有一个RemoteInvocationHandler和一个Remote接口的代理对象也能支持多线程。那么只需要将RemoteInvocationHandler和Remote接口的代理对象的创建移入HomeInvocationHandler的构造函数即可:
package ejb.sessionbean.stateless;
import java.lang.reflect.*; import java.rmi.*;
public class HomeInvocationHandler implements InvocationHandler { private Class remoteClass_; private Class beanClass_; private BeanPool beanPool_; private Object proxyObj_; private RemoteInvocationHandler handler_;
public HomeInvocationHandler( Class remoteClass, Class beanClass ) { remoteClass_ = remoteClass; beanClass_ = beanClass; beanPool_ = new BeanPool( beanClass ); handler_ = new RemoteInvocationHandler( beanClass_, beanPool_ ); proxyObj_ = Proxy.newProxyInstance( remoteClass_.getClassLoader(), new Class[]{remoteClass_ }, handler_ ); }
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { try { if( method.getName().equals( "create" ) ) { handler_.invoke( proxyObj_, "ejbCreate", method.getParameterTypes(), args ); } return proxyObj_; }catch( Exception ex ) { throw new RemoteException( "fail to call the method:" + method, ex ); } }
} |
HomeProxyFactory可以定义为:
package ejb.sessionbean.stateless;
import java.lang.reflect.*;
public class HomeProxyFactory { public static Object createHomeProxy( String homeClassName, String remoteClassName, String beanClassName ) throws ClassNotFoundException { return createHomeProxy( Class.forName( homeClassName ), Class.forName( remoteClassName ), Class.forName( beanClassName ) ); }
public static Object createHomeProxy( ClassLoader cl, String homeClassName, String remoteClassName, String beanClassName ) throws ClassNotFoundException { return createHomeProxy( Class.forName( homeClassName, true, cl ), Class.forName( remoteClassName, true, cl ), Class.forName( beanClassName, true, cl ) ); }
public static Object createHomeProxy( Class homeClass, Class remoteClass, Class beanClass ) { try { InvocationHandler handler = new HomeInvocationHandler ( remoteClass, beanClass );
return Proxy.newProxyInstance( homeClass.getClassLoader(), new Class[]{homeClass}, handler ); }catch( Exception ex ) { throw new RuntimeException( ex ); } }
}
|
现在,我们可以创建一个与无状态Session Bean行为的Container了:
package ejb.sessionbean.stateless;
public class Container {
private Object homeObj_;
public Container(String homeClassName, String remoteClassName, String beanClassName ) throws ClassNotFoundException { this( ClassLoader.getSystemClassLoader(), homeClassName, remoteClassName, beanClassName); }
public Container( ClassLoader cl, String homeClassName, String remoteClassName, String beanClassName,) throws ClassNotFoundException { this( Class.forName( homeClassName, true, cl ), Class.forName( remoteClassName, true, cl ), Class.forName( beanClassName, true, cl ) ); }
public Container( Class homeClass, Class remoteClass, Class beanClass ) throws ClassNotFoundException { homeObj_ = HomeProxyFactory.createHomeProxy( homeClass, remoteClass, beanClass ); }
public Object getHomeProxy() { return homeObj_; } } |
这个Container只能从本地调用,还不具备从网络调用的能力。如果将创建的Home接口代理和Remote接口代理export到网络上去,就能从网络上调用了。EJB container使用的是RMI-IIOP网络协议,我们看看如何将Home接口代理和Remote接口代理export到RMI-IIOP网络上去。最简单的办法是创建Home接口代理和Remote接口代理的时候将它们export出去,因此我们需要修改HomeInvocationHandler类和Container类。
修改后的HomeInvocationHandler如下:
package ejb.sessionbean.stateless;
import java.rmi.*; import javax.rmi.PortableRemoteObject; import java.lang.reflect.*;
public class HomeInvocationHandler implements InvocationHandler { private Class remoteClass_; private Class beanClass_; private BeanPool beanPool_; private Object proxyObj_; private RemoteInvocationHandler handler_;
public HomeInvocationHandler( Class remoteClass, Class beanClass ) throws RemoteException { remoteClass_ = remoteClass; beanClass_ = beanClass; beanPool_ = new BeanPool( beanClass ); handler_ = new RemoteInvocationHandler( beanClass_, beanPool_ ); proxyObj_ = Proxy.newProxyInstance( remoteClass_.getClassLoader(), new Class[]{remoteClass_ }, handler_ ); PortableRemoteObject.exportObject( (Remote)proxyObj_ ); }
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { try { if( method.getName().equals( "create" ) ) { handler_.invoke( proxyObj_, "ejbCreate", method.getParameterTypes(), args ); } return proxyObj_; }catch( Exception ex ) { throw new RemoteException( "fail to call the method:" + method, ex ); } }
} |
新的HomeInvocationHandler只增加了PortableRemoteObject.exportObject( (Remote)proxyObj_ ); 这条语句是将创建的Remote接口的代理对象export出去,网络客户端可以通过网络存取创建的Remote接口代理。
下面是修改后的Container类:
package ejb.sessionbean.stateless;
import java.rmi.Remote; import java.rmi.RemoteException; import javax.rmi.PortableRemoteObject; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.CompositeName;
public class Container {
private Object homeObj_;
public Container(String homeClassName, String remoteClassName, String beanClassName, String bindName, Context ctx ) throws RemoteException, NamingException, ClassNotFoundException { this( ClassLoader.getSystemClassLoader(), homeClassName, remoteClassName, beanClassName, bindName, ctx ); }
public Container( ClassLoader cl, String homeClassName, String remoteClassName, String beanClassName, String bindName, Context ctx ) throws RemoteException, NamingException, ClassNotFoundException { this( Class.forName( homeClassName, true, cl ), Class.forName( remoteClassName, true, cl ), Class.forName( beanClassName, true, cl ), bindName, ctx ); }
public Container( Class homeClass, Class remoteClass, Class beanClass, String bindName, Context ctx ) throws RemoteException, NamingException, ClassNotFoundException { homeObj_ = HomeProxyFactory.createHomeProxy( homeClass, remoteClass, beanClass ); PortableRemoteObject.exportObject( (Remote)homeObj_ ); homeObj_ = PortableRemoteObject.toStub( (Remote)homeObj_ ); rebind( ctx, bindName, homeObj_ ); }
public Object getHomeProxy() { return homeObj_; }
private static void rebind( Context ctx, String name, Object obj ) throws NamingException { CompositeName compName = new CompositeName( name );
if( compName.size() == 1 ) ctx.rebind( name, obj ); else { int i = 0, n = compName.size() - 1; for( ; i < n; i++ ) { Context localCtx = null;
try { localCtx = (Context)ctx.lookup( compName.get( i ) ); }catch( Exception ex ) { } if( localCtx != null ) ctx = localCtx; else ctx = ctx.createSubcontext( compName.get( i ) );
} ctx.rebind( compName.get( n ), obj ); } }
} |
修改后的Container增加了如下语句:
PortableRemoteObject.exportObject( (Remote)homeObj_ );
homeObj_ = PortableRemoteObject.toStub( (Remote)homeObj_ );
rebind( ctx, bindName, homeObj_ );
并且每一个构造函数都增加了参数bindName和ctx,这两个参数是为了将Home接口代理和Remote接口代理export到网络,客户端可以通过网络调用我们的Home接口代理和Remote接口代理。
现在我们的无状态Session Bean已经完成了。我们的无状态Session Bean能否工作呢,现在就让我们写一个测试程序,测试一下。
我们以J2EE教程的Converter作为测试用例:
Convert的Home接口定义为:
package converter;
import java.rmi.RemoteException;
public interface ConverterHome extends java.rmi.Remote { Converter create() throws RemoteException; } |
Convert的Remote接口定义为:
package converter;
import java.rmi.RemoteException; import java.math.*;
public interface Converter extends java.rmi.Remote { public BigDecimal dollarToYen(BigDecimal dollars) throws java.rmi.RemoteException;
public BigDecimal yenToEuro(BigDecimal yen) throws java.rmi.RemoteException; } |
ConvertBean定义如下:
package converter;
import java.rmi.RemoteException; import java.math.*;
public class ConverterBean { BigDecimal yenRate = new BigDecimal("121.6000"); BigDecimal euroRate = new BigDecimal("0.0077");
public ConverterBean() { }
public BigDecimal dollarToYen(BigDecimal dollars) { BigDecimal result = dollars.multiply(yenRate);
return result.setScale(2, BigDecimal.ROUND_UP); }
public BigDecimal yenToEuro(BigDecimal yen) { BigDecimal result = yen.multiply(euroRate);
return result.setScale(2, BigDecimal.ROUND_UP); }
public void ejbCreate() { }
public void ejbRemove() { }
public void ejbActivate() { }
public void ejbPassivate() { } } |
可以看到上面我们定义的Home接口、Remote接口、Bean的定义与EJB2.X的要求有些差别,没有从EJB2.X规范中的EJBHome、EJBObject和SessionBean接口继承。其实,EJB2.X规范中的EJBHome接口、EJBObject接口、SessionBean接口主要是为开发EJB Container的开发者所使用,不是为了给最终使用EJB Container的开发者所使用。正是这个原因,将EJB Container开发者所要使用的接口暴露给最终开发者使EJB2.X遭受到了许多批评。
我们定义了Home接口,Remote接口和ConvertBean,但是谁来初始化我们的Container呢?下面写了一个十分简单的Server来帮我们初始化Container:
import ejb.sessionbean.stateless.*; import javax.naming.*;
public class Server { public static void main( String[] args ) { try { InitialContext initialNamingContext = new InitialContext(); Container container = new Container( "converter.ConverterHome", "converter.Converter", "converter.ConverterBean", "java:comp/env/ejb/SimpleConverter", initialNamingContext ); }catch( Exception ex ) { ex.printStackTrace(); } } } |
Convert client的代码如下:
import converter.Converter; import converter.ConverterHome; import javax.naming.Context; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; import java.math.BigDecimal;
public class ConverterClient { public static void main(String[] args) { try { Context initial = new InitialContext(); Context myEnv = (Context) initial.lookup("java:comp/env"); Object objref = myEnv.lookup("ejb/SimpleConverter");
ConverterHome home = (ConverterHome) PortableRemoteObject.narrow(objref, ConverterHome.class);
Converter currencyConverter = home.create();
BigDecimal param = new BigDecimal("100.00"); BigDecimal amount = currencyConverter.dollarToYen(param);
System.out.println(amount); amount = currencyConverter.yenToEuro(param); System.out.println(amount);
System.exit(0); } catch (Exception ex) { System.err.println("Caught an unexpected exception!"); ex.printStackTrace(); } } } |
将上面的代码编译,就可以执行了,首先启动JNDI
orbd -ORBInitialPort 1060
然后启动我们的Server
java –classpath . -Dcom.sun.CORBA.ORBUseDynamicStub=true -Djava.naming.factory.initial=com.sun.jndi.cosnaming.CNCtxFactory -Djava.naming.provider.url=iiop://localhost:1060 Server
最后启动我们的ClientConverter
java –classpath . -Dcom.sun.CORBA.ORBUseDynamicStub=true -Djava.naming.factory.initial=com.sun.jndi.cosnaming.CNCtxFactory -Djava.naming.provider.url=iiop://localhost:1060 ConverterClient