内容


为 J2EE 应用程序构建分布式对象管理框架

感知范围的系统可以简化开发并防止内存泄漏

Comments

为了提高 Java™ 应用程序的性能,许多开发人员都会开发自己的对象管理解决方案来替代默认的垃圾收集器。一种常用的解决方案是对象池(object pooling)。创建 Java 对象是一种开销很大的操作;如果只创建对象一次并多次重复使用它们,就会减少开销。这对于服务器端应用程序尤其有意义,因为通常会重复执行相同的服务器代码。对于在单一 Java 虚拟机(JVM)中运行的应用程序,这样的解决方案通常很容易实现。但是,在多层的体系结构中,要跨多台机器和多个 JVM 管理对象实例,这会非常复杂。

在本文中,我将介绍一个在多层 J2EE 体系结构中管理 Java 对象的框架。这个框架基于 Servlet、JSP、EJB、JMS 和 Struts 技术定义的标准接口。因此,它并不依赖于任何特定厂商的解决方案。当不再需要对象实例时,其他许多对象管理解决方案要求应用程序显式地释放它们;但是这个框架并非如此,它使用范围的概念自动地释放对象实例。在禁用默认的垃圾收集器时内存泄漏是一个严重的问题,这个特性会显著降低内存泄漏的风险。总之,这个框架为改进分布式 J2EE 环境中的性能提供了一个实用且易用的解决方案。

对象创建和池

Java 对象创建和垃圾收集都是开销很大的操作。频繁的对象创建是导致 Java 应用程序性能问题的主要原因之一。这个问题在 servlet 等服务器端程序中影响更严重。在方法中常常有许多短期存在的本地变量,但是方法本身会被频繁地反复调用。因此,会频繁地创建对象,然后通过垃圾收集销毁它们。这种 object churning 现象会严重损害性能。

对象池 是这个问题的一种解决方案。其思想是创建一个对象集合(通常包含相同类型的对象)并把它们存储在一个池中。如果需要一个对象,就从池中获取它,而不需要重新创建它。当这个对象完成它的工作之后,它被返回给池,可供以后重新使用。对象池需要跟踪每个对象的状态,而且应该是线程安全的。可以找到许多 Java 对象池的实现,比如 Apache 的 Common Pool(参见 参考资料)。如果不考虑实现细节,对象池一般采用清单 1 所示的接口。

清单 1. 对象池 API
public Object getObject(Class clazz);
public void returnObject(Object obj);

getObject() 方法从池中获取给定类的一个实例。returnObject() 方法把这个对象释放回池中。

对象池在大多数情况下都可以提高性能。但是,它并非没有代价。首先,当不再需要对象时,开发人员必须显式地调用 returnObject(object)。这完全背离了自动垃圾收集机制的目标,而垃圾收集是 Java 语言的重要特性之一(可能是最重要的特性)。第二,应用程序有出现内存泄漏的风险。如果不把对象显式地返回池中,对象就会一直留着。这两个问题使开发人员不太敢采用对象池技术。

如果 Java 开发人员可以享受对象池的性能优势,同时不必为显式释放对象操心,那就好了。幸运的是,这在大多数服务器端 J2EE 应用程序中是可行的。本文讨论的框架会提供一个实用的解决方案。

一个基于范围(scope)的解决方案

我们看看一个包含 servlet、JSP 页面和 EJB 组件的服务器端应用程序。图 1 给出一种典型的设计,它使用 MVC 和 Façade 模式。

图 1. 服务器端应用程序
服务器端应用程序
服务器端应用程序

在收到请求时,servlet 调用会话 bean 上的一个方法,这个方法进而调用实体 bean 上的一个方法。在会话调用返回之后,servlet 把请求转发给 JSP 页面。在整个流程中,按照 图 1 所示的方式创建对象。不同的对象有不同的寿命。一些对象只在一个请求的处理周期中存在;请求完成之后,就不再需要这些对象了。其他对象的寿命可能更长:例如,它们可能在一个 HTTP 会话甚至整个应用程序范围内存在。如果标识出每个对象的范围,就可以指定在这个范围结束时自动释放对象。例如,图中的 object1object4 在请求范围内存在。请求完成之后,可以自动释放这两个对象。object2 在事务范围内存在。在事务终止之后,可以自动释放它。object3 在应用程序范围内存在,因此在应用程序的整个生命周期内都存活。

为了实现这种效果,要解决两个问题:首先,修改对象池,让它理解范围的概念。第二,建立一种自动通知机制,从而在范围结束时发出通知。

ObjectManager API

在本文中,我将用对象管理器(object manager) 这个词表示这个框架,以便区别于传统的对象池。与对象池一样,对象管理器也负责从池中获取对象和把对象返回池中。另外,对象管理器还理解范围的概念。清单 2 给出对象管理器的 API。

清单 2. ObjectManager API
public interface ObjectManager {
    /**
     * Retrieve an object instance of the give class from the object pool 
     * for the given scope, identified by a scope type and a scope key.
     *
     * @param clazz         the class
     * @param scopeType     the type of the scope
     * @param scopeKey      the key to identify the scope
     *
     * @return  an object instance retrieved from the object pool
     */
    public Object getObject(Class clazz, int scopeType, Object scopeKey);

    /**
     * Release an object back to the object pool.
     *
     * @param object   the object to be released
     */
    public void releaseObject(Object object);

    /**
     * Release all objects of the given scope, identified by a scope type 
     * and a scope key.
     * @param scopeType     the type of the scope that objects bound to
     * @param scopeKey      the key to identify the scope
     */
    public void releaseAll(int scopeType, Object scopeKey);

}

为了从对象池获取对象,需要提供一个范围类型和一个范围键。对象管理器把范围信息与获取的每个对象关联起来。在调用 releaseAll() 方法时,对象管理器可以识别出与给定的范围相关联的对象,并把它们释放回池中。

范围类型

大多数服务器端 J2EE 应用程序中有六种常用的范围类型:

  • Transaction(事务)
  • Request(请求)
  • HTTP session(HTTP 会话)
  • Application(应用程序)
  • Global(全局)
  • None(无)

开发人员可以根据应用程序的需要增加其他范围类型。清单 3 定义本文中讨论的范围类型。

清单 3. 范围类型定义
 public class ScopeTypes {

    public final static int TRANSACTION =   	1; 
    public final static int REQUEST =       	2; 
    public final static int HTTP_SESSION =       	3; 
    public final static int APPLICATION =  	4; 
    public final static int GLOBAL =        		5; 
    public final static int NONE =          		6; 

}

事务范围

事务范围覆盖一个事务的整个生命周期。这个范围开始于一个事务的开始。这时会创建一个惟一的范围键。这个范围结束于提交或回滚事务时。这时,与事务范围相关联的所有对象被自动释放回它们的池。

请求范围

请求范围与一个 servlet 请求的范围对应;在容器调用 servlet 来处理请求之后,请求范围立即开始。同时会创建一个惟一的范围键。在 servlet 完成处理之前请求范围结束。这时,与这个范围相关联的所有对象被自动释放回它们的池。

HTTP 会话范围

HTTP 会话范围与一个 HTTP 会话的生命周期对应。它从创建一个新的 HttpSession 时开始。这时会创建一个惟一的范围键。它结束于会话被销毁或过期时。这时,与这个范围相关联的所有对象被自动释放回它们的池。

应用程序范围

应用程序范围覆盖应用程序的整个生命周期。它开始于把一个应用程序部署到应用服务器时。这时会创建一个惟一的范围键。这个范围结束于应用程序停止运行或从应用服务器中删除时。这时,与这个范围相关联的所有对象被自动释放回它们的池。

全局范围

全局范围是最大的范围。采用这种范围的对象不会被释放。

无范围

无范围用于不使用对象池的对象。采用这种范围的对象每次都通过自己的对象构造函数来创建,并由 Java 垃圾收集器释放。对象管理器根本不管理它们。

范围键和事件通知

为了使用 前一节 中定义的对象管理器和标识范围,需要使用范围键。在本节中,讨论如何创建范围键以及如何存储它们。让对象管理器理解范围的概念,就可以解决本文开头提到的一个问题。另一个问题是如何根据对象的范围自动释放对象。为了让开发人员不必显式地释放对象,需要在范围结束时通知对象管理器,让它能够正确地释放对象。servlet 和 EJB 容器提供了处理范围变化所需的事件触发机制。我们来看看如何使用事件触发机制处理各种类型的范围。

事务范围

根据 EJB 2.0 规范,实现 javax.ejb.SessionSynchronization 的会话 bean 类必须实现 afterBegin()beforeCompletion()afterCompletion()。在事务开始之后,EJB 容器马上调用 afterBegin();在事务完成之前,它会调用 beforeCompletion()。在事务完成或回滚之后,EJB 容器马上调用 afterCompletion()

  • 使用 afterBegin() 为事务范围创建惟一的范围键。把范围键存储在会话 bean 中,供以后获取。
  • 使用 afterCompletion() 通知对象管理器事务范围已经结束,应该释放对应的对象。

请求范围

根据所用技术的不同,可以以不同方式为请求范围生成事件。本节讨论目前最流行的四种技术。

Servlet 2.4. Java Servlet 规范的 2.4 版增加了与请求相关的事件添加了一个监听器。javax.servlet.ServletRequestListener 包含 requestInitialized() 方法和 requestDestroyed() 方法。

  • requestInitialized() —— 当一个 servlet 请求即将进入范围时,容器调用这个方法。使用它为请求范围创建惟一的范围键。把范围键存储在请求对象本身中,供以后获取。
  • requestDestroyed() —— 当一个 servlet 请求即将离开范围时,容器调用这个方法。使用它通知对象管理器请求范围即将结束,应该释放对应的对象。

Servlet 2.3 或更老的版本。Java Servlet 规范的 2.3 版没有为与请求相关的事件提供任何监听功能。克服这个缺陷的最好方法是,在一个扩展自 javax.servlet.http.HttpServlet 的超类中创建范围键并实现通知逻辑。然后,就可以从这个超类派生所有应用程序 servlet。应该在这个超类的 service() 方法中创建范围键并实现通知逻辑。

Struts 1.0。与 Servlet 2.3 一样,在 Struts 1.0 中应该在一个扩展自 org.apache.struts.action.ActionServlet 的超类中创建范围键并实现通知逻辑。可以使用 process() 方法创建范围键并实现通知逻辑。

Struts 1.1。从 Struts 1.1 开始,在每次从容器接收 servlet 请求时 ActionServlet 执行的处理逻辑中,添加了 org.apache.struts.action.RequestProcessor。使用 RequestProcessor.process() 方法创建范围键并实现通知逻辑。

HTTP 会话范围

根据 Servlet 2.3 规范,实现 javax.servlet.http.HttpSessionListener 的监听器需要实现 sessionCreated()sessionDestroyed()

  • sessionCreated() —— 在创建新会话之后,容器调用这个方法。使用它为会话范围创建惟一的范围键。把范围键存储在会话对象本身中,供以后获取。
  • sessionDestroyed() —— 在会话失效或过期之后,容器调用这个方法。使用它通知对象管理器会话范围已经结束(或即将结束),应该释放对应的对象。(在 Servlet 2.4 规范中,当会话即将失效时,还会调用另一个方法;这个更新并不影响这个框架)。

应用程序范围

根据 Servlet 2.3 规范,实现 javax.Servlet.ServletContextListener 的监听器需要实现 contextInitialized()contextDestroyed()

  • contextInitialized() —— 当应用程序完成初始化并准备提供服务时,容器调用这个方法。使用它为应用程序范围创建惟一的范围键。把范围键存储在 servlet 上下文对象本身中,供以后获取。
  • contextDestroyed() —— 当应用程序即将停止运行时,容器调用这个方法。使用它通知对象管理器应用程序范围已经结束,应该释放对应的对象。

全局范围

这个范围不需要事件,因为与它相关联的对象不会被释放。

无范围

这个范围不需要事件,因为与它相关联的对象由垃圾收集器释放,而不是由对象管理器负责。

示例

在本节中,我通过一个示例进一步解释前面介绍的概念。示例代码演示如何用对象管理器在一个 Struts 1.1 应用程序中管理请求范围。

清单 4 所示的 MyRequestProcessor 拦截每个 Struts 动作请求,在处理每个请求之前创建请求范围键,在处理请求之后释放对象。

清单 4. RequestProcessor 类的实现
public class MyRequestProcessor extends RequestProcessor{
   public void 
          process(HttpServletRequest request, HttpServletResponse response) 
      throws ServletException {

      // before each request is processed, create a request key
      String requestScopeKey = UniqueIDGenerator.GetID();
      request.setAttribute("requestScopeKey ", requestScopeKey);

      try {

         // process the request
         super.process(request, response);
	
      } finally {

         // after the request is processed, release all objects 
         // bound to this request scope
         ObjectManager objMgr = ObjectManagerFactory.GetInstance();
         objMgr.releaseAll(ScopeTypes.REQUEST, requestScopeKey);
      }
   }

}

在清单 5 中,AnyAction 代表任何 Struts 动作;它演示如何从对象管理器获取对象。

清单 5. 动作类的实现
public class AnyAction extends Action {
   public ActionForward execute(ActionMapping mapping,
               ActionForm form,
               HttpServletRequest request,
               HttpServletResponse response) {

         // ...

         String requestScopeKey = request.getAttribute("requestScopeKey");
         ObjectManager objMgr = ObjectManagerFactory.GetInstance();

         obj = objMgr.getObject(obj.getClass(), ScopeTypes.REQUEST,
requestScopeKey);

         // ...
   }

}

只要范围处于同一个组件或容器中,前面讨论的所有概念就都是有效的。在一般情况下,这个条件是成立的。例如,请求范围的开始和结束都在一个 servlet 容器中。事务范围的开始和结束都在一个 EJB 容器中。如果这种设计适合您的需要,您就不需要阅读本文余下的内容了。但是,在许多情况下,范围可能跨多个组件。例如,假设一个 Struts 动作中总是调用一个 EJB 会话 bean 方法。这个会话 bean 方法中的一些对象可能属于一个 HTTP 请求。也就是说,HTTP 请求开始于一个 Struts 动作,然后延续到一个 EJB 会话 bean。在下面几节中,讨论这种分布式环境中的问题。

分布式对象管理器

典型的 J2EE 应用程序可能有多个组件,这些组件在不同的 JVM 甚至不同的物理机器上运行。在集群环境中,一个逻辑组件可能由多个物理节点组成,每个节点在一个 JVM 上运行。在这种分布式计算环境中(见图 2),每个 JVM 至少有一个对象管理器。

图 2. 分布式计算环境
图 2
图 2

不同的对象管理器可能管理属于同一范围的对象。例如,图 2 中的 object1object2object3object4 都属于同一个请求范围。在这个请求范围结束之后,它们各自的对象管理器必须释放它们。因此,通知机制的工作范围不能只限于一个 JVM,必须跨多个 JVM。可以使用 Java Messaging Service(JMS)实现这个目标。

使用 JMS 进行通信

如果必须将一个事件同时通知多个对象管理器,那么使用发布-订阅模型是最合适的。JMS 支持发布-订阅模型,它使用主题进行消息广播。为了使用 JMS,需要把每个对象管理器与一个 JMS 监听器关联起来。如图 3 所示,当一个范围结束时,一个消息被发布到一个主题。这个消息包含一个范围类型和一个范围键。然后,所有订阅这个主题的 JMS 监听器会检查这个消息,并要求它们的对象管理器释放与这个范围相关联的对象。

图 3. 使用 JMS 协调对象管理
图 3
图 3

为了更详细地了解这种机制的工作方式,我们看看示例代码。清单 6 演示如何广播包含范围信息的消息。

清单 6. 广播消息的实现示例
// ---------------------------------------------------
// a scope is ended at this point
// 	the scope type is stored in scopeType
// 	the scope key is stored in scoreKey
// 	ctx is a JNDI context


TopicConnectionFactory tcf = (TopicConnectionFactory) 
	ctx.lookup("ObjectManagerJMSConnection");
TopicConnection tc = tcf.createTopicConnection();
tc.start();
TopicSession ts = tc.createTopicSession(false, 1);
Topic t = (Topic) ctx.lookup("ObjectManagerJMSTopic");
TopicPublisher tp = ts.createPublisher(t);

TextMessage tm = ts.createTextMessage();
tm.setText(scopeType + ":" + scopeKey);
tp.publish(tm);

清单 6 给出发布 JMS 消息的标准方法。TopicConnectionFactoryTopic 的名称应该反映 JMS 服务器中设置的名称。消息是一个包含范围信息的文本消息。

清单 7 演示如何实现与对象管理器相关联的 JMS 监听器。

清单 7. MessageListener 类实现
public class ObjectManagerListener implements MessageListener {

   private Context ctx = null;
   private TopicConnection tcon = null;
   private TopicSession tsession = null;
   private TopicSubscriber tsubscriber = null;

   /**
    * Constructor.
    * 
    * @param ctx	a JNDI context
    * @throws NamingException
    */
   public ObjectManagerListener(Context ctx) throws NamingException{
      this.ctx = ctx;
   }


   /**
    * Starts the listener.
    *
    * @throws Exception
    */
   public void start() throws Exception {
      TopicConnectionFactory tconFactory = 
         (TopicConnectionFactory) ctx.lookup("ObjectManagerJMSConnection");
      tcon = tconFactory.createTopicConnection();
      tsession = tcon.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
      Topic topic = (Topic) ctx.lookup("ObjectManagerJMSTopic");
      tsubscriber = tsession.createSubscriber(topic);
      tsubscriber.setMessageListener(this);
      tcon.start();
   }

   /**
    * Stops the listener.
    *
    * @throws Exception
    */
   public void stop() throws Exception {
      this.close();
   }

   /**
    * Upon receiving a message, the listener asks its object manager
    * to release objects in the scope specified in the message.
    * 
    * @param msg	A message containing a scope type and a scope key.
    */
   public void onMessage(Message msg){
      try {
         String msgText;
         if (msg instanceof TextMessage) {
            msgText = ((TextMessage)msg).getText();
         } else {
            msgText = msg.toString();
      }

         StringTokenizer st = new StringTokenizer(msgText, ":");
         if (st.countTokens() == 2) {
            String scopeType = st.nextToken();
            String scopeKey = st.nextToken();
            ObjectManager objMgr = ObjectManagerFactory.GetInstance();
            objMgr.releaseAll(scopeType, scopeKey);
      }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }


	public void close() throws Exception {
		tsubscriber.close();
		tsession.close();
		tcon.stop();
		tcon.close();
		ctx = null;
	}

}

ObjectManagerListener 是一个 JMS 监听器。启动之后,它连接到 JMS 服务器并监听主题。注意,在 start() 方法中,TopicConnectionFactoryTopic 的名称应该反映 JMS 服务器中设置的名称。在收到文本消息时,如果消息的格式是正确的,ObjectManagerListener 就从消息中提取出范围类型和范围键,并要求对象管理器释放与这个范围相关联的所有对象。

用来传递范围信息的上下文

在分布式环境中,范围跨多个组件。因此,在一个组件中创建的范围键必须被传递给其他组件,让这些组件可以使用键获取与这个范围相关联的对象。跨多个组件传递范围键的惟一方法是通过方法调用。为了帮助实现这种方式,需要用一个上下文对象存储范围信息。对于每个请求,创建一个上下文对象。这个对象存储当前请求范围、HTTP 会话范围和应用程序范围的键。然后,通过方法调用把这个对象传递给其他组件。当一个事务开始时,这个事务的范围键被添加到这个上下文对象中。当事务结束时,删除对应的范围键。清单 8 定义这个对象的 API。

清单 8. RequestContext API
public interface RequestContext {
   /**
    * Get the scope key for the given scope type.
    * 
    * @param scopeType
    * @return
    */
   public Object getScopeKey(int scopeType);

   /**
    * Set the scope key for the given scope type.
    * 
    * @param scopeType
    * @param scopeKey
    */
   public void setScopeKey(int scopeType, Object scopeKey);
}

因为需要跨组件传递上下文对象,所以这个对象应该是可序列化的。

修改后的示例

现在可以用 JMS 和 RequestContext 修改前面的示例。除了创建请求范围键之外,在处理请求之前,MyRequestProcessor 需要获得应用程序范围键和会话范围键,并把它们存储在 RequestContext 对象中。在处理请求之后,它发送一个 JMS 消息,表示这个请求范围已经结束。清单 9 演示实现方法。

清单 9. 修改后的 RequestProcessor 类实现
public class MyRequestProcessor extends RequestProcessor{
   public void 
          process(HttpServletRequest request, HttpServletResponse response) 
      throws IOException, ServletException {

      // assume when application started, the application id was generated 
      // and stored in ServletContext with attribute name " appScopeKey"
      String appScopeKey = 
                request.getServletContext().getAttribute("appScopeKey");

      // session scope key 
      String sessionScopeKey = request.getSession().getId();

      // request scope key
      String requestScopeKey = UniqueIDGenerator.GetID();

      RequestContext ctx = RequestContextFactory.getInstance();
      ctx.setScopeKey(ScopeType.APPLICATION, appScopeKey);
      ctx.setScopeKey(ScopeType.HTTP_SESSION, sessionScopeKey);
      ctx.setScopeKey(ScopeType.REQUEST, requestScopeKey);

      // store the request context in request
      request.setAttribute("requestContext", ctx);

      try {

         // process the request
         super.process(request, response);

      } finally {

         // after the request is processed, release all objects bound 
         // to this request scope
         TopicConnectionFactory tcf = (TopicConnectionFactory) 
         ctx.lookup("ObjectManagerJMSConnection");
         TopicConnection tc = tcf.createTopicConnection();
         tc.start();
         TopicSession ts = tc.createTopicSession(false, 1);
         Topic t = (Topic) ctx.lookup("ObjectManagerJMSTopic");
         TopicPublisher tp = ts.createPublisher(t);

         TextMessage tm = ts.createTextMessage();
         tm.setText(ScopeTypes.REQUEST + ":" + requestScopeKey);
         tp.publish(tm);

      }
   }

}

修改后的 AnyAction 类(清单 10)演示如何从 RequestContext 对象获取请求范围键并把 RequestContext 对象传递给 EJB 组件。

清单 10. 修改后的 Action 类实现
public class AnyAction extends Action{
   public ActionForward execute(ActionMapping mapping,
   ActionForm form,
      HttpServletRequest request,
      HttpServletResponse response) {

         // ...

         RequestContext ctx = 
              (RequestContext) request.getAttribute("requestContext");

         // get the request scope key from the context object
         String scopeKey = ctx.getScopeKey(ScopeType.REQUEST);
         ObjectManager objMgr = ObjectManagerFactory.GetInstance();
         obj = 
              objMgr.getObject(obj.getClass(), ScopeTypes.REQUEST, scopeKey);

         // call methodY of sessionBeanX and pass the context object 
         // as an parameter
        sessionBeanX.methodY(params, ctx);

         // ...
   }
}

清单 10 中的示例代码并不像 清单 5 那样从 HttpServletRequest 获取范围键,而是从 HttpServletRequest 获取 RequestContext 对象。然后,从 RequestContext 对象获取范围键。在调用 EJB 对象的远程或本地接口时,通过参数传递 RequestContext 对象。

我还添加了 SessionBeanX(清单 11),演示如何在 EJB 组件中使用 RequestContext 对象。

清单 11. SessionBean 类实现
public class SessionBeanX implements SessionBean{

   public void methodY (Object params, RequestContext ctx) {

      // ...

      // get the request scope key from the context object
      String scopeKey = ctx.getScopeKey(ScopeType.REQUEST);
      ObjectManager objMgr = ObjectManagerFactory.GetInstance();
      obj = objMgr.getObject(obj.getClass(), ScopeTypes.REQUEST, scopeKey);

      // ...

   }

}

为了获取范围信息,SessionBeanX 类的每个方法必须有 RequestContext 参数。如 清单 10 所示,当 EJB 客户机调用远程或本地接口时,它必须传递 RequestContext 对象,这个对象包含关于当前范围的信息。然后,bean 方法中的 RequestContext 对象可以获得当前范围的键 —— 例如,通过 methodY()

结束语

本文介绍了一个用于在 J2EE 应用程序中管理对象实例的框架。这个框架的主要优点是,它使用在 J2EE 中定义的事件触发机制自动地释放对象实例。这不仅使开发人员不必在代码中显式地释放对象,还可以减少内存泄漏的风险。本文的前半部分针对服务器端代码在单一 JVM 中运行的系统。后半部分针对服务器端代码在多个 JVM 中运行的环境。在本文中,我通过示例代码解释了概念。性能基准测试超出了本文的范围,将在以后的文章中讨论。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Java technology
ArticleID=376105
ArticleTitle=为 J2EE 应用程序构建分布式对象管理框架
publish-date=03162009