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