如果你想开发一个应用(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记录,留个纪念吧:

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

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>

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

里边内容可以自行查看。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354