
這一章開始的時(shí)候,先拿一個(gè)廣告圖鎮(zhèn)樓:

圖是網(wǎng)上隨便找的,哈哈好希望真的有路虎
這句廣告此很有意思,雖然腳踏實(shí)地的走路是最踏實(shí)的(jdbc),如果可以,當(dāng)然有輛自行車(JdbcTemplate)就更好了.但我相信,一輛能裝載,速度快,安全性高的路虎,是每個(gè)人心中的夢(mèng)想。
路虎
我們想要這樣一些能力:
- 對(duì)象可以和數(shù)據(jù)庫(kù)字段自動(dòng)進(jìn)行映射
- 自動(dòng)生成sql語(yǔ)句
- 自動(dòng)完成查詢條件
- 自動(dòng)生成級(jí)聯(lián)關(guān)系
- 自動(dòng)管理數(shù)據(jù)庫(kù)緩存和延遲加載等
這些能力可以使我們從無(wú)休止的?中解脫出來(lái),那么有沒(méi)有這樣一種既簡(jiǎn)單,又方便的工具呢?Spring集成的JPA功能登場(chǎng)了。
JPA(Java Persistence API)Java持久性API,是用于對(duì)象/關(guān)系映射(ORM)的Java API,其中Java對(duì)象映射到數(shù)據(jù)庫(kù)工件,以便在java應(yīng)用程序中管理數(shù)據(jù)關(guān)系。JPA包括Java持久性查詢語(yǔ)言(JPQL),Java持久性標(biāo)準(zhǔn)API以及用于定義對(duì)象/關(guān)系映射元數(shù)據(jù)的Java API和XML模式。
需要再次強(qiáng)調(diào)一下,JPA不是orm,他僅僅是一套API標(biāo)準(zhǔn)。
Spring2開始集成了JPA功能,就像有一輛車之前需要駕照,使用JPA之前同樣需要引入JPA所依賴的包:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
然后我們就可以去4S點(diǎn)去試駕,或者選車,出去飛了。
試駕
引入JPA的依賴包之后開始對(duì)JPA進(jìn)行配置,而配置JPA的第一步就是要配置實(shí)體管理工廠的Bean,以獲取實(shí)體管理器,在JPA中定義了兩種實(shí)體管理工廠:
- 應(yīng)用程序管理類型:程序向管理器工廠直接請(qǐng)求時(shí),會(huì)創(chuàng)建一個(gè)管理器,適合不在JavaEE容器中的應(yīng)用程序,需配置
persistence.xml文件 - 容器管理類型:應(yīng)用程序不和管理器工廠打交道,它的創(chuàng)建由容器負(fù)責(zé)。適合運(yùn)行在容器中的程序,可不需要配置
persistence.xml文件
我們的程序即在JavaEE容器中運(yùn)行,有極力的想要全java配置,所以當(dāng)然選擇容器管理類型了,在Spring中使用LocalContainerEntityManagerFactoryBean的FactoryBean來(lái)配置實(shí)體管理器:
@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;
}
注意這個(gè)Bean需要兩個(gè)參數(shù),分別為數(shù)據(jù)源和Jpa實(shí)現(xiàn)適配器,然后分別set到對(duì)象里,并且通過(guò)'setPackagesToScan'方法設(shè)置默認(rèn)掃描的實(shí)體包。
在這個(gè)bean的參數(shù)里,數(shù)據(jù)源即上一章設(shè)置的數(shù)據(jù)源,這里不在敘述,而JpaVendorAdapter是針對(duì)JPA不同的實(shí)現(xiàn),目前JPA的實(shí)現(xiàn)有很多種,主要有Hibernate,OpenJpa,EclipseJpa等,對(duì)于Spring-jpa的用戶來(lái)說(shuō),使用哪種實(shí)現(xiàn)在代碼上都無(wú)所謂,因?yàn)橐呀?jīng)在容器中透明了,這里我選擇了EclipseLinkJPA的實(shí)現(xiàn),首先還是引入依賴:
<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 設(shè)置訪問(wèn)的數(shù)據(jù)庫(kù)類型
2 設(shè)置在日志中輸出生成的SQL
3 設(shè)置是否根據(jù)數(shù)據(jù)實(shí)體生成修改數(shù)據(jù)庫(kù)結(jié)構(gòu),這里不修改
4 設(shè)置sql方言
然后,根據(jù)JPA實(shí)際的需求,我們還需要對(duì)實(shí)體類進(jìn)行一些改造,這里以Todo類為例,改造方式如下:
- 增加JPA所需的一些注解
- 將基本數(shù)據(jù)類型換成包裝類形式
改造完后代碼如下:
@Entity(name = "todos")
public class Todo {
@Id
private Integer id;
private String item;
private Date createTime=new Date();
private Integer userId;
get... set...
}
現(xiàn)在挑選完成,準(zhǔn)備起飛。
低配版##
為了和上一章的dao類區(qū)分,我們新創(chuàng)建一個(gè)persistence包,用來(lái)存放基于JPA實(shí)現(xiàn)的持久層類,首先,創(chuàng)建一個(gè)TodoRepository類,并在里定義三個(gè)方法,即將TodoDao接口的方法拷貝入內(nèi):
public interface TodoRepository {
public List<Todo> getAll();
public List<Todo> getTodoByUserId(int userId);
public void save(Todo todo);
}
然后統(tǒng)一創(chuàng)建impl,作為接口的實(shí)現(xiàn),這里創(chuàng)建一個(gè)基于jpa實(shí)現(xiàn)的類:
public class JpaTodoRepository implements TodoRepository {
public List<Todo> getAll() {
return null;
}
public List<Todo> getTodoByUserId(int userId) {
return null;
}
public void save(Todo todo) {
}
}
下面完成這個(gè)類:
@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);
}
}
我知道你想說(shuō)什么,看上去代碼好復(fù)雜,尤其是條件查詢的部分,這里先對(duì)條件查詢進(jìn)行一下說(shuō)明:
CriteriaBuilder builder=entityManagerFactory.createEntityManager().getCriteriaBuilder(); //基于建造模式,構(gòu)建一個(gè)Criteria構(gòu)建器對(duì)象(基于Criteria模式進(jìn)行條件查詢)
CriteriaQuery<Todo> criteriaQuery=builder.createQuery(Todo.class); //為Todo對(duì)象創(chuàng)建一個(gè)基礎(chǔ)查詢
Root<Todo> todoRoot = criteriaQuery.from(Todo.class); //為基礎(chǔ)查詢?cè)O(shè)置一個(gè)查詢條件列表
Predicate predicate = builder.equal(todoRoot.get("userId"), userId); //通過(guò)userId進(jìn)行查詢
criteriaQuery.where(predicate);
return entityManagerFactory.createEntityManager().createQuery(criteriaQuery).getResultList(); //設(shè)置 查詢條件并返回
其余的代碼很簡(jiǎn)單就不在敘述,接下來(lái)使用土土的測(cè)試方式,運(yùn)行一下,阿啊哦,報(bào)錯(cuò)了,查看一下報(bào)錯(cuò)信息(復(fù)制其中的一句):
Failed to load class "org.slf4j.impl.StaticLoggerBinder".
這是因?yàn)镋clipseLink默認(rèn)使用了slf4j的API記錄日志,所以之類需要添加對(duì)它的引用即可:
<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>
然后土土的跑起來(lái),測(cè)試一下,啊哦,還是有錯(cuò)誤,查看一下報(bào)錯(cuò)信息:
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
提示對(duì)LoadTimeWeaver的調(diào)用失敗,那么LoadTimeWeaver又是做什么用的呢?LoadTimeWeaver顧名思義,就是使用AspectJ提供在Aop中類加載時(shí)織入切片的能力。
那么如何使用LoadTimeWeaver呢?首先,需要通過(guò)JVM的-javaagent參數(shù)設(shè)置LTW的織入器類包,以代理JVM默認(rèn)的類加載器;第二,LTW織入器需要一個(gè) aop.xml文件,在該文件中指定切面類和需要進(jìn)行切面織入的目標(biāo)類。簡(jiǎn)單說(shuō),就是提供動(dòng)態(tài)代理的能力。我們可以使用注解:
@EnableLoadTimeWeaving( aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.DISABLED)
對(duì)他進(jìn)行關(guān)閉。
這時(shí)候運(yùn)行,ok 成功出現(xiàn)了我們需要的頁(yè)面。
但這樣顯然不是什么好主意,因?yàn)镾pring現(xiàn)在就是基于注解在使用的,而基于注解,肯定會(huì)不可避免的使用到動(dòng)態(tài)代理的織入,所以,將LTW禁用顯然是不合理的。所以,最簡(jiǎn)單的方法是,既然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進(jìn)去,在運(yùn)行一下,還是報(bào)錯(cuò),查看一下報(bào)錯(cuò)信息:
Must start with Java agent to use InstrumentationLoadTimeWeaver
難道一定要修改java的啟動(dòng)參數(shù)么?當(dāng)然不是,進(jìn)入源碼看一看(此源碼為在Idea環(huán)境下直接雙擊進(jìn)入):
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);
}
}
可以看到,這個(gè)錯(cuò)誤是在判斷儀表盤是否為空的時(shí)候產(chǎn)生的,而我們現(xiàn)在不需要這個(gè),所以完全可以把這個(gè)錯(cuò)誤隱藏掉,因此,創(chuàng)建一個(gè)擴(kuò)展類,覆蓋這點(diǎn)代碼:
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;
}
這時(shí),土土的運(yùn)行一下,完全ok。
當(dāng)然,還可以在tomcat配置的地方為VM options設(shè)置參數(shù),-javaagent:spring-agent.jar的絕對(duì)路徑,因?yàn)樗褂昧私^對(duì)路徑,所以我很不喜歡。故不采用這種方法。
還可以使用一個(gè)更簡(jiǎn)單的方法,即換一個(gè)JavaEE的容器,如Jetty,因?yàn)檫@個(gè)Bug只在Tomcat中會(huì)出現(xiàn)(至少目前我只在Tomcat中發(fā)現(xiàn))
中配版
折騰半天,終于開著低配版的路虎起飛了,但你可能也發(fā)現(xiàn)了:
- 代碼并沒(méi)有減少,甚至更加復(fù)雜
- 每次都調(diào)用entityManagerFactory.createEntityManager(),看著很不爽
- 同2,這意味著會(huì)創(chuàng)建很多EntityManager對(duì)象。
那么有沒(méi)有更方便的方法呢,就像換一輛中配的汽車?
當(dāng)然可以,可是有個(gè)大問(wèn)題就是EntityManager不是線程安全的,一般來(lái)說(shuō),不適合作為共享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);
}
}
這里的關(guān)鍵就是@PersistenceContext,它的精彩之處是不沒(méi)有真的注入EntityManager,而是產(chǎn)生了一個(gè)代理(貌似Spring大量的使用了代理模式),然后真正的實(shí)體管理器始終是與當(dāng)前事物相關(guān)聯(lián)的那一個(gè),當(dāng)然如果不存在,則會(huì)重新創(chuàng)建一個(gè),這樣的話,就能始終保持他是線程安全的。
@PersistenceContext與@PersistenceUnit均不是Spring的注解,他是jpa的注解。
好,現(xiàn)在土土的運(yùn)行一下,發(fā)現(xiàn)中配版的路虎也可以起飛了。
高配版路虎
中配版升級(jí)了實(shí)體管理器,實(shí)現(xiàn)了由容器自動(dòng)管理實(shí)體管理器的創(chuàng)建和使用,那么接下來(lái)看一下代碼,能不能升級(jí)一下查詢的方法體呢?
答案是當(dāng)然可以,甚至我們都可以只寫一個(gè)Repository的接口就可以了,繼續(xù)修改TodoRepository:
public interface TodoRepository extends JpaRepository<Todo,Integer> {
public List<Todo> getTodoByUserId(int userId);
}
然后我們將此接口的實(shí)現(xiàn)刪除,土土的運(yùn)行一下,完全Ok。
這很令人驚訝,為什么,完全沒(méi)有實(shí)現(xiàn)類和任何的注解!實(shí)際上,因?yàn)門odoRepository繼承JpaRepository,而JpaRepository經(jīng)過(guò)一系列的繼承,最終繼承并擴(kuò)展了Repository接口,于是,Spring-Data框架會(huì)掃描定義包內(nèi)所有的Repository的子接口,并在應(yīng)用啟動(dòng)的時(shí)候創(chuàng)建他的實(shí)現(xiàn)類,而且實(shí)現(xiàn)類中會(huì)默認(rèn)包含CurdRepository等父接口所包含的18個(gè)方法。
一個(gè)非常令人驚嘆的技術(shù)。
通過(guò)JpaRepository提供的18個(gè)方法,幾乎可以進(jìn)行任何通用的操作,那么我的需求超過(guò)這些方法了怎么辦,比如getTodoByUserId方法
這里就牽扯到Spring-Data的另一個(gè)令人驚嘆的技術(shù),根據(jù)方法名與實(shí)體對(duì)像推斷方法的目的:
動(dòng)詞(get)--主題(Todo)--關(guān)鍵詞(by)--斷言(UserId)
根據(jù)這種組合,我們幾乎可以實(shí)現(xiàn)任何功能,如根據(jù)User獲取todo列表并更加創(chuàng)建時(shí)間排序:
getTodoByUserIdOrderByCreateTime
Spring-Data允許的動(dòng)詞:
get,read,find,count等
get,read,find沒(méi)有明顯差別。
由于此實(shí)現(xiàn)是基于泛型的,所以主題可以省略。
而斷言部分則是精華所在,非常的繁復(fù),靈活,幾乎支持所有的sql語(yǔ)句關(guān)鍵字,具體可以根據(jù)日志打印的sql語(yǔ)句與斷言匹配以練習(xí)。
改裝車
一個(gè)無(wú)法改裝的越野車不是好越野車,當(dāng)我發(fā)現(xiàn)這些均無(wú)法滿足要求怎么辦?我查詢的sql語(yǔ)句無(wú)比復(fù)雜,斷言幾乎無(wú)法完成,那怎么辦呢?
這時(shí)候我們可以部分退化到中配版,但依然使用高配版的全自動(dòng)化,機(jī)創(chuàng)建一個(gè)實(shí)現(xiàn)類,但這個(gè)實(shí)現(xiàn)類按照約定命名,即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接口是因?yàn)槿绻褂肨odoRepository接口的話,會(huì)要求實(shí)現(xiàn)所有的18個(gè)方法,ExtTodoRepository的代碼如下:
public interface ExtTodoRepository {
public List<Todo> getTodoByUserId(int userId);
}
最后,還要讓TodoRepository知道ExtTodoRepository定義的方法:
public interface TodoRepository extends JpaRepository<Todo,Integer> ,ExtTodoRepository{
}
這樣,就可以靈活的使用hql(?)來(lái)進(jìn)行查詢了,甚至可以直接使用createSqlQuery來(lái)直接使用SQL進(jìn)行查詢。
這部分內(nèi)容提交后刪除
行車記錄儀
整理代碼,將不需要的,如Dao和impl包下的內(nèi)容全部刪除,并允許,同時(shí)添加一條新的todo記錄,留個(gè)紀(jì)念吧:
很完美,不是么,但是,控制臺(tái)有這樣一條輸出缺引起了我的注意:
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.
沒(méi)有找到日志的配置項(xiàng),所以就輸出到控制臺(tái)了,當(dāng)然我們能從控制臺(tái)看到好多東西,比如生成的sql語(yǔ)句:
SELECT ID, CREATETIME, ITEM, USERID FROM TODOS WHERE (USERID = ?)
但是,就像是開車一樣,沒(méi)有任何人喜歡碰撞,但是如果真的出現(xiàn)了,緊靠研究記錄肯定是不行的,這時(shí)候需要一個(gè)行車記錄儀就方便多了,而日志也起了同樣的作用,就是將程序中任何的問(wèn)題,輸出均記錄下來(lái)。而Spring其實(shí)已經(jīng)將日志的一切都自動(dòng)化執(zhí)行了,我們所需要的,僅僅是配置一個(gè)日志配置文件即可.
Log4j2不支持properties文件,只可以使用xml,yaml和json,下面是一個(gè)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" />
<!-- 這里是輸入到控制臺(tái)-->
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
運(yùn)行后,到日志路徑去看,日志書寫完成:
里邊內(nèi)容可以自行查看。
不知不覺(jué),寫了這么多字,看來(lái)能開上路虎真的不容易呀:)
11章最終版代碼 v1-11_5
謝謝觀看