EJB 简介
原文:http://zetcode.com/java/ejb/
在本教程中,我们学习如何使用 Enterprise JavaBeans。 我们使用 GlassFish,NetBeans,Derby 和 Maven。
Enterprise JavaBean (EJB) 是服务器端组件,封装了应用的业务逻辑。 EJB 在 EJB 容器中运行,该容器负责各种系统级服务,包括事务管理,安全性和并发控制。 EJB 是 Java EE 规范的一部分。
GlassFish 是 Java EE 的参考实现,它包括 Enterprise JavaBeans 容器。 我们将在 GlassFish 中运行示例。 Apache Derby 是完全用 Java 实现的开源关系数据库。 Oracle 以 Java DB 的名义分发相同的二进制文件。
第一个 EJB
我们在 NetBeans 中创建一个新的 Web 应用。 该项目将称为MyFirstEJB
。 从“服务器和设置”页面,选择 GlassFish 服务器,并将上下文更改为myfirstejb
。
图:服务器和设置
在此对话框中,我们选择应用服务器,Java EE 版本和上下文路径。
index.html
<!DOCTYPE html>
<html>
<head>
<title>Test page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<p>Test page</p>
</body>
</html>
这是我们的index.html
页面。 如果我们访问应用的根页面,它将返回。
我们右键单击应用图标,然后选择一个 Session Bean 类型的新 EJB。 我们将 bean 称为MyFirstBean
,键入com.zetcode.ejb
包,然后选择无状态会话类型。
图:在 NetBeans 中创建一个新的会话 Bean
无状态会话 Bean 不维护与客户端的对话状态。 当客户端调用无状态 Bean 的方法时,该 Bean 的实例变量可能包含特定于该客户端的状态,但仅在调用期间包含。 该方法完成后,客户端特定的状态将丢失。
FirstBean.java
package com.zetcode.ejb;
import javax.ejb.Stateless;
@Stateless
public class FirstBean {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String sayHello() {
StringBuilder sb = new StringBuilder("Hello ");
sb.append(this.getName()).append("!");
return sb.toString();
}
}
MyFirstBean
是无状态会话 Bean。 使用@Stateless
装饰创建一个无状态会话 bean。 它具有no-interface view
,其中不使用本地业务接口,并且 Bean 类的所有公共方法都自动向调用者公开。
public String sayHello() {
StringBuilder sb = new StringBuilder("Hello ");
sb.append(this.getName()).append("!");
return sb.toString();
}
MyFirstBean's
的工作是构造对调用者的问候。
接下来,我们通过右键单击项目图标并从 Web 类别中选择 Servlet 文件类型来创建一个新的 Servlet。 我们将 servlet 称为Greet
,然后键入com.zetcode.web
包。 我们将 URL 模式更改为/greet
。
图:创建新的 servlet
在新的 Servlet 对话框中,我们提供 Servlet 名称及其包。
Greet.java
package com.zetcode.web;
import com.zetcode.ejb.FirstBean;
import java.io.IOException;
import java.io.PrintWriter;
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;
@WebServlet(name = "Greet", urlPatterns = {"/greet"})
public class Greet extends HttpServlet {
@EJB
private FirstBean firstBean;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain;charset=UTF-8");
firstBean.setName(request.getParameter("name"));
String msg = firstBean.createMessage();
try (PrintWriter out = response.getWriter()) {
out.println(msg);
}
}
}
Greet
Servlet 从客户端发送的 URL 中读取名称参数,调用 EJB 的createMessage()
业务方法,并以纯文本形式返回响应。
@EJB
private FirstBean firstBean;
@EJB
注解将 EJB 注入 Servlet。
response.setContentType("text/plain;charset=UTF-8");
servelt 响应以 UTF-8 字符集的纯文本格式显示。
firstBean.setName(request.getParameter("name"));
我们从请求中检索name
参数,并将其设置为 EJB。
String msg = firstBean.createMessage();
我们将其称为createMessage()
业务方法。
图:MyFirstEJB 项目结构
我们构建应用并将其部署到 GlassFish 服务器。 要构建应用,请右键单击项目图标,然后选择“构建”。 要部署应用,请右键单击项目图标,然后选择“部署”。
$ curl localhost:8080/myfirstejb/greet?name=Jan
Hello Jan!
使用curl
工具,我们连接到myfirstejb
Web 应用的Greet
servlet,并向其传递参数。 Web 应用以问候语响应。
$ curl localhost:8080/myfirstejb/
<!DOCTYPE html>
<html>
<head>
<title>Test page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<p>Test page</p>
</body>
</html>
访问根页面,应用将返回 HTML 测试页面。
使用实体 bean 持久化数据
在第二个示例中,我们创建一个将读取和保存汽车的 Web 应用。 汽车对象将保存在 Derby 数据库中。
我们使用car-app
名称创建一个新的 Java Web 应用。 然后,我们创建一个新的Car
实体。 实体类文件类型位于持久性类别中。 包将为com.zetcode.persistence
。 主键类型为Long
,并选中了创建持久性单元选项。
在下一页中,我们将持久性单元名称更改为carpu
,然后选择默认的EclipseLink
持久性供应器。 我们选择jdbc/sample
数据源,并选择了Create
表生成策略。 jdbc/sample
数据源是指默认情况下位于用户主目录的.netbeans-derby
子目录中的sample
数据库。
图:持久性供应器
在此视图中,我们提供了持久性单元名称,持久性供应器,数据源和表生成策略。
实体 Bean
实体 Bean 是一种 Enterprise JavaBean,它表示持久存储中存在的业务实体对象。 实体 bean 由主键标识。 与在客户端会话的生存期内生存的会话 Bean 不同,实体 Bean 即使在 EJB 容器崩溃时也可以幸免。
Car.java
package com.zetcode.persistence;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="Cars")
public class Car implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="Id")
private Long id;
@Column(name="Name")
private String name;
@Column(name="Price")
private int price;
public Car() { }
public Car(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
Car
是 EJB 实体 Bean。 它是要存储在 Derby 数据库中的业务对象。
@Entity
@Table(name="Cars")
public class Car implements Serializable {
实体 bean 用@Entity
注解修饰。 该实体映射到Cars
表。
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="Id")
private Long id;
每个实体都有一个唯一的对象标识符。 此唯一标识符或主键使客户端能够定位特定的实体实例。 @Id
声明此实体 bean 的标识符属性,@GeneratedValue
注解用于指定主键的生成方式,@Column
将标识符映射到数据库表的Id
列。
public Car() { }
持久性框架需要无参数的构造器。
EJB
我们创建一个本地无状态ManageCarBean
企业 bean。 这次 EJB 具有本地接口视图。
ManageCarBeanLocal.java
package com.zetcode.ejb;
import javax.ejb.Local;
import com.zetcode.persistence.Car;
@Local
public interface ManageCarBeanLocal {
void saveCar(Car car);
void setPrice(int price);
void setName(String name);
Car getCar(Long id);
}
该接口定义了 EJB 客户端要使用的方法。 客户端只能通过 bean 的业务接口中定义的方法来访问会话 bean。
@Local
public interface ManageCarBeanLocal {
@Local
装饰指定该接口是本地业务接口。
ManageCarBean.java
package com.zetcode.ejb;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import com.zetcode.persistence.Car;
@Stateless
public class ManageCarBean implements ManageCarBeanLocal {
private String name;
private int price;
@PersistenceContext(unitName = "carpu")
private EntityManager em;
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
@Override
public void setPrice(int price) {
this.price = price;
}
@Override
public void saveCar(Car car) {
em.persist(car);
}
@Override
public Car getCar(Long id) {
Car car = em.find(Car.class, id);
return car;
}
}
ManageCarBean
读取并保存汽车对象。
@PersistenceContext(unitName = "carpu")
private EntityManager em;
容器使用persistence.xml
中的信息创建EntityManager
。 @PersistenceContext
注解将实体管理器注入到 Bean 中。 管理器已映射到carpu
持久性单元。 实体管理器用于通过持久性上下文与数据进行交互。
@Override
public void saveCar(Car car) {
em.persist(car);
}
saveCar()
方法将汽车对象保存到数据库中; 在我们的例子中是 Derby。
@Override
public Car getCar(Long id) {
Car car = em.find(Car.class, id);
return car;
}
getCar()
方法通过其 ID 查找汽车。
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="carpu" transaction-type="JTA">
<jta-data-source>jdbc/sample</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="eclipselink.ddl-generation" value="create-tables" />
</properties>
</persistence-unit>
</persistence>
persistence.xml
文件是一个配置文件,其中包含有关我们在应用中使用的数据库的信息。 jdbc/sample
是 GlassFish 服务器随附的内置数据源。 在我们的应用中,我们将数据保存在 Derby 的预先创建的sample
数据库中。 如果eclipselink.ddl-generation
属性不存在,则会自动导致创建Cars
表。
图:NetBeans Derby 工具
我们可以使用 NetBeans Derby 工具来连接和管理数据库中的数据。 该工具位于“服务”窗口中。
servlet
我们创建两个 servlet:SaveCar
和ReadCar
。 我们将它们放入com.zetcode.web
包中。 Servlet 是从 NetBeans Web 类别创建的。 两个 servlet 均以纯文本形式响应。
图:Apache Common Lang JAR
我们还包括用于帮助程序方法的 Apache Common Lang JAR。 可以从项目网站下载该库。
ValidateParameter.java
package com.zetcode.util;
import org.apache.commons.lang3.math.NumberUtils;
public class ValidateParameter {
private static final int MAX_PRICE_CAR = 10_000_000;
public static boolean validateName(String param) {
return !(null == param || "".equals(param) ||
NumberUtils.isNumber(param));
}
public static boolean validateId(String param) {
return !(null == param || "".equals(param) ||
!NumberUtils.isNumber(param));
}
public static boolean validatePrice(String param) {
if (null == param || "".equals(param) || !NumberUtils.isNumber(param)) {
return false;
}
int price = Integer.valueOf(param);
return !(price < 0 || price > MAX_PRICE_CAR);
}
}
ValidateParameter
类用于验证请求参数。 参数不能为null
或为空,并且 ID 和价格值必须为数字。 另外,价格必须在合理范围内。 我们使用 Apache Common Lang 的NumberUtils.isNumber()
检查数字值。
SaveCar.java
package com.zetcode.web;
import java.io.IOException;
import java.io.PrintWriter;
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 com.zetcode.ejb.ManageCarBeanLocal;
import com.zetcode.persistence.Car;
import com.zetcode.util.ValidateParameter;
@WebServlet(name = "SaveCar", urlPatterns = {"/save"})
public class SaveCar extends HttpServlet {
@EJB
private ManageCarBeanLocal manageCarBean;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String sname = request.getParameter("name");
String sprice = request.getParameter("price");
String message;
if (ValidateParameter.validateName(sname)
&& ValidateParameter.validatePrice(sprice)) {
message = String.format("%s car is saved", sname);
int price = Integer.valueOf(sprice);
Car car = new Car(sname, price);
manageCarBean.saveCar(car);
} else {
message = "Wrong parameters";
}
response.setContentType("text/plain;charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
out.println(message);
}
}
}
SaveCar
Servlet 的目的是调用适当的 EJB 来保存汽车。 汽车对象的数据是从客户端发送的 URL 中读取的。
@WebServlet(name = "SaveCar", urlPatterns = {"/save"})
@WebServlet
注解将SaveCar
Servlet 映射到/save
路径。
@EJB
private ManageCarBeanLocal manageCarBean;
ManageCarBeanLocal
企业 bean 被注入到 servlet 中。
String sname = request.getParameter("name");
String sprice = request.getParameter("price");
从请求中读取参数。
if (ValidateParameter.validateName(sname)
&& ValidateParameter.validatePrice(sprice)) {
参数正在验证中。
Car car = new Car(sname, price);
manageCarBean.saveCar(car);
创建一个新的汽车对象并将其保存到数据库。 为了保存汽车对象,我们利用了ManageCarBean
的saveCar()
方法。
ReadCar.java
package com.zetcode.web;
import com.zetcode.ejb.ManageCarBeanLocal;
import java.io.IOException;
import java.io.PrintWriter;
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 com.zetcode.persistence.Car;
import com.zetcode.util.ValidateParameter;
@WebServlet(name = "ReadCar", urlPatterns = {"/read"})
public class ReadCar extends HttpServlet {
@EJB
private ManageCarBeanLocal manageCarBean;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain;charset=UTF-8");
String message;
String pid = request.getParameter("id");
if (ValidateParameter.validateId(pid)) {
Long carId = Long.valueOf(pid);
Car rcar = manageCarBean.getCar(carId);
if (null != rcar) {
message = String.format("Name: %s, Price: %d",
rcar.getName(), rcar.getPrice());
} else {
message = "Cannot find car with this ID";
}
} else {
message = "Wrong parameters";
}
try (PrintWriter out = response.getWriter()) {
out.println(message);
}
}
}
ReadCar
servlet 调用ManageCarBean
企业 bean 从数据库中读取数据; 它根据提供的 ID 选择汽车对象。
String sid = request.getParameter("id");
从 URL 读取 ID。
Long carId = Long.valueOf(pid);
Car rcar = manageCarBean.getCar(carId);
if (null != rcar) {
message = String.format("Name: %s, Price: %d",
rcar.getName(), rcar.getPrice());
} else {
message = "Cannot find car with this ID";
}
ManageCarBean
的readCar()
方法用于检索汽车对象。
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<error-page>
<error-code>404</error-code>
<location>/error404.txt</location>
</error-page>
<error-page>
<location>/error.txt</location>
</error-page>
</web-app>
在web.xml
文件中,我们提供了两个错误页面。 error.txt
是所有错误的默认错误页面,error404.txt
是 404 错误的错误页面,当客户端请求不存在的资源时触发。 注意,error404.txt
必须先于error.txt
。
图:错误页面s
我们将两个文本文件放置在网页中。
error.txt
Error has occurred, check the GlassFish log file
这是一般错误页面。
error404.txt
404 : Page was not found
这是 404 错误的错误页面。
部署方式
是时候部署应用了。 通过右键单击 Web 项目并选择 Deploy 命令来部署应用。 请注意,当我们清理项目时,将取消部署该应用。 如果未运行所选的应用服务器,那么 deploy 命令还将启动它。
$ ./asadmin list-applications
MyFirstEJB <ejb, web>
car-app <ejb, web>
Command list-applications executed successfully.
GlassFish 的asadmin
工具可用于确定当前已部署的应用。
GlassFish 启动时,NetBeans 会自动启动 Derby 服务器。 可以在 GlassFish 服务器设置中将其关闭。
图:GlassFish 服务器属性
在 NetBeans 的“服务”选项卡中,我们展开“服务器”节点,然后选择 GlassFish 属性。 在那里,我们可以看到“启动已注册 Derby 服务器”选项。
$ curl localhost:8080/car-app/doread
404 : Page was not found
如果尝试访问不存在的资源,则会收到自定义 404 错误消息。
$ curl "localhost:8080/car-app/save?name=Volvo&price=29000"
Volvo car is saved
沃尔沃汽车将保存到数据库中。 注意 URL 的双引号的用法。
$ curl "localhost:8080/car-app/save?name=Bentley&price=299000000"
Wrong parameters
验证器发现了不切实际的高汽车价格。
$ curl localhost:8080/car-app/read?id=401
Name: Volvo, Price: 29000
我们从数据库中读取了一辆汽车。
使用 Maven 构建项目
NetBeans 在开发过程中为我们节省了许多繁琐的工作。 但是使用 Maven 手动构建项目是有益的。
mvn archetype:generate -DgroupId=com.zetcode -DartifactId=car-app
-DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false
我们创建一个新的 Maven 项目。 我们使用maven-archetype-webapp
类型。
$ tree
.
└── car-app
├── pom.xml
└── src
└── main
├── resources
└── webapp
├── index.jsp
└── WEB-INF
└── web.xml
6 directories, 3 files
tree
命令向我们显示了创建的项目结构。
$ mkdir -p src/main/java/com/zetcode/ejb
$ mkdir src/main/java/com/zetcode/persistence
$ mkdir src/main/java/com/zetcode/web
$ mkdir src/main/java/com/zetcode/util
$ mkdir src/main/resources/META-INF
我们创建目录。
复制应用源文件后,项目结构如下所示:
$ tree
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── zetcode
│ ├── ejb
│ │ ├── ManageCarBean.java
│ │ └── ManageCarBeanLocal.java
│ ├── persistence
│ │ └── Car.java
│ ├── util
│ │ └── ValidateParameter.java
│ └── web
│ ├── ReadCar.java
│ └── SaveCar.java
├── resources
│ └── META-INF
│ └── persistence.xml
└── webapp
├── index.jsp
└── WEB-INF
└── web.xml
不要忘记复制web.xml
文件。 Maven 为我们创建了一个空的web.xml
文件。
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zetcode</groupId>
<artifactId>car-app</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>car-app Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<finalName>car-app</finalName>
</build>
</project>
pom.xml
是一个文件,其中包含有关项目的信息以及 Maven 用于构建项目的配置详细信息。 我们提供了 Eclipse 链接持久性供应器,Apache Commons lang 库和 Java EE Web API 的依赖项。 我们指示 Maven 使用 Java8。最终的构建文件称为car-app.war
,位于target
子目录中。
$ mvn package
该项目是使用mvn package
命令构建的。
$ ./asadmin start-domain
从 GlassFish 的bin
目录中,启动 GlassFish 服务器。
$ ./asadmin start-database --db-home ~/.netbeans-derby
Derby 服务器已启动。 NetBeans 在.netbeans-derby
目录中创建 Derby 系统主目录,该目录位于用户的主目录中。 在这里,我们可以找到我们之前使用过的sample
数据库。
$ ~/bin/glassfish-4.1.1/glassfish/bin/asadmin deploy target/car-app.war
我们部署应用。 我们看到警告语Table/View 'CARS' already exists in Schema 'APP'
。 这是eclipselink.ddl-generation
属性的结果,该属性设置为create_tables
。 我们可以忽略警告。
$ curl localhost:8080/car-app/read?id=401
Name: Volvo, Price: 29000
我们检索先前创建的汽车。
在本教程中,我们为一些简单的业务逻辑创建了 Enterprise JavaBeans。 我们已使用 NetBeans,Derby 和 Maven 作为示例。 ZetCode 具有以下相关教程: Derby 教程, Java 教程和 Stripes 教程。