之前写过使用IDEA创建EJB工程的文章,不过现在有个课题需要结合 JPA + EJB + JSF, 虽然可以按照前文所述方法进行,但是我想使用 Maven 进行管理,因此直接在新建工程时选择 EJB 项目就不行了,应该选择 Maven 项目。
你可以先看看之前的文章:使用 IDEA 创建 EJB 工程:
分析
首先,我们分析一下项目所需结构。
JPA 主要是用于 ORM 了,因此都是实体类,相当于 Model 层。
EJB 则分为接口和实现类,接口应该复制给客户端(JSF Web 层)调用,而实现类则是 EJB 的 Bean, 相当于 DAO 层。
最后是 JSF Web模块,页面展示 View 层。
因此得到如下依赖关系:
EJB -> EJB-API -> JPA
Web -> EJB-API -> JPA
这么分模块是为了使用 Maven 管理时更好地处理模块之间的依赖关系。
新建各模块
下一步我们就可以新建一个 Maven 父项目。
给他配置全局的依赖,如 slf4j 和 logback, Java EE API 等。
配置内容:
<dependencies>
<!-- 日志 -->
<dependency>
<groupid>org.slf4j</groupid>
<artifactid>slf4j-api</artifactid>
<version>1.7.21</version>
</dependency>
<dependency>
<groupid>ch.qos.logback</groupid>
<artifactid>logback-core</artifactid>
<version>1.1.7</version>
</dependency>
<dependency>
<groupid>ch.qos.logback</groupid>
<artifactid>logback-classic</artifactid>
<version>1.1.7</version>
</dependency>
<!--JavaEE API,注意是 provided,因为 JBoss 作为 JavaEE 服务器实现了其 API-->
<dependency>
<groupid>javax</groupid>
<artifactid>javaee-api</artifactid>
<version>7.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupid>org.apache.maven.plugins</groupid>
<artifactid>maven-compiler-plugin</artifactid>
<version>3.3</version>
<!-- 指定源码级别,防止每次编辑 pom 文件后 IDEA 都会认为源码级别为 1.5 -->
<configuration>
<source />1.8
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
JPA
之后按照依赖顺序新建 JPA 模块。注意需要配置打包类型为 jar(默认是 pom )以供 EJB 模块和 web 模块使用。
<!-- 打包成 jar 给 web--> <packaging>jar</packaging>
EJB-API
然后添加 EJB-api 模块,因为 Web 层只需要 EJB 的接口,不需要其实现类,因此在这里我们把接口和实现分在两个模块中。
这个模块也需要打包成 jar. 该模块需要依赖于 JPA 模块。
<dependencies>
<dependency>
<groupid>com.youthlin.demo</groupid>
<artifactid>Demo-jpa</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
EJB
添加 EJB 模块。依赖中设置其依赖于 api 模块。
<dependencies>
<dependency>
<groupid>com.youthlin.demo</groupid>
<artifactid>Demo-ejb-api</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
Web
最后添加 web 模块。在新建 Module 时可以选择 Create from archetype, 然后选择 maven-archtype-webapp.

注意:可以添加 archetypeCatalog=internal属性以使得创建模块时更快,不至于卡死。

该模块的依赖是:
<dependencies>
<dependency>
<groupid>junit</groupid>
<artifactid>junit</artifactid>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupid>com.youthlin.demo</groupid>
<artifactid>Demo-ejb-api</artifactid>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-entitymanager -->
<dependency>
<groupid>org.hibernate</groupid>
<artifactid>hibernate-entitymanager</artifactid>
<version>5.0.10.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
其中 junit 是创建 web 模块时选择的模板自动添加的,EJB-api 是我们需要手动添加的,api 层已经依赖 jpa 模块了,不需重复指定。
Hibernate-entitymanager 作为 JPA 的规范的实现提供商,当然你也可以选择其他的,比如 Eclipse-Link 之类的。
项目结构
新建后项目结构如下:

演示
然后我们写个最简单的 Hello World 演示一下。
JPA
首先是实体类 User.
package com.youthlin.demo.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;
/**
* Created by lin on 2016-09-14-014.
* User 要通过 EJB 传输的实体类需要实现 Serializable 接口才能序列化
*/
@Entity
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
API
然后是 DAO 接口。
package com.youthlin.demo.dao;
import com.youthlin.demo.model.User;
import javax.ejb.Remote;
import java.util.List;
/**
* Created by lin on 2016-09-14-014.
* user dao
*/
@Remote
public interface UserDao {
User save(User user);
List<User> findAll();
}
当然,使用 Remote 还是 Local 可以你自己选了,这里随便选了个:远程接口。
EJB
再次是 DAO 实现类。
首先我们在这个模块中配置 persisten.xml 和 ejb-jar.xml 两个文件。
<?xml version="1.0" encoding="UTF-8" ?>
<persistence>
<persistence-unit name="demo" transaction-type="JTA">
<!-- 直接用数据源。不用数据源而在 properties 里指定连接的话可以连接但 persist 的对象不会同步到数据库 -->
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<!-- 不需要指定 provider-->
<!--<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>-->
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<!--<property name="hibernate.format_sql" value="true"/>-->
</properties>
</persistence-unit>
</persistence>
上述代码为了演示方便,使用的是 JBoss 默认自带的内存数据库的数据源 ExampleDS (服务器关闭释放内存,则数据库中数据也将消失), 当用于实际项目中时,你需要自行配置 JBoss 数据源,相关操作可自行搜索。
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd"
version="3.1">
<display-name>Demo-ejb</display-name>
</ejb-jar>
该文件用于指明本项目是一个 EJB 项目,需要生成打包文件部署到服务器。
package com.youthlin.demo.dao.impl;
import com.youthlin.demo.dao.UserDao;
import com.youthlin.demo.model.User;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.List;
/**
* Created by lin on 2016-09-14-014.
* user dao Impl
*/
@Stateless
public class UserDaoImpl implements UserDao {
@PersistenceContext(name = "demo")
private EntityManager em;
@Override
public User save(User user) {
em.persist(user);
return user;
}
@Override
public List<User> findAll() {
TypedQuery<User> query = em.createQuery("select u from User as u", User.class);
return query.getResultList();
}
}
这里的 EntityManager 对象 em 使用 @PersistenceContext(name = "demo")注解表明,该对象由容器自动注入。name的值就是 persistence.xml 中配置的 unitName.
Web
最后是 Web 层。根据上面的接口,你也可以看出我打算只写一个功能:添加用户和列出用户。
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
第一部分定义 JSF 拦截器,所有 .xhtml 请求都由 JSF 处理。第二部分说明欢迎文件 index.xhtml.
package com.youthlin.demo.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by lin on 2016-09-02-002.
* 获取远程对象工具类
*/
public class EJBUtil {
private static final Logger log = LoggerFactory.getLogger(EJBUtil.class);
private static ConcurrentHashMap<Class, Object> map = new ConcurrentHashMap<>();
/**
* 获取远程对象, 第一次获取后会缓存起来,之后获取的将是缓存的对象
* 注意:远程接口实现类 <strong> 必须 </strong > 以 < code>Impl</code > 结尾
*
* @param clazz 远程对象的类型
* @return 获取的远程对象
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> clazz) {
return getBean(clazz, false);
}
/**
* 获取远程对象
* 注意:远程接口实现类 <strong> 必须 </strong > 以 < code>Impl</code > 结尾
*
* @param clazz 远程对象的类型
* @param force true 表示强制每次都从远程获取而不使用缓存
* @return 获取的远程对象
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> clazz, boolean force) {
if (!force && map.containsKey(clazz)) {
log.trace(" 直接返回已缓存的对象:{}", clazz);
return (T) map.get(clazz);
}
boolean hasException = false;
Object result = null;
Hashtable<String, String> jndiProperties = new Hashtable<>();
jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
try {
Context context = new InitialContext(jndiProperties);
//这里的moduleName在Artifact里设置,不确定是什么名字的话可以在EJB部署时看到发布的JNDI名称
final String moduleName = "Demo_ejb_EJB";
final String fullName = "ejb:/" + moduleName + "/"
+ clazz.getSimpleName() + "Impl" + "!" + clazz.getName();
log.debug("EJB 全名 =" + fullName);
result = context.lookup(fullName);
log.debug("result={},class={}", result, result.getClass());
return (T) result;
} catch (NamingException e) {
e.printStackTrace();
hasException = true;
} catch (Exception e) {
hasException = true;
} finally {
log.trace(" 远程对象获取完毕 ");
if (!hasException && result != null) {
map.put(clazz, result);
}
}
log.warn(" 获取远程对象失败 ");
return null;
}
}
目前演示 EJB 中只有一个类,若有多个类,那么每次获取远程 EJB 都要写个 Context.lookup 再强制转换是很烦人的,因此我把它提取出来作为一个工具类。不过这里约定了实现类和接口的命名规则:实现类必须以接口名加Impl命名。当然仅仅是为了获取方便(虽然实现类和客户端不应有关联,客户端只和 JNDI 名称有关联,但这里默认 JNDI 名称就是包含类名嘛),你也可以自己自定义啦。
package com.youthlin.demo.action;
import com.youthlin.demo.dao.UserDao;
import com.youthlin.demo.model.User;
import com.youthlin.demo.util.EJBUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import java.util.List;
/**
* Created by lin on 2016-09-14-014.
* UserBean
*/
@ManagedBean
@SessionScoped
public class UserBean {
private final Logger log = LoggerFactory.getLogger(UserBean.class);
private UserDao userDao = EJBUtil.getBean(UserDao.class);
private String name;
public void save() {
User user = new User();
user.setName(name);
userDao.save(user);
log.debug("保存成功");
}
public List<User> getUsers() {
List<User> users = userDao.findAll();
log.debug("users = {}", users);
return users;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
JSF 中的Managed Bean 相当于 Action 层,你可以根据业务需要自行设置 Scope 为会话或请求范围。这里演示得很简单,为了减少获取 EJB 的次数,设置成了会话范围。(不过在 EJBUtil 里我们已经做了缓存了。)
<!DOCTYPE html>
<html lang="zh-CN"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:c="http://java.sun.com/jsp/jstl/core">
<h:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>Demo</title>
</h:head>
<body>
<h:form>
<h:inputText value="#{userBean.name}"/>
<h:commandButton action="#{userBean.save}" value="添加"/>
</h:form>
<ul id="list">
<c:forEach items="#{userBean.users}" var="user">
<li>${user.name}</li>
</c:forEach>
</ul>
</body>
</html>
页面很简单,甚至连 CSS 都没写,因为我是演示的目的。只涉及到了表单、循环,以及 XHTML 和 Bean 之间的关联。
#{bean.property} 就是 JSF 中的表达式写法了,在%lt;c:foreach>里还是用的美元符号 ${xxx}.
配置
写好后,打开 Project Structure 定位到 Modules – Demo-ejb – EJB 发现右边有提示
‘EJB’ Facet resources are not includeed in an artifact.
因此点击 Create Artifact,注意打包类型只能选择 Archived, 且后缀需要以 jar 或 rar 结尾。并且需要加上 ejb 和 jpa 模块的编译内容到 Output 中,否则运行时会找不到类。

同样, web 打包也需要确保 api 和 jpa 模块的代码包含进来了。

最后,如果需要日志打印,这里配置的是 slf4j + logback, 但是 JBoss 的日志会和这个由冲突,还需要另外的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<jboss-deployment-structure>
<ear-subdeployments-isolated>false</ear-subdeployments-isolated>
<deployment>
<!--Exclusions allow you to prevent the server from automatically adding some dependencies-->
<exclusions>
<module name="org.slf4j"/>
<module name="org.slf4j.ext"/>
<module name="org.slf4j.impl"/>
<module name="org.slf4j.jcl-over-slf4j"/>
</exclusions>
</deployment>
</jboss-deployment-structure>
这样才能正常打印日志。
效果
启动服务器,就可以看到如下界面了:

index 页面
添加用户后:

总结
使用 Maven 分层次管理还是比较方便的,需要注意的就是,生成的 Artifact 包需要保护该模块依赖的模块的编译内容,否则将导致 ClassNotFound 异常。另外就是 JBoss 的日志冲突的坑需要略微注意。
你可以在 GitHub 上找到本文的完整代码,直接使用 IEDA 即可打开运行。
感谢阅读。
最后,祝大家中秋节快乐!
声明
- 本作品采用署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。除非特别注明, 霖博客文章均为原创。
- 转载请保留本文(《使用 Maven 通过 IDEA 开发 JPA + EJB + JSF 项目》)链接地址: https://youthlin.com/?p=1300
- 订阅本站:https://youthlin.com/feed/