分类
代码

使用 Maven 通过 IDEA 开发 JPA + EJB + JSF 项目

之前写过使用IDEA创建EJB工程的文章,不过现在有个课题需要结合 JPA + EJB + JSF, 虽然可以按照前文所述方法进行,但是我想使用 Maven 进行管理,因此直接在新建工程时选择 EJB 项目就不行了,应该选择 Maven 项目。
你可以先看看之前的文章:使用 IDEA 创建 EJB 工程

使用 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.

新建web模块
新建web模块

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

新建web模块:属性设置
新建web模块:属性设置

该模块的依赖是:

<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, 且后缀需要以 jarrar 结尾。并且需要加上 ejb 和 jpa 模块的编译内容到 Output 中,否则运行时会找不到类。

EJB 打包注意
EJB 打包注意

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

web 模块打包
web 模块打包

最后,如果需要日志打印,这里配置的是 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 页面

index 页面

添加用户后:

添加用户后
添加用户后

总结

使用 Maven 分层次管理还是比较方便的,需要注意的就是,生成的 Artifact 包需要保护该模块依赖的模块的编译内容,否则将导致 ClassNotFound 异常。另外就是 JBoss 的日志冲突的坑需要略微注意。
你可以在 GitHub 上找到本文的完整代码,直接使用 IEDA 即可打开运行。

感谢阅读。
最后,祝大家中秋节快乐!


发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

[/鼓掌] [/难过] [/调皮] [/白眼] [/疑问] [/流泪] [/流汗] [/撇嘴] [/抠鼻] [/惊讶] [/微笑] [/得意] [/大兵] [/坏笑] [/呲牙] [/吓到] [/可爱] [/发怒] [/发呆] [/偷笑] [/亲亲]