如果你想开发一个应用(1-11)

这一章开始的时候,先拿一个广告图镇楼:

图是网上随便找的,哈哈好希望真的有路虎

这句广告此很有意思,虽然脚踏实地的走路是最踏实的(jdbc),如果可以,当然有辆自行车(JdbcTemplate)就更好了.但我相信,一辆能装载,速度快,安全性高的路虎,是每个人心中的梦想。

路虎

我们想要这样一些能力:

  • 对象可以和数据库字段自动进行映射
  • 自动生成sql语句
  • 自动完成查询条件
  • 自动生成级联关系
  • 自动管理数据库缓存和延迟加载等

这些能力可以使我们从无休止的?中解脱出来,那么有没有这样一种既简单,又方便的工具呢?Spring集成的JPA功能登场了。

JPA(Java Persistence API)Java持久性API,是用于对象/关系映射(ORM)的Java API,其中Java对象映射到数据库工件,以便在java应用程序中管理数据关系。JPA包括Java持久性查询语言(JPQL),Java持久性标准API以及用于定义对象/关系映射元数据的Java API和XML模式。

需要再次强调一下,JPA不是orm,他仅仅是一套API标准。

Spring2开始集成了JPA功能,就像有一辆车之前需要驾照,使用JPA之前同样需要引入JPA所依赖的包:

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jpa</artifactId>
  <version>2.0.2.RELEASE</version>
</dependency>

然后我们就可以去4S点去试驾,或者选车,出去飞了。

试驾

引入JPA的依赖包之后开始对JPA进行配置,而配置JPA的第一步就是要配置实体管理工厂的Bean,以获取实体管理器,在JPA中定义了两种实体管理工厂:

  • 应用程序管理类型:程序向管理器工厂直接请求时,会创建一个管理器,适合不在JavaEE容器中的应用程序,需配置persistence.xml文件
  • 容器管理类型:应用程序不和管理器工厂打交道,它的创建由容器负责。适合运行在容器中的程序,可不需要配置persistence.xml文件

我们的程序即在JavaEE容器中运行,有极力的想要全java配置,所以当然选择容器管理类型了,在Spring中使用LocalContainerEntityManagerFactoryBean的FactoryBean来配置实体管理器:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                   JpaVendorAdapter jpaVendorAdapter){
    LocalContainerEntityManagerFactoryBean lcemf=new LocalContainerEntityManagerFactoryBean();
    lcemf.setDataSource(dataSource);
    lcemf.setJpaVendorAdapter(jpaVendorAdapter);
    lcemf.setPackagesToScan("com.niufennan.jtodos.models");
    return lcemf;
}

注意这个Bean需要两个参数,分别为数据源和Jpa实现适配器,然后分别set到对象里,并且通过’setPackagesToScan’方法设置默认扫描的实体包。

在这个bean的参数里,数据源即上一章设置的数据源,这里不在叙述,而JpaVendorAdapter是针对JPA不同的实现,目前JPA的实现有很多种,主要有Hibernate,OpenJpa,EclipseJpa等,对于Spring-jpa的用户来说,使用哪种实现在代码上都无所谓,因为已经在容器中透明了,这里我选择了EclipseLinkJPA的实现,首先还是引入依赖:

<dependency>
  <groupId>org.eclipse.persistence</groupId>
  <artifactId>org.eclipse.persistence.jpa</artifactId>
  <version>2.7.0</version>
</dependency>

然后增加jpaVendorAdapter的Bean:

@Bean
public JpaVendorAdapter jpaVendorAdapter(){
    EclipseLinkJpaVendorAdapter adapter=new EclipseLinkJpaVendorAdapter();
    adapter.setDatabase(Database.MYSQL);  //1
    adapter.setShowSql(true);              //2    
    adapter.setGenerateDdl(false);          //3    
    adapter.setDatabasePlatform(MySQLPlatform.class.getName());                                  //4
    return adapter;
}

1 设置访问的数据库类型
2 设置在日志中输出生成的SQL
3 设置是否根据数据实体生成修改数据库结构,这里不修改
4 设置sql方言

然后,根据JPA实际的需求,我们还需要对实体类进行一些改造,这里以Todo类为例,改造方式如下:

  1. 增加JPA所需的一些注解
  2. 将基本数据类型换成包装类形式

改造完后代码如下:

@Entity(name = "todos")
public class Todo {
    @Id
    private Integer id;
    private String item;
    private Date createTime=new Date();
    private Integer userId;
    get... set...
}

现在挑选完成,准备起飞。

##低配版##

为了和上一章的dao类区分,我们新创建一个persistence包,用来存放基于JPA实现的持久层类,首先,创建一个TodoRepository类,并在里定义三个方法,即将TodoDao接口的方法拷贝入内:

public interface TodoRepository {
    public List<Todo> getAll();
    public List<Todo> getTodoByUserId(int userId);
    public void save(Todo todo);
}

然后统一创建impl,作为接口的实现,这里创建一个基于jpa实现的类:

public class JpaTodoRepository implements TodoRepository {
    public List<Todo> getAll() {
        return null;
    }
    public List<Todo> getTodoByUserId(int userId) {
        return null;
    }
    public void save(Todo todo) {

    }
}

下面完成这个类:

@Transactional
@Repository
public class JpaTodoRepository implements TodoRepository {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;
    public List<Todo> getAll() {
        CriteriaQuery<Todo> criteriaQuery=entityManagerFactory.createEntityManager().getCriteriaBuilder().createQuery(Todo.class);
        return entityManagerFactory.createEntityManager().createQuery(criteriaQuery).getResultList();
    }

    public List<Todo> getTodoByUserId(int userId) {
        CriteriaBuilder builder=entityManagerFactory.createEntityManager().getCriteriaBuilder();
        CriteriaQuery<Todo> criteriaQuery=builder.createQuery(Todo.class);
        Root<Todo> todoRoot = criteriaQuery.from(Todo.class);
        Predicate predicate = builder.equal(todoRoot.get("userId"), userId);
        criteriaQuery.where(predicate);
        return entityManagerFactory.createEntityManager().createQuery(criteriaQuery).getResultList();
    }

    public void save(Todo todo) {
        entityManagerFactory.createEntityManager().persist(todo);
    }
}

我知道你想说什么,看上去代码好复杂,尤其是条件查询的部分,这里先对条件查询进行一下说明:

CriteriaBuilder builder=entityManagerFactory.createEntityManager().getCriteriaBuilder();  //基于建造模式,构建一个Criteria构建器对象(基于Criteria模式进行条件查询)

CriteriaQuery<Todo> criteriaQuery=builder.createQuery(Todo.class);  //为Todo对象创建一个基础查询

Root<Todo> todoRoot = criteriaQuery.from(Todo.class); //为基础查询设置一个查询条件列表

Predicate predicate = builder.equal(todoRoot.get("userId"), userId);  //通过userId进行查询

criteriaQuery.where(predicate);

return entityManagerFactory.createEntityManager().createQuery(criteriaQuery).getResultList();  //设置 查询条件并返回

其余的代码很简单就不在叙述,接下来使用土土的测试方式,运行一下,阿啊哦,报错了,查看一下报错信息(复制其中的一句):

Failed to load class "org.slf4j.impl.StaticLoggerBinder".

这是因为EclipseLink默认使用了slf4j的API记录日志,所以之类需要添加对它的引用即可:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.25</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j-impl</artifactId>
  <version>2.9.1</version>
</dependency>

然后土土的跑起来,测试一下,啊哦,还是有错误,查看一下报错信息:

16:55:30.136 [RMI TCP Connection(5)-127.0.0.1] ERROR org.springframework.web.context.ContextLoader - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [com/niufennan/jtodos/config/DataBaseConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Cannot apply class transformer without LoadTimeWeaver specified

提示对LoadTimeWeaver的调用失败,那么LoadTimeWeaver又是做什么用的呢?LoadTimeWeaver顾名思义,就是使用AspectJ提供在Aop中类加载时织入切片的能力。

那么如何使用LoadTimeWeaver呢?首先,需要通过JVM的-javaagent参数设置LTW的织入器类包,以代理JVM默认的类加载器;第二,LTW织入器需要一个 aop.xml文件,在该文件中指定切面类和需要进行切面织入的目标类。简单说,就是提供动态代理的能力。我们可以使用注解:

@EnableLoadTimeWeaving( aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.DISABLED)

对他进行关闭。

这时候运行,ok 成功出现了我们需要的页面。

但这样显然不是什么好主意,因为Spring现在就是基于注解在使用的,而基于注解,肯定会不可避免的使用到动态代理的织入,所以,将LTW禁用显然是不合理的。所以,最简单的方法是,既然entityManagerFactory需要,那么给它就好了,修改entityManagerFactory的Bean:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                   JpaVendorAdapter jpaVendorAdapter){
    LocalContainerEntityManagerFactoryBean lcemf=new LocalContainerEntityManagerFactoryBean();
    lcemf.setDataSource(dataSource);
    lcemf.setJpaVendorAdapter(jpaVendorAdapter);
    lcemf.setPackagesToScan("com.niufennan.jtodos.models");
    lcemf.setLoadTimeWeaver(new InstrumentationLoadTimeWeaver());
    return lcemf;
}

最后将LoadTimeWeaver给set进去,在运行一下,还是报错,查看一下报错信息:

Must start with Java agent to use InstrumentationLoadTimeWeaver

难道一定要修改java的启动参数么?当然不是,进入源码看一看(此源码为在Idea环境下直接双击进入):

public void addTransformer(ClassFileTransformer transformer) {
    Assert.notNull(transformer, "Transformer must not be null");
    InstrumentationLoadTimeWeaver.FilteringClassFileTransformer actualTransformer = new InstrumentationLoadTimeWeaver.FilteringClassFileTransformer(transformer, this.classLoader);
    List var3 = this.transformers;
    synchronized(this.transformers) {
        Assert.state(this.instrumentation != null, "Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.");
        this.instrumentation.addTransformer(actualTransformer);
        this.transformers.add(actualTransformer);
    }
}

可以看到,这个错误是在判断仪表盘是否为空的时候产生的,而我们现在不需要这个,所以完全可以把这个错误隐藏掉,因此,创建一个扩展类,覆盖这点代码:

public class ExtInstrumentationLoadTimeWeaver extends
        InstrumentationLoadTimeWeaver {
    @Override
    public void addTransformer(ClassFileTransformer transformer) {
        try {
            super.addTransformer(transformer);
        } catch (Exception e) {}
    }
}

然后修改setLoadTimeWeaver方法:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                                                                   JpaVendorAdapter jpaVendorAdapter){
    LocalContainerEntityManagerFactoryBean lcemf=new LocalContainerEntityManagerFactoryBean();
    lcemf.setLoadTimeWeaver(new ExtInstrumentationLoadTimeWeaver( ));
    lcemf.setDataSource(dataSource);
    lcemf.setJpaVendorAdapter(jpaVendorAdapter);
    lcemf.setPackagesToScan("com.niufennan.jtodos.models");
    return lcemf;
}

这时,土土的运行一下,完全ok。

当然,还可以在tomcat配置的地方为VM options设置参数,-javaagent:spring-agent.jar的绝对路径,因为它使用了绝对路径,所以我很不喜欢。故不采用这种方法。

还可以使用一个更简单的方法,即换一个JavaEE的容器,如Jetty,因为这个Bug只在Tomcat中会出现(至少目前我只在Tomcat中发现)

截止目前源代码v1-11_1

中配版

折腾半天,终于开着低配版的路虎起飞了,但你可能也发现了:

  1. 代码并没有减少,甚至更加复杂
  2. 每次都调用entityManagerFactory.createEntityManager(),看着很不爽
  3. 同2,这意味着会创建很多EntityManager对象。

那么有没有更方便的方法呢,就像换一辆中配的汽车?

当然可以,可是有个大问题就是EntityManager不是线程安全的,一般来说,不适合作为共享bean注入到Repository中,但是好在Spring依然为我们提供了方法:

@Transactional
@Repository
public class JpaTodoRepository implements TodoRepository {

    @PersistenceContext
    private EntityManager entityManager;
    public List<Todo> getAll() {
        CriteriaQuery<Todo> criteriaQuery=entityManager.getCriteriaBuilder().createQuery(Todo.class);
        return entityManager.createQuery(criteriaQuery).getResultList();
    }
    public List<Todo> getTodoByUserId(int userId) {
        CriteriaBuilder builder=entityManager.getCriteriaBuilder();
        CriteriaQuery<Todo> criteriaQuery=builder.createQuery(Todo.class);
        Root<Todo> todoRoot = criteriaQuery.from(Todo.class);
        Predicate predicate = builder.equal(todoRoot.get("userId"), userId);
        criteriaQuery.where(predicate);
        return entityManager.createQuery(criteriaQuery).getResultList();
    }
    public void save(Todo todo) {
        entityManager.persist(todo);
    }
}

这里的关键就是@PersistenceContext,它的精彩之处是不没有真的注入EntityManager,而是产生了一个代理(貌似Spring大量的使用了代理模式),然后真正的实体管理器始终是与当前事物相关联的那一个,当然如果不存在,则会重新创建一个,这样的话,就能始终保持他是线程安全的。

@PersistenceContext与@PersistenceUnit均不是Spring的注解,他是jpa的注解。

好,现在土土的运行一下,发现中配版的路虎也可以起飞了。

截止目前源代码v1-11_2

高配版路虎

中配版升级了实体管理器,实现了由容器自动管理实体管理器的创建和使用,那么接下来看一下代码,能不能升级一下查询的方法体呢?

答案是当然可以,甚至我们都可以只写一个Repository的接口就可以了,继续修改TodoRepository:

public interface TodoRepository extends JpaRepository<Todo,Integer> {
    public List<Todo> getTodoByUserId(int userId);
}

然后我们将此接口的实现删除,土土的运行一下,完全Ok。

这很令人惊讶,为什么,完全没有实现类和任何的注解!实际上,因为TodoRepository继承JpaRepository,而JpaRepository经过一系列的继承,最终继承并扩展了Repository接口,于是,Spring-Data框架会扫描定义包内所有的Repository的子接口,并在应用启动的时候创建他的实现类,而且实现类中会默认包含CurdRepository等父接口所包含的18个方法。

一个非常令人惊叹的技术。

通过JpaRepository提供的18个方法,几乎可以进行任何通用的操作,那么我的需求超过这些方法了怎么办,比如getTodoByUserId方法

这里就牵扯到Spring-Data的另一个令人惊叹的技术,根据方法名与实体对像推断方法的目的:

动词(get)--主题(Todo)--关键词(by)--断言(UserId)

根据这种组合,我们几乎可以实现任何功能,如根据User获取todo列表并更加创建时间排序:

getTodoByUserIdOrderByCreateTime

Spring-Data允许的动词:

get,read,find,count等

get,read,find没有明显差别。

由于此实现是基于泛型的,所以主题可以省略。

而断言部分则是精华所在,非常的繁复,灵活,几乎支持所有的sql语句关键字,具体可以根据日志打印的sql语句与断言匹配以练习。

截止目前源代码v1-11_3

改装车

一个无法改装的越野车不是好越野车,当我发现这些均无法满足要求怎么办?我查询的sql语句无比复杂,断言几乎无法完成,那怎么办呢?

这时候我们可以部分退化到中配版,但依然使用高配版的全自动化,机创建一个实现类,但这个实现类按照约定命名,即Repository接口加impl后缀,(此类仅为举例):

public class TodoRepositoryImpl implements ExtTodoRepository {
    @PersistenceContext
    private EntityManager entityManager;
    public List<Todo> getTodoByUserId(int userId) {
        String sql="select t from  com.niufennan.jtodos.models.Todo t where  t.userId=:userId";
        Query query= entityManager.createQuery(sql);
        query.setParameter("userId",userId);
        return query.getResultList();
    }
}

这里使用ExtTodoRepository接口是因为如果使用TodoRepository接口的话,会要求实现所有的18个方法,ExtTodoRepository的代码如下:

public interface ExtTodoRepository {
    public List<Todo> getTodoByUserId(int userId);
}

最后,还要让TodoRepository知道ExtTodoRepository定义的方法:

public interface TodoRepository extends JpaRepository<Todo,Integer> ,ExtTodoRepository{
}

这样,就可以灵活的使用hql(?)来进行查询了,甚至可以直接使用createSqlQuery来直接使用SQL进行查询。

截止目前源代码v1-11_4

这部分内容提交后删除

行车记录仪

整理代码,将不需要的,如Dao和impl包下的内容全部删除,并允许,同时添加一条新的todo记录,留个纪念吧:

[图片上传失败…(image-543fa5-1513757229395)]

很完美,不是么,但是,控制台有这样一条输出缺引起了我的注意:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console. Set system property 'log4j2.debug' to show Log4j2 internal initialization logging.

没有找到日志的配置项,所以就输出到控制台了,当然我们能从控制台看到好多东西,比如生成的sql语句:

SELECT ID, CREATETIME, ITEM, USERID FROM TODOS WHERE (USERID = ?)

但是,就像是开车一样,没有任何人喜欢碰撞,但是如果真的出现了,紧靠研究记录肯定是不行的,这时候需要一个行车记录仪就方便多了,而日志也起了同样的作用,就是将程序中任何的问题,输出均记录下来。而Spring其实已经将日志的一切都自动化执行了,我们所需要的,仅仅是配置一个日志配置文件即可.

Log4j2不支持properties文件,只可以使用xml,yaml和json,下面是一个xml配置的例子:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <properties>
        <property name="LOG_HOME">${sys:catalina.home}/WEB-INF/logs</property>
        <property name="FILE_NAME">jtodos_log</property>
    </properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
        <RollingFile name="RollingFile" fileName="${LOG_HOME}/${FILE_NAME}.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log.gz"
                     immediateFlush="true">
            <PatternLayout
                    pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="10 M" />
            </Policies>
            <DefaultRolloverStrategy max="20" />
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <!-- 这里是输入到文件-->
            <AppenderRef ref="RollingFile" />
            <!-- 这里是输入到控制台-->
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>

运行后,到日志路径去看,日志书写完成:

[图片上传失败…(image-581afa-1513757229395)]

里边内容可以自行查看。

不知不觉,写了这么多字,看来能开上路虎真的不容易呀:)
11章最终版代码 v1-11_5
谢谢观看