美文网首页Java 杂谈
如果你想开发一个应用(1-11)

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

作者: 双鱼座的牛 | 来源:发表于2017-12-20 16:07 被阅读0次

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

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

    这句广告此很有意思,虽然脚踏实地的走路是最踏实的(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
    谢谢观看

    相关文章

      网友评论

        本文标题:如果你想开发一个应用(1-11)

        本文链接:https://www.haomeiwen.com/subject/vlwgwxtx.html