EJB(Enterprise JavaBean)是JavaEE平台中用于开发和部署企业级分布式业务组件的技术。Sun公司对EJB的定义是用于开发和部署多层结构、分布式、面向对象的Java应用系统的跨平台构件体系结构,在JavaEE体系中,EJB也是构成企业级应用程序的核心规范。本系列笔记我们将基于最新的JavaEE8、EJB3.2和Wildfly14应用服务器对EJB技术进行介绍。
EJB目前不是一个流行的技术,这有一定的历史原因。在早期的EJB2.0时代,EJB因其过度复杂和低效的设计而饱受诟病,很多采用JavaEE技术栈的项目被它推入深渊,Spring及其生态的快速崛起也让标准JavaEE(主要指EJB部分)错过了一个历史发展机遇,在那一时期大部分用户都迁移到了Spring生态中,尽管EJB3.0大幅简化了开发模型,但历史是有惯性的,即使EJB是“JavaEE标准”的,且无论EJB如何向Spring学习、设计的如何美好,在未来可预见的一段时间里,都不会有项目再采用EJB这个“冷门”技术了。
不过无论如何,作为标准JavaEE规范中的核心部分,我们对EJB进行学习和了解也是有意义的,通过和Spring的对比,我们可以更深入的理解两种技术体系的相似点和区别,在实际的开发中我们可以取长补短,编写符合最佳实践的代码。
EJB规范将一般的业务抽象成了可被调用的“组件”,这种组件被称为企业级JavaBean(Enterprise JavaBean,即EJB)。
EJB容器:EJB需要部署在EJB容器内,EJB容器负责管理EJB的生命周期以及协助EJB对外提供服务。通常来说EJB容器是标准JavaEE应用服务器(例如Wildfly)的一个模块,因此EJB通常都部署在JavaEE应用服务器上。
EJB客户端:EJB是提供服务的业务组件,EJB可以被其它模块通过本地方法或远程(RMI)调用,而调用这些业务组件的就是EJB的“客户端”,它们可能是同样位于JavaEE应用服务器上的Servlet、JSF托管Bean组件,或是位于客户机的Swing程序等。通常JavaEE应用服务器会基于JNDI暴露EJB的调用路径,我们可以在“客户端”代码中通过JNDI调用本地或远程EJB。此外EJB本身也是CDI托管的,在Servlet、JSF或是其它EJB内我们可以通过依赖注入的形式注入EJB,这需要用到@EJB
注解。
会话Bean(Session Bean):会话Bean是最常见的EJB形式,会话Bean接受客户端的本地或远程调用并返回结果。会话Bean的种类包括无状态会话Bean(Stateless Session Bean)、有状态会话Bean(Stateful Session Bean)和单例会话Bean(Singleton Session Bean)。
消息驱动Bean(Message-Driven Bean,MDB):消息驱动Bean可以简单理解为一个JMS消费者组件,JMS中的消息可以触发消息驱动Bean内业务逻辑的异步执行。
通过上面这些概念,我们应该大致能理解EJB是个什么东西了。我们都知道企业级应用中的三层架构,它将一个工程划分为了表现层、业务逻辑层和持久层。举例来说,假如我们使用标准JavaEE开发一个银行系统,这个程序的Web部分(表现层)可以使用Servlet/JSP实现,业务逻辑层将使用EJB实现,而持久层可以选择JPA或JDBC等技术实现。对于一个转账功能,用户输入的数据是来源账户、目标账户和转账金额(为了保证操作幂等性通常还有个交易流水号),表现层负责展示操作页面并校验请求数据,业务逻辑层负责具体扣减来源账户并将金额加入目标账户,此外还需要把这一系列操作包裹在数据库事务中,这些就是所谓的业务逻辑,这种业务逻辑层组件就是EJB在一个JavaEE应用程序中的定位。
在JavaEE6版本之前,EJB通常需要单独打包到EJB JAR文件中并部署到JavaEE应用服务器,EJB JAR还可以和Web应用程序的WAR包共同构建为EAR包(Enterprise Application Archive)并整体部署到JavaEE应用服务器。在JavaEE6后EJB的部署进一步简化了,现在WAR包其实可以直接包含EJB,这意味着Servlet、JSP、EJB等可以放在一个模块中,只需要构建一个WAR包即可,这在小型项目中简化了代码模块的拆分和管理工作。
不过我们这里还是以比较经典的EAR作为例子,我们将使用Maven构建系统搭建一个简单的多模块EAR例子工程并部署到Wildfly14应用服务器上。工程的Maven模块划分如下。
netstore
|_ netstore-api # 接口模块,包含EJB接口和接口依赖的DTO类
|_ pom.xml
|_ netstore-ejb # EJB实现类模块
|_ pom.xml
|_ netstore-web # JavaWeb模块,包含Servlet、JSP等内容
|_ pom.xml
|_ netstore-ear # EAR构建模块,不包含代码,仅用于将EJB JAR和WAR打包为EAR
|_ pom.xml
|_ pom.xml
模块划分乍一看可能有点复杂,其中netstore-api
是一个基础模块,它包含了EJB接口类和传输数据的DTO类,netstore-ejb
和netstore-web
都依赖于它,该模块最终构建为一个JAR文件;netstore-ejb
包含具体的EJB实现代码,它最终构建为EJB JAR文件;netstore-web
是包含Servlet、JSP等内容的Web模块,它最终构建为WAR包;netstore-ear
中没有Java代码,它用于合并EJB JAR和Web项目的WAR,构建EAR包。
工程的根pom.xml
内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<modules>
<module>netstore-web</module>
<module>netstore-ejb</module>
<module>netstore-api</module>
<module>netstore-ear</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-ejb</artifactId>
<version>1.0-SNAPSHOT</version>
<type>ejb</type>
</dependency>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-web</artifactId>
<version>1.0-SNAPSHOT</version>
<type>war</type>
</dependency>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-ear</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
HelloService.java
package com.gacfox.netstore.api;
import javax.ejb.Local;
@Local
public interface HelloService {
String hello();
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>netstore-api</artifactId>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
netstore-api
模块包含了一个接口类,它使用@Local
标注该接口是一个本地的EJB接口。
HelloServiceImpl.java
package com.gacfox.netstore.ejb;
import com.gacfox.netstore.api.HelloService;
import javax.ejb.Stateless;
@Stateless
public class HelloServiceImpl implements HelloService {
@Override
public String hello() {
return "Hello, EJB!";
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>netstore-ejb</artifactId>
<packaging>ejb</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
EJB模块中的Java代码包含了EJB实现类,其中@Stateless
注解标注该类是无状态会话Bean,这个类实现了netstore-api
模块中定义的接口类。
netstore-ejb
模块依赖netstore-api
模块,不过这里注意这个依赖需要标注为<scope>provided</scope>
,这是因为后面EAR包构建中我们会把netstore-api
模块构建生成的JAR放在EAR包的统一依赖目录下,这里构建EJB JAR内如果重复包含了netstore-api
模块JAR,可能导致接口类被JavaEE应用服务器的Module类加载器重复加载,出现明明是同一个Java接口类但却被JVM认为是不同类的奇怪错误。
HelloServlet.java
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);
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>netstore-web</artifactId>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
netstore-web
模块中我们编写了一个Servlet类,它使用@EJB
注入了EJB类,这个注入是JavaEE应用服务器帮我们完成的。Servlet代码中,我们调用了EJB并将结果输出到响应流中。netstore-web
模块中还包含了webapp/WEB-INF/web.xml
描述文件,这里就不列出了。有关JavaWeb工程的内容可以参考Servlet相关章节。
netstore-web
模块也依赖netstore-api
,不过它也需要标注为<scope>provided</scope>
,理由和之前相同。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>netstore-ear</artifactId>
<packaging>ear</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-api</artifactId>
</dependency>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-ejb</artifactId>
<type>ejb</type>
</dependency>
<dependency>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-web</artifactId>
<type>war</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ear-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<version>8</version>
<defaultLibBundleDir>lib</defaultLibBundleDir>
<modules>
<ejbModule>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-ejb</artifactId>
<bundleDir>/</bundleDir>
</ejbModule>
<webModule>
<groupId>com.gacfox.netstore</groupId>
<artifactId>netstore-web</artifactId>
<contextRoot>/</contextRoot>
</webModule>
</modules>
</configuration>
</plugin>
</plugins>
</build>
</project>
netstore-ear
模块中我们使用了maven-ear-plugin
这个插件,它用于将依赖的EJB JAR、WAR等资源合并构建为EAR包。EAR包有特定的包结构,该插件可以帮我们自动按照EAR包结构组装部署包,以及填写EAR包的描述配置文件等操作。
<version>
:该配置指定EAR包使用JavaEE 8版本<defaultLibBundleDir>
:该配置指定了EAR包的JAR依赖放置的目录,这样配置后netstore-api
构建的JAR就会放在EAR包内/lib
下<modules>
:该配置指定了EAR包内的部署模块,我们这里有EJB和Web两个模块按照如上配置,最终构建的EAR包内部大致结构如下(EAR包本身也是ZIP格式的压缩包,可以用7z等压缩软件查看内部结构)。
netstore-ear-1.0-SNAPSHOT.ear
|_ lib
|_ com.gacfox.netstore-netstore-api-1.0-SNAPSHOT.jar
|_ META-INF
|_ application.xml
|_ MANIFEST.MF
|_ com.gacfox.netstore-netstore-ejb-1.0-SNAPSHOT.jar
|_ com.gacfox.netstore-netstore-web-1.0-SNAPSHOT.war
执行Maven命令构建项目。
mvn clean package
执行完成后,我们可以在netstore-ear
模块的target
目录下找到netstore-ear-1.0-SNAPSHOT.ear
文件,将其复制到Wildfly14应用服务器的部署路径下即可。以Wildfly14单实例(standalone)模式启动为例,我们可以将EAR包复制到wildfly-14.0.1.Final\standalone\deployments
文件夹内。
执行以下命令即可启动Wildfly应用服务器。
standalone.sh -c standalone-full.xml
启动成功后,我们即可使用浏览器访问http://localhost:8080/hello
查看Servlet的输出结果了。