简介
尽管WebSphere/WebLogic都是J2EE应用服务器,但是由于J2EE标准本身的原因,以及不同应用服务器提供不尽相同的特性,而程序员在开发应用的时候又没有考虑到应用要兼容不同应用服务器,这就出现了J2EE应用在不同应用服务器上的移植问题。
下面介绍我们在把J2EE Web应用从WebLogic移植WebSphere应用服务器过程中遇到的一些问题和解决办法。
至于更详细的系统环境准备,移植步骤等细节,请参考IBM红皮书,如Migrating WebLogic Applications to WebSphere V5, REDP0448。
一、Servlet/JSP移植问题
WebSphere 4/5和WebLogic 6.1应用服务器中的JSP编译器对于空对象(null String, null object等)的处理是不同的。
在Welbogic 6.1当中,如果字符串为null,或者对象为null,那么使用PrintWriter输出该对象的时候,输出的是长度为0的字符串"";而在WebSphere 4/5、Tomcat 4.1以及WebLogic 7.0当中是输出了长度为4的字符串"null"。
下面servlet/jsp的例子在WebLogic 6.1/WebSphere中的运行结果是截然不同的。
servlet测试代码:
java.io.PrintWriter out = response.getWriter();
out.println(null);
jsp测试代码:
<% String s = null; %>
<%=s%>
或者
<% Integer i = null; %>
<%=i%>
解决办法1:
所有要在Servlet/JSP输出的对象都有初始值,换言之就不会有输出空对象的情况。这样在servlet/jsp当中通过PrintWriter输出对象的时候就不会出现"null"字样。
解决办法2:
如果整个Web应用已经编写完毕,没有时间去修改包含业务逻辑的代码,那么可以使用如下的类(java class)处理servlet/jsp的输出。对于jsp页面通常可以通过手工替换<%=为<%=NullObject.get(,替换%>为)%>。
package utils;
public class NullObject {
public static String get(String o) {
return (o == null) ? "" : o;
}
public static Integer get(Integer o) {
return (o == null) ? new Integer(0) : o;
}
public static Long get(Long o) {
return (o == null) ? new Long(0) : o;
}
public static Object get(Object o) {
return (o == null) ? "" : o;
}
}
比如jsp代码片断<%=s%>可以修改为<%=NullObject.get(s))%>
注:在Java Language Specification [sec 15.18.1.1 String Conversion]中写到 If the reference is null, it is converted to the string "null" (four ASCII characters n, u, l, l). Otherwise, the conversion is performed as if by an invocation of the toString method of the referenced object with no arguments; but if the result of invoking the toString method is null, then the string "null" is used instead.
The toString method is defined by the primordial class Object; many classes override it, notably Boolean, Character, Integer, Long, Float, Double, and String.
二、JNDI移植问题
2.1 上下文工厂
J2EE程序在访问不同J2EE应用服务器名字空间的时候,需要指定相应服务器的名字空间上下文的工厂class名称,名字空间提供者的URL。
WebLogic相应参数分别为weblogic.jndi.WLInitialContextFactory和t3://localhost:7001
WebLogic创建InitialContext的样例代码:
Properties h = new Properties();
h.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
h.put(Context.PROVIDER_URL,"t3://localhost:7001");
InitialContext ctx = new InitialContext(h);
WebSphere相应参数分别为com.ibm.websphere.naming.WsnInitialContextFactory和iiop://localhost:2809/
WebSphere创建InitialContext的样例代码:
Properties h = new Properties();
h.put(Context.INITIAL_CONTEXT_FACTORY,
" com.ibm.websphere.naming.WsnInitialContextFactory ");
h.put(Context.PROVIDER_URL,"iiop://localhost:2809/");
InitialContext ctx = new InitialContext(h);
如果是访问本地应用服务器,可以采用缺省配置创建名字空间上下文,代码如下:
InitialContext ctx = new InitialContext();
推荐应用程序采用共同的类访问JNDI名字空间,比如com.xxx.utils.Xxx,该类是singleton的,提供static方法来获得名字空间上下文。这样做的好处是
应用中访问名字空间的代码都在这个类中,应用程序的移植性好
采用singleton设计模式和static方法,可以减少对象的重复生成,减少JVM的垃圾回收
2.2 java.user.Transaction
在WebLogic 6.1中,javax.transaction.UserTransaction是通过名字javax.transaction.UserTransaction查找到的,而WebSphere 5中是通过名字jta/usertransaction查找到的。
WebLogic例子代码如下:
import javax.transaction.UserTransaction;
(UserTransaction)getInitialContext().lookup("javax.transaction.UserTransaction");
WebSphere例子代码如下:
(UserTransaction)getInitialContext().lookup("jta/usertransaction")
三、EJB访问
EJB 1.0,java程序使用EJB部署后的JNDI名称访问EJB的例子代码如下:
InitialContext ctx = new InitialContext();
Object home = ctx.lookup("ejb/account");
AccountHome accountHome =
(AccountHome) PortableRemoteObject.narrow(home, AccountHome.class);
EJB 1.1引入了EJB Refrence,java程序访问EJB的例子代码如下:
InitialContext ctx = new InitialContext();
Object home = ctx.lookup("java:comp/env/ejb/account");
AccountHome accountHome =
(AccountHome) PortableRemoteObject.narrow(home, AccountHome.class);
在EJB 2.0,引入了EJB local home interface,从此java客户程序可以通过引用调用在一个JVM中的EJB。EJB Refrence,java程序访问EJB的例子代码如下:
InitialContext ctx = new InitialContext();
Object home = ctx.lookup("java:comp/env/ejb/account");
AccountHome accountHome = (AccountHome) home;
强烈推荐使用EJB Reference访问EJB,使用EJB Refrence访问EJB有很多好处。在程序移植方面,各种应用服务器对local home的EJB在名字空间中部署是不一样的,但是都可以通过EJB Refrence访问到local home的EJB。
在WebLogic 6.1中,可以在EJB部署描述文件weblogic-ejb-jar.xml 中定义具有Local Home 的EJB的local jndi name。类如下面的部署描述符指定了具有local home的EJB Account的local jndi name为ejb/account,具有remote home的EJB AccountAccess的jndi name为ejb/accountaccess。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE weblogic-ejb-jar PUBLIC '-//BEA Systems, Inc.//DTD WebLogic 6.0.0 EJB//EN'
'http://www.bea.com/servers/wls600/dtd/weblogic-ejb-jar.dtd'>
<weblogic-ejb-jar>
<weblogic-enterprise-bean>
<ejb-name>Account</ejb-name>
<local-jndi-name>ejb/account</local-jndi-name>
</weblogic-enterprise-bean>
<weblogic-enterprise-bean>
<ejb-name>AccountAccess</ejb-name>
<jndi-name>ejb/accountaccess</jndi-name>
</weblogic-enterprise-bean>
</weblogic-ejb-jar>
在WebLogic中,java代码中使用EJB部署后的JNDI名称访问local home/remote home的EJB例子代码如下:
Properties h = new Properties();
h.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
h.put(Context.PROVIDER_URL,"t3://localhost:7001");
InitialContext ctx = new InitialContext(h);
AccountHome accountHome = (AccountHome) ctx.lookup("ejb/account");
AccountAccessHome accountAccessHome = (AccountAccessHome) ctx.lookup("ejb/accountaccess");
在WebSphere 5中,java代码中使用EJB部署后的JNDI名称访问local home/remote home的EJB例子代码如下:
InitialContext ctx = new InitialContext();
Context ctx_local = (Context) ctx.lookup("local:");
AccountHome accountHome = (AccountHome) ctx.lookup("ejb/" + "ejb/account");
AccountAccessHome accountAccessHome = (AccountAccessHome) ctx.lookup("ejb/accountaccess");
四、安全
4.1 在Web Application中,用户使用Form方式登录后无权访问资源
如果一个用户经过用户身份验证(J2EE form based authentication)为合法用户,但是该用户试图访问其无权访问的Web资源,WebLogic 6.1将返回给客户该Web应用的登陆页面(比如login.jsp),而WebSphere 5.0将返回一个403的HTTP code给浏览器,在IE浏览器中将显示 "You are not authorized to view this page...",或者"Error 403: AuthorizationFailed"(如果IE没有打开缺省选项"显示友好HTTP错误消息")。 J2EE form-based authentication的例子配置(web.xml片断):
<login-config>
<auth-method>FORM</auth-method>
<realm-name>basicrealm</realm-name>
<form-login-config>
<form-login-page>/login.jsp</form-login-page>
<form-error-page>/failedlogin.jsp</form-error-page>
</form-login-config>
</login-config>
解决办法:
在WEB-INF\web.xml文件中加入error-page指令,如果出现403错误,那么WebSphere将告诉浏览器出现403错误,同时返回/failedlogin.jsp页面给浏览器。
<error-page>
<error-code>403</error-code>
<location>/failedlogin.jsp</location>
</error-page>
需要注意的是:IE缺省配置中打开了"显示友好HTTP错误消息",如果web.xml文件中eror-page指定的页面的内容小于500个字节,那么IE将忽略服务器返回的页面,而"显示友好HTTP错误消息",即显示"You are not authorized to view this page..."。
4.2 HttpServletRequest.getRemoteUser()
在WebLogic 6.1中,用户使用J2EE Form方式登陆以后,可以在任何servlet/jsp当中使用HttpServletRequest对象的getRemoteUser()方法获得登录用户的ID;而在WebSphere 5.0中只能在受保护的资源(servlet/jsp)当中使用这个Servlet API获得登录用户的ID,而且要求被授权访问该资源的安全性角色(J2EE Role)在WebSphere中不能被映射为"每个用户"(everyone)。注:在WebSphere 5中,每个安全性角色可以被映射为"每个用户"(everyone), "所有已认证的用户"(all authenticated), "映射的用户"(mapped users),"映射的组" (mapped groups)。
4.3 Webloigic LDAP Realm的移植
WebLogic提供了Realm API,java程序可以调用该API进行用户的管理、组的管理。 SPC LDAP Realm API具有和WebLogic Realm相似的API代码,在调用weblogic.security.acl.CachingRealm的java代码中,把中weblogic.security替换为spc即可。
if(WebLogic) {
weblogic.security.acl.CachingRealm realm = (weblogic.security.acl.CachingRealm)
weblogic.security.acl.Security.getRealm();
weblogic.security.acl.User u = realm.newUser(clientId,password,null);
realm.getGroup("AdminGroup").addMember(u);
int ret = weblogic.servlet.security.ServletAuthentication.weak
(clientId,password,session);
if (ret != weblogic.servlet.security.ServletAuthentication.AUTHENTICATED){
System.out.print("Login failed!");
}
}
else {
spc.acl.CachingRealm realm = (spc.acl.CachingRealm)
spc.acl.Security.getRealm();
spc.acl.User u = realm.newUser(clientId,password,null);
realm.getGroup("HauiGroup").addMember(u);
try {
LoginContext lc =
new LoginContext("WSLogin",
newcom.ibm.websphere.security.auth.callback.WSCallbackHandlerImpl(clientId,
"realm", password));
lc.login();
lc.logout();
} catch (LoginException le) {
System.out.print("Login failed!");
return;
//throw new InvalidClientIdException(le.toString());
}
}
spc.acl.LdapRealm程序中一些参数最好在以后的版本当中修改为从配置文件中读取。
//LDAP服务器的IP或者HOSTNAME
String LDAPSERVER = "192.168.1.30";
//搜索用户的开始节点
String USER_DN_BASE = "cn=users,dc=xxx,dc=com";
String USER_DN_BASE = "dc=xxx,dc=com";
//搜索组的开始节点
String GROUP_DN_BASE = "cn=groups,dc=xxx,dc=com";
String GROUP_DN_BASE = "dc=xxx,dc=com";
// com.ibm.jndi.LDAPCtxFactory = IBM SecureWay
//C:\Program Files\IBM\LDAP\java\ibmjndi.jar
// com.sun.jndi.ldap.LdapCtxFactory = Any Version 3 compliant LDAP
String FACTORY_INITIAL = "com.ibm.jndi.LDAPCtxFactory";
//登陆目录服务器的有管理员权限的用户名称
String SECURITY_PRINCIPAL = "cn=root";
//登陆目录服务器的有管理员权限的用户口令
String CREDENTIALS = "root";
4.4 将servlet中使用WebLogic Security API的代码改为WebSphere Security API。
注意:WebLogic API weblogic.servlet.security.ServletAuthentication.weak是让浏览器用户登陆到WebLogic服务器上, 而com.ibm.websphere.security.auth.callback.WSCallbackHandlerImpl/LoginContext.login是让用户在java程序(包含servlet)中登陆到服务器上。在下面的代码中,WebSphere API只是起到了验证增加的用户是否能够登陆到WebSphere应用服务器上。
4.5 WebSphere 5 User Registry
WebLogic/WebSphere都支持将LDAP服务器中的用户和J2EE角色之间的绑定,都支持定制用户注册表,比如两者在产品中都提供了基于文件的用户注册表。
WebLogic提供了RDBMS Realm的example实现,该实现采用数据库的两个表来保存用户和组的信息,表的定义如下:
CREATE TABLE aclentries (A_NAME varchar(255), A_PRINCIPAL varchar(255), A_PERMISSION varchar(255));
CREATE TABLE groupmembers(GM_GROUP varchar(255), GM_MEMBER varchar(255));
WebSphere 5 Security redbook中提供了基于RDBMS User Registry的实现,表的定义如下:
create table users(username varchar2(250), password varchar2(250), description varchar2(250), userid varchar(250));
create table groups(name varchar2(250), description varchar2(250), gid varchar2(250));
create table uidgid(gid varchar2(250), userid varchar2(250));
附件中的WebSphere RDBMS User Registry在两方面进行了修改:
支持JDBC数据库连接池。WebSphere Security/User
Registry是在WebSphere其他服务启动之前启动的,因此在WebSphere数据库连接池/名字服务器启动之前,User Registry不能使用数据库连接池,但之后就可以使用数据库连接池了。
将SQL语句作为static常量定义,便于修改数据库的表设计。
这个RDBMS User Registry在部署的时候需要配置四个参数:
DBDRIVER:比如COM.ibm.db2.jdbc.app.DB2Driver
DBURL:比如java:db2:sample
DBUSERNAME:比如db2admin
DBPASSWORD:比如db2admin
JDBCJNDINAME:比如jdbc/sample