会话Bean

EJB中最常用的组件就是会话Bean(Session Bean)。会话Bean是实现了业务逻辑的Java类,会话Bean分为三种,无状态会话Bean(Stateless Session Bean)、有状态会话Bean(Stateful Session Bean)和单例会话Bean(Singleton Session Bean)。这篇笔记我们将对EJB中会话Bean的使用以及这3种会话Bean的概念和用法进行介绍。

注:本篇笔记将基于第一节中搭建的EAR工程编写代码,如果你采用其它的工程结构,代码可能稍有不同(如JNDI名等),不过思路是类似的。

无状态会话Bean(Stateless Session Bean)

无状态会话Bean使用@Stateless注解标注,顾名思义其中包含的代码一般是无状态的业务逻辑。一个无状态会话Bean的生命周期通常伴随着EJB客户端的请求和响应被EJB容器自动创建和“销毁”(这里销毁加了引号,是因为Bean使用完后不一定是真正的被销毁,一般是返回对象池)。

package com.gacfox.netstore.api;

import javax.ejb.Local;

@Local
public interface HelloService {
    String hello();
}
package com.gacfox.netstore.ejb;

import com.gacfox.netstore.api.HelloService;
import org.jboss.logging.Logger;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Stateless;

@Stateless
public class HelloServiceImpl implements HelloService {
    private static final Logger logger = Logger.getLogger(HelloServiceImpl.class);
    @Override
    public String hello() {
        return "Hello, EJB!";
    }

    @PostConstruct
    public void init() {
        logger.info("HelloServiceImpl initialized");
    }

    @PreDestroy
    public void destroy() {
        logger.info("HelloServiceImpl destroyed");
    }
}

代码中,我们的无状态会话Bean对外暴露的接口提供了hello()方法,此外我们还使用@PostConstruct@PreDestroy编写了在该Bean创建和销毁时的代码逻辑。

前面我们提到过,会话Bean都是由EJB容器管理的,无状态会话Bean会在首次客户端请求时被创建,也就是说我们第一次调用该Bean时init()方法才会被回调,当无状态会话Bean的业务逻辑处理完成后,Bean通常会返回对象池,因此我们可能观测不到destroy(),只有当EJB容器决定销毁池中过多的Bean时(可能是内存资源紧张等情况)才可能观察到destroy()方法的回调。另外一点是如果你只是简单的用浏览器测试调用无状态会话Bean,你很可能会看到init()只被调用了一次,这并不是说无状态会话Bean是单例的,它只是EJB容器使用对象池优化资源调度表现出的假象,如果你使用JMeter等对无状态会话Bean的调用进行压力测试,你大概就可以看到init()的多次调用了。

在Servlet中,我们可以使用@EJB注解注入无状态会话Bean,标注该注解后,JavaEE应用服务器会自动查找JNDI并注入合适的EJB资源。

package com.gacfox.netstore.web.servlet;

import com.gacfox.netstore.api.HelloService;

import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "HelloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    @EJB
    private HelloService helloService;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String result = helloService.hello();
        resp.getWriter().write(result);
    }
}

@EJB注解实际上也是基于JNDI注入的,容器启动时,我们可能看到类似如下的日志信息,这些都是HelloService这个EJB的JNDI名。如果我们不使用@EJB注解,使用合适的JNDI名字进行注入也是可以的。Wildfly中,EJB的JNDI名格式为java:<作用域>/<EAR工程名>/<模块名>/<Bean名>!<接口全限定名>,在同一个模块中我们可以基于module作用域的JNDI注入,在同一个EAR工程中我们可以基于app作用域注入,global则代表全局作用域,可以在同一个应用服务器内跨工程和模块注入。

java:global/netstore-ear-1.0-SNAPSHOT/com.gacfox.netstore-netstore-ejb-1.0-SNAPSHOT/HelloServiceImpl!com.gacfox.netstore.apiHelloService
java:app/com.gacfox.netstore-netstore-ejb-1.0-SNAPSHOT/HelloServiceImpl!com.gacfox.netstore.api.HelloService
java:module/HelloServiceImpl!com.gacfox.netstore.api.HelloService
ejb:netstore-ear-1.0-SNAPSHOT/com.gacfox.netstore-netstore-ejb-1.0-SNAPSHOT/HelloServiceImpl!com.gacfox.netstore.apiHelloService
java:global/netstore-ear-1.0-SNAPSHOT/com.gacfox.netstore-netstore-ejb-1.0-SNAPSHOT/HelloServiceImpl
java:app/com.gacfox.netstore-netstore-ejb-1.0-SNAPSHOT/HelloServiceImpl
java:module/HelloServiceImpl

单例会话Bean(Singleton Session Bean)

对于单例会话Bean,EJB容器将仅对其初始化一次,所有客户端访问单例会话Bean时,调用的都是同一个实例。下面例子中,我们使用单例会话Bean实现了一个对地区信息的缓存功能。

package com.gacfox.netstore.api;

import com.gacfox.netstore.api.model.District;

import javax.ejb.Local;
import java.util.List;

@Local
public interface CachedDistrictService {
    List<District> getDistrictList();
}
package com.gacfox.netstore.ejb;

import com.gacfox.netstore.api.CachedDistrictService;
import com.gacfox.netstore.api.model.District;

import javax.annotation.PostConstruct;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;
import java.util.ArrayList;
import java.util.List;

@Singleton
@Lock(LockType.READ)
public class CachedDistrictServiceImpl implements CachedDistrictService {
    private final List<District> districtList = new ArrayList<>();

    @PostConstruct
    public void init() {
        // ... 假设这里从数据库加载了数据
        District d1 = new District("美国", "US");
        District d2 = new District("中国", "CN");
        districtList.add(d1);
        districtList.add(d2);
    }

    @Override
    public List<District> getDistrictList() {
        return districtList;
    }
}

在Web模块中,我们使用Servlet调用了这个单例会话Bean,并在JSP中渲染了一个表格页面。

package com.gacfox.netstore.web.servlet;

import com.gacfox.netstore.api.CachedDistrictService;
import com.gacfox.netstore.api.model.District;

import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "DistrictServlet", urlPatterns = "/district")
public class DistrictServlet extends HttpServlet {
    @EJB
    private CachedDistrictService cachedDistrictService;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<District> districtList = cachedDistrictService.getDistrictList();
        req.setAttribute("districtList", districtList);
        req.getRequestDispatcher("/WEB-INF/district.jsp").forward(req, resp);
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>地区表</title>
</head>
<body>
<table style="border: 1px solid #333;">
    <tr>
        <th>地区名称</th>
        <th>地区编码</th>
    </tr>
    <c:forEach items="${districtList}" var="item">
        <tr>
            <td>${item.name}</td>
            <td>${item.code}</td>
        </tr>
    </c:forEach>
</table>
</body>
</html>

单例会话Bean中,我们还使用了@Lock(LockType.READ)注解,前面我们说过所有客户端都使用同一个单例会话Bean实例,因此单例会话Bean需要对并发访问的线程安全问题进行处理,@Lock注解可以放置在类或方法上,也可以同时使用,其中LockType取值READWRITE表示该类或方法逻辑采用共享锁或排他锁控制。实际上,EJB容器提供了两种并发控制方式,这两种控制模式可以使用@ConcurrencyManagement注解配置。

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class CachedDistrictServiceImpl implements CachedDistrictService { }

容器管理并发(CMC):默认选项,根据@Lock注解的配置,由EJB容器控制单例会话Bean内部的某段逻辑使用共享锁还是排他锁

Bean管理并发(BMC):这种配置下我们需要自己使用代码控制单例会话Bean的并发代码逻辑

此外,单例会话Bean默认会在用户首次访问时由EJB容器创建,有时我们希望在EJB容器启动时就立即创建,这可以使用@Startup注解实现。

@Singleton
@Startup
public class CachedDistrictServiceImpl implements CachedDistrictService { }

在某些业务逻辑中,我们可能需要一个单例会话Bean必须在另一个单例会话Bean创建后才能创建,这可以使用@DependsOn注解,其中注解的value参数是依赖EJB的Bean名,它是字符串数组类型,因此也可以指定多个。

@Singleton
@DependsOn("AnotherService")
public class CachedDistrictServiceImpl implements CachedDistrictService { }

有状态会话Bean(Stateful Session Bean)

有状态会话Bean保持了额外的会话状态数据,这使得它的生命周期和无状态会话Bean、单例会话Bean完全不同。简单来说,当客户端调用有状态会话Bean时,一个新的有状态会话Bean就会被创建,如果客户端保持了这个有状态会话Bean的引用,那么客户端的请求将一直由同一个有状态会话Bean处理,会话数据可以安全的保存在有状态会话Bean中。

对于有状态会话Bean,还有钝化活化的概念。当客户端未调用有状态会话Bean超过指定时间后,EJB容器将钝化有状态会话Bean,此时有状态会话Bean可能被从内存中卸载,序列化并保存到磁盘或其它位置上。如果客户端访问了钝化的有状态会话Bean,它则会再次被活化,恢复到内存中。如果客户端依然没有访问钝化的有状态会话Bean,超过一定期限后,EJB容器将彻底销毁该有状态会话Bean。具有钝化和活化特性也意味着有状态会话Bean必须是可序列化的。

package com.gacfox.netstore.api;

import com.gacfox.netstore.api.model.CartItem;

import javax.ejb.Local;
import java.util.List;

@Local
public interface CartService {
    void addCartItem(CartItem cartItem);

    List<CartItem> getCartItems();
}
package com.gacfox.netstore.ejb;

import com.gacfox.netstore.api.CartService;
import com.gacfox.netstore.api.model.CartItem;
import org.jboss.logging.Logger;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.PostActivate;
import javax.ejb.PrePassivate;
import javax.ejb.Stateful;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Stateful
public class CartServiceImpl implements CartService, Serializable {
    private static final Logger logger = Logger.getLogger(HelloServiceImpl.class);

    private final List<CartItem> cartItemList = new ArrayList<>();

    @Override
    public void addCartItem(CartItem cartItem) {
        cartItemList.add(cartItem);
    }

    @Override
    public List<CartItem> getCartItems() {
        return cartItemList;
    }

    @PostConstruct
    public void init() {
        logger.info("CartServiceImpl initialized");
    }

    @PreDestroy
    public void destroy() {
        logger.info("CartServiceImpl destroyed");
    }

    @PrePassivate
    public void prePassivate() {
        logger.info("CartServiceImpl prePassivate");
    }

    @PostActivate
    public void postActivate() {
        logger.info("CartServiceImpl postActivate");
    }
}

代码中,CartService是一个有状态会话Bean,它使用@Stateful注解标注,实现了购物车功能。@PostConstruct@PreDestroy@PrePassivate@PostActivate会分别在有状态会话Bean创建、销毁、钝化和激活时期被回调。

下面代码包含了Servlet和JSP实现的购物车界面,它展示了一个可以录入信息的表单和展示购物车内容的表格。Servlet中,调用有状态会话Bean的例子如下。

package com.gacfox.netstore.web.servlet;

import com.gacfox.netstore.api.CartService;
import com.gacfox.netstore.api.model.CartItem;

import javax.enterprise.inject.spi.CDI;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "CartServlet", urlPatterns = "/cart")
public class CartServlet extends HttpServlet {
    private CartService retrieveSessionCartService(HttpSession httpSession) {
        CartService cartService = (CartService) httpSession.getAttribute("cartService");
        if (cartService == null) {
            InitialContext initialContext = new InitialContext();
            cartService = (CartService) initialContext.lookup("java:global/netstore-ear-1.0-SNAPSHOT/com.gacfox.netstore-netstore-ejb-1.0-SNAPSHOT/CartServiceImpl!com.gacfox.netstore.api.CartService");
            httpSession.setAttribute("cartService", cartService);
        }
        return cartService;
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession httpSession = req.getSession();
        CartService cartService = retrieveSessionCartService(httpSession);
        List<CartItem> cartItemList = cartService.getCartItems();
        req.setAttribute("cartItemList", cartItemList);
        req.getRequestDispatcher("/WEB-INF/cart.jsp").forward(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession httpSession = req.getSession();
        CartService cartService = retrieveSessionCartService(httpSession);
        String productName = req.getParameter("productName");
        String quantity = req.getParameter("quantity");
        cartService.addCartItem(new CartItem(productName, Integer.parseInt(quantity)));
        resp.sendRedirect(req.getContextPath() + "/cart");
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>购物车</title>
</head>
<body>
<fieldset>
    <form action="${pageContext.request.contextPath}/cart" method="post" enctype="application/x-www-form-urlencoded">
        <label for="productName">商品名称:</label>
        <input type="text" id="productName" name="productName">
        <label for="quantity">数量:</label>
        <input type="number" id="quantity" name="quantity">
        <input type="submit" value="加入购物车">
    </form>
</fieldset>
<table style="border: 1px solid #333;">
    <tr>
        <th>商品名称</th>
        <th>数量</th>
    </tr>
    <c:forEach items="${cartItemList}" var="item">
        <tr>
            <td>${item.productName}</td>
            <td>${item.quantity}</td>
        </tr>
    </c:forEach>
</table>
</body>
</html>

这里其实我们并没有使用@EJB注解注入有状态会话Bean,而是使用Java代码手动基于JNDI获取了它,这是因为我们需要将有状态会话Bean的生命周期和Web的HttpSession生命周期关联到一起,前面我们说过会话Bean都是由EJB容器管理的,对于有状态会话Bean客户端需要在会话内保持其引用,Servlet中我们可以将这个引用存放在HttpSession中,用户浏览器请求Servlet时,我们可以查看HttpSession中是否存储了有状态会话Bean的引用,如果有则还是用这个已有的引用,如果没有再重新获取,这样我们就可以保证用户浏览器的一次会话内,使用的是同一个有状态会话Bean实例了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。