在OECP(Open Enterprise Components Plateform,开放的企业级组件平台)项目的架构体系中,各业务组件需要有运行在不同的EJB容器中的能力,在组件层构建起一个业务处理的分布式集群环境。
我们知道在客户端我们需要通过JNDI来调用EJB容器中的session bean,在EJB3中,获得JNDI上下文主要有两种方式。(默认采用jboss的获取方式)
1、通过程序编码的方式
public static InitialContext getInitialContext() throws NamingException{
Properties p = new Properties();
p.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
p.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");
p.put(Context.PROVIDER_URL, "jnp://localhost:1099");
return new InitialContext(p);
}
客户端通过InitialContext ctx = getInitialContext();的方式来获取上下文。
2、通过配置属性文件的方式
在应用的src下新建一个jndi.properties的资源文件(注意文件名必须是jndi.properties)
内容:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces
java.naming.provider.url=localhost
客户端只要通过下面的方法就能获得上下文对象了
InitialContext ctx = new InitialContext();
为了方便在程序开发的过程中的测试,我们一般情况下会封装一个获得JNDI上下文的工具类来使用。比如我们构建的一个如下的EJBFinder.java的类:
public class EJBFinder {
public static InitialContext getInitialContext() throws NamingException{
Properties p = new Properties();
p.put("server", "jboss");
p.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
p.put(Context.URL_PKG_PREFIXES,
"org.jboss.naming:org.jpn.interfaces");
p.put(Context.PROVIDER_URL, "jnp://localhost:1099");
InitialContext ctx = new InitialContext(p);
return ctx;
}
public static Object findEJB(String ejbname) throws NamingException{
return getInitialContext().lookup(ejbname);
}
}
这样的一个工具类给我们测试EJB组件提供了很大的方便,不用我们再去重复的去构建JNDI上下文的环境。但是当EJB组件开发完成后,正式部署到正式环境中,客户端调用EJB组件怎样去调用呢?现在我们分析这其中出现的问题。
1、 客户端不在同一个JVM环境中,应该怎样处理?
2、 Session Bean对于客户端是隐藏的,开发客户端的程序员怎么能非常方便的获得Session Bean的绑定名?怎么能方便的得到JNDI上下文呢?
3、 各EJB组件分别部署在不同的服务器上,我们怎么能方便的调用?
4、 各EJB组件如果运行在不同的EJB容器中,我们怎么能方便的得到对应的JNDI上下文环境?
针对这些问题,我们逐步的去分析解决。
客户端不在同一个JVM中这是普遍出现的情况,我们可能将web客户端运行在Tomcat上,而EJB运行在JBoss容器中,对于这种情况,我们如果采用上面介绍的第一种情况来获得JNDI上下文就非常不可行,因为EJB的环境的变化会导致太多程序代码的改动,为日后的维护埋下了隐患。所以只能采用第二种方式,将JNDI上下文信息配置在属性文件中,即使以后EJB的环境更换,也可以通过修改配置文件来实现这种改变。
通过JNDI的方式来调用EJB的Session Bean,必然少不了为Session Bean绑定JNDI名。默认的情况下(JBoss),容器会以实现类的类名作为JNDI名。比如getIntialContext().lookup(“LoginBean/remote”); LoginBean就是Login接口的实现类的名字。
@Stateless
@Remote(Login.class)
public class LoginBean implements Login{}
如果这样去做的话,我们开发客户端的程序员就必须要知道这个Session Bean接口的实现类是什么名字,这不是一种好的方式,违背了我们隐藏实现的编程思想。才开始大家建议,约定Session Bean的命名格式为接口+Bean后缀,这是一个比较可行的解决方案。但是一个约定好的命名格式,只能给我们的是一个已知的字符串,我们利用这个字符串所能做的事情太少了。我们利用@Stateless中的name属性,可以为Session Bean起个别名,这个东西应该很好的利用起来。我们再一次讨论,对于客户端来说,什么是透明的呢?无疑是接口,我们能不能把接口的名字作为Session Bean的别名呢。这个主意不错,大家一致通过,看了网上的一些例子,好多人也是采用这种方式。但是也有不好的方面,如果一个Session Bean实现多个接口就不是很好处理了。我们只能再次约定,一个Session Bean只能对应着一个远程接口。这样我们只知道接口就能方便的调用EJB了。
但问题又接踵而至,我们使用EJB作为组件开发技术,很大程度上是利用它强大的分布式计算的能力,这就要求每个EJB组件有可能运行在不同的服务器上,我们调用这些EJB组件,只采用一套上下文配置的方式是非常不可行的。我们需要为每个组件配置各自的JNDI上下文环境。怎么去做呢?在客户端我们为每个组件建立一个Properties文件,里面包含了该组件的JNDI配置。我们调用不同的组件时,我们就去获取该组件的JNDI配置,这已经很好的解决了相关的问题,完全可以适应EJB分布式的环境,但是有没有更好的方法让用户不再去选择每个组件的配置,让他们感觉和本地调用一样简单呢。于是我们在Session Bean接口上做了一些文章,通过自定义annotation的方式,来为每个接口标志相关的组件信息,通过程序解析annotation来获得组件的信息来进行相关的处理。我们定义一个@ComponentInfo的元数据来标注组件的相关信息。
@Target( { TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentInfo {
String name();
String description() default "";
}
name属性为组件的名字,description顾名思义就是组件的描述,通过作用在每个接口上的这个annotation,我们就可以获得组件名称。我们约定客户端的JNDI配置的属性文件的文件名要和这里的组件名称对应,这样我们就可以自动找到该组件需要的JNDI配置。
为了方便EJB组件的开发,我们可以为每个EJB组件做一个全局的接口。将该annotation作用在最上层父类的TYPE上,而该组件所有的Session Bean接口都继承这个接口。
问题都解决了吗?我们不断地寻找问题。我们在这里不得不对我们尊敬的SUN一口恶骂,骂它做事情什么时候能做的彻底,为什么不统一标准,让所有的EJB容器都采用同样格式的JNDI获取方式。现在问题来了,不同的容器存在自己特有的lookup实现方式,太郁闷了,这样就要将EJB组件和容器绑死了,这为以后EJB组件的移植设置了巨大的障碍。我们思考着,这个问题似曾相识,对,用工厂模式来解决这种移植性的问题,对不同的实现通过工厂的方式进行集中管理,从而方便系统的移植。说干就干,我们建立一个公共的接口:
public interface JNDIFinder {
public Object lookup(InitialContext ctx, Class clazz)throws NamingException;
}
我们为每个容器建立自己的JNDI查找实现类:JbossJNDIFinderImpl.java,WeblogicJNDIFinderImpl.java
然后再建立JNDIFinder的工厂来进行管理和获取:JNDIFactory.java
public static JNDIFinder getFinder(String server) {
if (JBOSS_SERVER.equals(server)) {
return new JbossJNDIFinderImpl();
} else if (WEBLOGIC_SERVER.equals(server)) {
return new WeblogicJNDIFinderImpl();
}
return new JbossJNDIFinderImpl();// 默认返回jboss的实现
}
而getFinder(String server)中的server我们可以将它配置到我们的JNDI配置文件中:
server=jboss
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.factory.url.pkgs=org.jboss.naming:org.jpn.interfaces
java.naming.provider.url=jnp://localhost:1099
万事俱备只欠东风了,我们最后把这些要统一封装成一个最简单方法来进行调用。我们建立一个工具类JNDIUtil.java,在该类中,我们向外暴露一个静态方法:
public static Object lookup(Class clazz) throws NamingException
大功告成了,现在只需要一个简单的调用Login login =(Login)JNDIUtil.lookup(Login.class); 就解决了上面提出的相关问题,实现了跨EJB服务器的分布式EJB组件的调用。(后面将附上该方案的相关源代码)
当然,这个解决方案还需要经过实际开发的检验,我们也将不断地优化该方案,也希望更多人讨论交流,提出宝贵的意见,目的是为了更好的解决该问题。
原文: