簡介
盡管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