EJB3 透明处理远程和本地调用的问题
———问题提出
EJB3提出了4中接口类型包括:远程接口(remote interface)、本地接口(local interface)端点接口(endpoint interface)和消息接口(message interface),本文主要议论前两种接口。
〇远程接口定义了session bean(具体接口的实现类)的业务方法,这些方法可以被来自EJB容器以外的应用访问到。它是个普通的Java接口。被标注了@java.ejb.Remoter注解;
〇本地接口同样定义了session bean得业务方法,这些方法可以被处于同一EJB容器的其他bean使用:也就是bean提供给运行于同一JVM中其他bean的业务方法。被标注了@javax.ejb.Local注解;
session bean的客户端端从来不于session bean class直接打交道,相应的,客户端必须始终使用session bean的组件接口所提供的方法来完成工作。尽管本地接口不涉及分布式对象协议,但他们人就为bean class提供了一个代理或存根。
在实际工作中我们的业务为了支持多种情况(远程或本地访问),我们是否可以让接口同时标注@java.ejb.Remoter和@javax.ejb.Local让其支持两种访问方式,使之在服务器端透明。在Client端我们定义一个全局常量(targetServicerLocation)来标注目前client和Server是否在同一jvm中。然后再到Clent端通过targetServicerLocation的值进行分支处理。不就可以完成在服务器端或者在Client端都透明了吗?
———服务器端透明的处理
以《EnterPrise JavaBean 3.0》中的Tiain系统为假想业务,来探索我们的处理。
Tiain系统是一个船运系统。既然是个船运系统,必然有实体Cabin(船舱),我们就这一实体的CRUD展开议论。
先定义Cabin实体
import javax.persistence.* ;
@Entity
@Table(name="CABIN")
public class Cabin {
private int id;
private String name;
private int deckLevel;
@Id
@GeneratedValue
@Column(name="CABIN_ID")
public int getId( ) { return id; }
public void setId(int pk) { this.id = pk; }
@Column(name="CABIN_NAME")
public String getName( ) { return name; }
public void setName(String str) { this.name = str; }
@Column(name="CABIN_DECK_LEVEL")
public int getDeckLevel( ) { return deckLevel; }
public void setDeckLevel(int level) { this.deckLevel = level; }
}
业务逻辑的定义
public interface TravelAgent {
/**创建船舱*/
public void createCabin(Cabin cabin);
/**获得船舱*/
public Cabin findCabin(int id);
}
为了让其支持远程和本地调用我们为之标注@java.ejb.Remoter和@javax.ejb.Local变成这样;
@Remote
@Local
public interface TravelAgent {
/**创建船舱*/
public void createCabin(Cabin cabin);
/**获得船舱*/
public Cabin findCabin(int id);
}
接口实现类
package com.titan.travelagent;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.titan.domain.Cabin;
@Stateless
public class TravelAgentBean implements TravelAgent{
@PersistenceContext
(unitName="titan")
private EntityManager manager;
public void createCabin(Cabin cabin) {
manager.persist(cabin);
}
public Cabin findCabin(int pKey) {
return manager.find(Cabin.class, pKey);
}
}
ok,到目前为止开来一切都非常顺利。我们定义了一个接口,并且标注@Remote和@Local。这样我们就可以通过远程和本地方法调用了。
部署测试!
结果大大的异常
java.lang.RuntimeException: An exception occurred initialising interceptors for class com.titan.travelagent.TravelAgentBean.equals
想想就明白了,如果可以这么搞EJB3规范早就这么搞了,还要我在这费心思!但是要同时实现远程接口和本地接口的需求实在很迫切。难道写两个接口仅仅是名字不同和标注的注解不同吗?我们可不可以将公用的代码提出来。
改造如下
1、将TravelAgent的@Remote和@Local去掉,加入如下两个接口
package com.titan.travelagent;
import javax.ejb.Remote;
@Remote
public interface TravelAgentRemote extends TravelAgent{}
package com.titan.travelagent;
import javax.ejb.Local;
@Local
public interface TravelAgentLocal extends TravelAgent{}
改造TravelAgentBean时之实现这两个接口
package com.titan.travelagent;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.titan.domain.Cabin;
@Stateless
public class TravelAgentBean implements TravelAgentLocal,TravelAgentRemote{
@PersistenceContext
(unitName="titan")
private EntityManager manager;
public void createCabin(Cabin cabin) {
manager.persist(cabin);
}
public Cabin findCabin(int pKey) {
return manager.find(Cabin.class, pKey);
}
}
至此服务器端的透明处理就完成了。写来开一下Client端的透明处理
———Client端的透明处理
文章开头简单提了一下思路,就是定义一个常量argetServicerLocation来标注目前client和Server是否在同一jvm中,然后更具argetServicerLocation值得不同来进行分支逻辑。
我们已java web开发为例,这个常量的首选位置当然是<context-param>了
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>targetServicerLocation</param-name>
<param-value>remote</param-value>
<description>标志是远程访问还是本地访问 local/remote</description>
</context-param>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
写个jsp页面读取targetServicerLocation值进行分支处理
首选看一下远程调用和本地调用有什么不同
1、获取JndiContext的方式不同,本地调用直接使用系统中的上下文,而远程调用需要制定相应的Properties;
2、session bean 自动绑定到jndi的名字不同 本地:TravelAgentBean/local 远程:TravelAgentBean/remote;
3、获取的对象类型不同 本地:TravelAgentLocal 远程TravelAgentRemote
对于第1、2个问题,都不算问题,一判断一个if就可以搞定,第3个问题也不难办。我们可以使用TravelAgent来保存TravelAgentLocal或者TravelAgentRemote,让它多态去吧!
代码实现(示例就直接写到jsp了)
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ page import="javax.naming.*,com.titan.travelagent.*,com.titan.domain.*" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>My JSP 'testLocal.jsp' starting page</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="This is my page">
<!--
<link rel="stylesheet" type="text/css" href="styles.css">
-->
</head>
<body>
<p>目标服务提供商位置:<%=getServletContext().getInitParameter("targetServicerLocation")%></p>
<%
String targetServicerLocation = getServletContext().getInitParameter("targetServicerLocation");
InitialContext jndiContext = null;
if(targetServicerLocation.equals("local")){
jndiContext = new javax.naming.InitialContext();
}else{
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");
jndiContext = new javax.naming.InitialContext(p);
}
jndiContext = new InitialContext();
String jndiName = "TravelAgent"+"Bean/"+(targetServicerLocation.equals("local")?"local":"remote");
Object ref = jndiContext.lookup(jndiName);
/使用TravelAgentTravelAgentLocal或者TravelAgentRemote
TravelAgent dao = (TravelAgent)ref;
Cabin cabin_2 = dao.findCabin(1);
out.println(cabin_2.getName( ));
out.println(cabin_2.getDeckLevel( ));
out.println(cabin_2.getShipId( ));
out.println(cabin_2.getBedCount( ));
%>
</body>
</html>
部署测试,
首先部署到不同的jvm中,我这里将ejb部署到jboss中,web部署到tomcat中。测试远程访问。
结果没问题一切工作完美。
然后将targetServicerLocation的值修改为local全部部署到jboss中。问题出来了!
类型转换失败 $.proxy72不能转换为TravelAgent!
————问题分析
文章开头提到了,通过jndi拿到的永远只是代理对象而不是实际对象。可是为什么部署到不同的jvm中可以正常工作而部署在同一jvm中就罢工了呢?
其实代理对象的生成有两种方式
第一种是基于接口的,他会使用java的动态代理技术生成代理对象。从而为原始对象添加新的特性或者方法。缺点是,不能将代理对象通过类型转换转换为原始对象。
知识链接:Spring中大量使用这种技术实现代理对象的生成。这是Spring生成代理对象的首选方式。
第二种是基于字节码,或者类加载器的。他可以通过cglib、或者classLoder等等。直接将逻辑插入到已经编译好的类。可以通过类型转换转换为原始对象。
知识链接:这是Spring的备选方案,若果你的类没有实现任何接口,却要织入逻辑的话便采用这种方式。
JBoss显然对远程和本地接口生成代理时使用了这两种不同的策略。其中远程接口是使用第二种策略,而本地接口则使用了第一种策略。所以造成了失败。
由于对JBoss的代理生成没有进行过升入研究。目前无法解决透明的Client端访问。欢迎批评、指正和赐教。
[解决办法]
刚看到,不好意思,只能恭喜楼主已经研究出结果。
不过,其实做法本质上跟我们EJB2.0时代做法,是基本一致的;只不过那时候不支持注解。