会话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
取值READ
或WRITE
表示该类或方法逻辑采用共享锁或排他锁控制。实际上,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实例了。