背景
springboot因其提供了各種開箱即用的插件,使得它成為了當(dāng)今最為主流的java web開發(fā)框架之一。mybatis是一個十分輕量好用的orm框架。redis是當(dāng)今十分主流的分布式key-value型數(shù)據(jù)庫,在web開發(fā)中,我們常用它來緩存數(shù)據(jù)庫的查詢結(jié)果。
本篇博客將介紹如何使用springboot快速搭建一個web應(yīng)用,并且采用mybatis作為我們的orm框架。為了提升性能,我們將redis作為mybatis的二級緩存。為了測試我們的代碼,我們編寫了單元測試,并且用h2內(nèi)存數(shù)據(jù)庫來生成我們的測試數(shù)據(jù)。通過該項目,我們希望讀者可以快速掌握現(xiàn)代化java web開發(fā)的技巧以及最佳實踐。
本文的示例代碼可在github中下載:https://github.com/lovelcp/spring-boot-mybatis-with-redis/tree/master
環(huán)境
開發(fā)環(huán)境:mac 10.11
ide:intellij 2017.1
jdk:1.8
spring-boot:1.5.3.release
redis:3.2.9
mysql:5.7
spring-boot
新建項目
首先,我們需要初始化我們的spring-boot工程。通過intellij的spring initializer,新建一個spring-boot工程變得十分簡單。首先我們在intellij中選擇new一個project:
然后在選擇依賴的界面,勾選web、mybatis、redis、mysql、h2:
新建工程成功之后,我們可以看到項目的初始結(jié)構(gòu)如下圖所示:
spring initializer已經(jīng)幫我們自動生成了一個啟動類——springbootmybatiswithredisapplication。該類的代碼十分簡單:
1
2
3
4
5
6
|
@springbootapplication public class springbootmybatiswithredisapplication { public static void main(string[] args) { springapplication.run(springbootmybatiswithredisapplication. class , args); } } |
@springbootapplication注解表示啟用spring boot的自動配置特性。好了,至此我們的項目骨架已經(jīng)搭建成功,感興趣的讀者可以通過intellij啟動看看效果。
新建api接口
接下來,我們要編寫web api。假設(shè)我們的web工程負(fù)責(zé)處理商家的產(chǎn)品(product)。我們需要提供根據(jù)product id返回product信息的get接口和更新product信息的put接口。首先我們定義product類,該類包括產(chǎn)品id,產(chǎn)品名稱name以及價格price:
1
2
3
4
5
6
7
|
public class product implements serializable { private static final long serialversionuid = 1435515995276255188l; private long id; private string name; private long price; // getters setters } |
然后我們需要定義controller類。由于spring boot內(nèi)部使用spring mvc作為它的web組件,所以我們可以通過注解的方式快速開發(fā)我們的接口類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@restcontroller @requestmapping ( "/product" ) public class productcontroller { @getmapping ( "/{id}" ) public product getproductinfo( @pathvariable ( "id" ) long productid) { // todo return null ; } @putmapping ( "/{id}" ) public product updateproductinfo( @pathvariable ( "id" ) long productid, @requestbody product newproduct) { // todo return null ; } } |
我們簡單介紹一下上述代碼中所用到的注解的作用:
@restcontroller:表示該類為controller,并且提供rest接口,即所有接口的值以json格式返回。該注解其實是@controller和@responsebody的組合注解,便于我們開發(fā)rest api。
@requestmapping、@getmapping、@putmapping:表示接口的url地址。標(biāo)注在類上的@requestmapping注解表示該類下的所有接口的url都以/product開頭。@getmapping表示這是一個get http接口,@putmapping表示這是一個put http接口。
@pathvariable、@requestbody:表示參數(shù)的映射關(guān)系。假設(shè)有個get請求訪問的是/product/123,那么該請求會由getproductinfo方法處理,其中url里的123會被映射到productid中。同理,如果是put請求的話,請求的body會被映射到newproduct對象中。
這里我們只定義了接口,實際的處理邏輯還未完成,因為product的信息都存在數(shù)據(jù)庫中。接下來我們將在項目中集成mybatis,并且與數(shù)據(jù)庫做交互。
集成mybatis
配置數(shù)據(jù)源
首先我們需要在配置文件中配置我們的數(shù)據(jù)源。我們采用mysql作為我們的數(shù)據(jù)庫。這里我們采用yaml作為我們配置文件的格式。我們在resources目錄下新建application.yml文件:
1
2
3
4
5
6
7
|
spring: # 數(shù)據(jù)庫配置 datasource: url: jdbc:mysql: //{your_host}/{your_db} username: {your_username} password: {your_password} driver- class -name: org.gjt.mm.mysql.driver |
由于spring boot擁有自動配置的特性,我們不用新建一個datasource的配置類,sping boot會自動加載配置文件并且根據(jù)配置文件的信息建立數(shù)據(jù)庫的連接池,十分便捷。
筆者推薦大家采用yaml作為配置文件的格式。xml顯得冗長,properties沒有層級結(jié)構(gòu),yaml剛好彌補了這兩者的缺點。這也是spring boot默認(rèn)就支持yaml格式的原因。
配置mybatis
我們已經(jīng)通過spring initializer在pom.xml中引入了mybatis-spring-boot-starte庫,該庫會自動幫我們初始化mybatis。首先我們在application.yml中填寫mybatis的相關(guān)配置:
1
2
3
4
5
6
7
|
# mybatis配置 mybatis: # 配置映射類所在包名 type-aliases- package : com.wooyoo.learning.dao.domain # 配置mapper xml文件所在路徑,這里是一個數(shù)組 mapper-locations: - mappers/productmapper.xml |
然后,再在代碼中定義productmapper類:
1
2
3
4
5
6
7
|
@mapper public interface productmapper { product select( @param ( "id" ) long id); void update(product product); } |
這里,只要我們加上了@mapper注解,spring boot在初始化mybatis時會自動加載該mapper類。
spring boot之所以這么流行,最大的原因是它自動配置的特性。開發(fā)者只需要關(guān)注組件的配置(比如數(shù)據(jù)庫的連接信息),而無需關(guān)心如何初始化各個組件,這使得我們可以集中精力專注于業(yè)務(wù)的實現(xiàn),簡化開發(fā)流程。
訪問數(shù)據(jù)庫
完成了mybatis的配置之后,我們就可以在我們的接口中訪問數(shù)據(jù)庫了。我們在productcontroller下通過@autowired引入mapper類,并且調(diào)用對應(yīng)的方法實現(xiàn)對product的查詢和更新操作,這里我們以查詢接口為例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@restcontroller @requestmapping ( "/product" ) public class productcontroller { @autowired private productmapper productmapper; @getmapping ( "/{id}" ) public product getproductinfo( @pathvariable ( "id" ) long productid) { return productmapper.select(productid); } // 避免篇幅過長,省略updateproductinfo的代碼 } |
然后在你的mysql中插入幾條product的信息,就可以運行該項目看看是否能夠查詢成功了。
至此,我們已經(jīng)成功地在項目中集成了mybatis,增添了與數(shù)據(jù)庫交互的能力。但是這還不夠,一個現(xiàn)代化的web項目,肯定會上緩存加速我們的數(shù)據(jù)庫查詢。接下來,將介紹如何科學(xué)地將redis集成到mybatis的二級緩存中,實現(xiàn)數(shù)據(jù)庫查詢的自動緩存。
集成redis
配置redis
同訪問數(shù)據(jù)庫一樣,我們需要配置redis的連接信息。在application.yml文件中增加如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
spring: redis: # redis數(shù)據(jù)庫索引(默認(rèn)為 0 ),我們使用索引為 3 的數(shù)據(jù)庫,避免和其他數(shù)據(jù)庫沖突 database: 3 # redis服務(wù)器地址(默認(rèn)為localhost) host: localhost # redis端口(默認(rèn)為 6379 ) port: 6379 # redis訪問密碼(默認(rèn)為空) password: # redis連接超時時間(單位為毫秒) timeout: 0 # redis連接池配置 pool: # 最大可用連接數(shù)(默認(rèn)為 8 ,負(fù)數(shù)表示無限) max-active: 8 # 最大空閑連接數(shù)(默認(rèn)為 8 ,負(fù)數(shù)表示無限) max-idle: 8 # 最小空閑連接數(shù)(默認(rèn)為 0 ,該值只有為正數(shù)才有作用) min-idle: 0 # 從連接池中獲取連接最大等待時間(默認(rèn)為- 1 ,單位為毫秒,負(fù)數(shù)表示無限) max-wait: - 1 |
上述列出的都為常用配置,讀者可以通過注釋信息了解每個配置項的具體作用。由于我們在pom.xml中已經(jīng)引入了spring-boot-starter-data-redis庫,所以spring boot會幫我們自動加載redis的連接,具體的配置類
org.springframework.boot.autoconfigure.data.redis.redisautoconfiguration。通過該配置類,我們可以發(fā)現(xiàn)底層默認(rèn)使用jedis庫,并且提供了開箱即用的redistemplate和stringtemplate。
將redis作為二級緩存
mybatis的二級緩存原理本文不再贅述,讀者只要知道,mybatis的二級緩存可以自動地對數(shù)據(jù)庫的查詢做緩存,并且可以在更新數(shù)據(jù)時同時自動地更新緩存。
實現(xiàn)mybatis的二級緩存很簡單,只需要新建一個類實現(xiàn)org.apache.ibatis.cache.cache接口即可。
該接口共有以下五個方法:
string getid():mybatis緩存操作對象的標(biāo)識符。一個mapper對應(yīng)一個mybatis的緩存操作對象。
void putobject(object key, object value):將查詢結(jié)果塞入緩存。
object getobject(object key):從緩存中獲取被緩存的查詢結(jié)果。
object removeobject(object key):從緩存中刪除對應(yīng)的key、value。只有在回滾時觸發(fā)。一般我們也可以不用實現(xiàn),具體使用方式請參考:org.apache.ibatis.cache.decorators.transactionalcache。
void clear():發(fā)生更新時,清除緩存。
int getsize():可選實現(xiàn)。返回緩存的數(shù)量。
readwritelock getreadwritelock():可選實現(xiàn)。用于實現(xiàn)原子性的緩存操作。
接下來,我們新建rediscache類,實現(xiàn)cache接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
public class rediscache implements cache { private static final logger logger = loggerfactory.getlogger(rediscache. class ); private final readwritelock readwritelock = new reentrantreadwritelock(); private final string id; // cache instance id private redistemplate redistemplate; private static final long expire_time_in_minutes = 30 ; // redis過期時間 public rediscache(string id) { if (id == null ) { throw new illegalargumentexception( "cache instances require an id" ); } this .id = id; } @override public string getid() { return id; } /** * put query result to redis * * @param key * @param value */ @override @suppresswarnings ( "unchecked" ) public void putobject(object key, object value) { redistemplate redistemplate = getredistemplate(); valueoperations opsforvalue = redistemplate.opsforvalue(); opsforvalue.set(key, value, expire_time_in_minutes, timeunit.minutes); logger.debug( "put query result to redis" ); } /** * get cached query result from redis * * @param key * @return */ @override public object getobject(object key) { redistemplate redistemplate = getredistemplate(); valueoperations opsforvalue = redistemplate.opsforvalue(); logger.debug( "get cached query result from redis" ); return opsforvalue.get(key); } /** * remove cached query result from redis * * @param key * @return */ @override @suppresswarnings ( "unchecked" ) public object removeobject(object key) { redistemplate redistemplate = getredistemplate(); redistemplate.delete(key); logger.debug( "remove cached query result from redis" ); return null ; } /** * clears this cache instance */ @override public void clear() { redistemplate redistemplate = getredistemplate(); redistemplate.execute((rediscallback) connection -> { connection.flushdb(); return null ; }); logger.debug( "clear all the cached query result from redis" ); } @override public int getsize() { return 0 ; } @override public readwritelock getreadwritelock() { return readwritelock; } private redistemplate getredistemplate() { if (redistemplate == null ) { redistemplate = applicationcontextholder.getbean( "redistemplate" ); } return redistemplate; } } |
講解一下上述代碼中一些關(guān)鍵點:
自己實現(xiàn)的二級緩存,必須要有一個帶id的構(gòu)造函數(shù),否則會報錯。
我們使用spring封裝的redistemplate來操作redis。網(wǎng)上所有介紹redis做二級緩存的文章都是直接用jedis庫,但是筆者認(rèn)為這樣不夠spring style,而且,redistemplate封裝了底層的實現(xiàn),未來如果我們不用jedis了,我們可以直接更換底層的庫,而不用修改上層的代碼。更方便的是,使用redistemplate,我們不用關(guān)心redis連接的釋放問題,否則新手很容易忘記釋放連接而導(dǎo)致應(yīng)用卡死。
需要注意的是,這里不能通過autowire的方式引用redistemplate,因為rediscache并不是spring容器里的bean。所以我們需要手動地去調(diào)用容器的getbean方法來拿到這個bean,具體的實現(xiàn)方式請參考github中的代碼。
我們采用的redis序列化方式是默認(rèn)的jdk序列化。所以數(shù)據(jù)庫的查詢對象(比如product類)需要實現(xiàn)serializable接口。
這樣,我們就實現(xiàn)了一個優(yōu)雅的、科學(xué)的并且具有spring style的redis緩存類。
開啟二級緩存
接下來,我們需要在productmapper.xml中開啟二級緩存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?xml version= "1.0" encoding= "utf-8" ?> <!doctype mapper public "-//mybatis.org//dtd mapper 3.0//en" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace= "com.wooyoo.learning.dao.mapper.productmapper" > <!-- 開啟基于redis的二級緩存 --> <cache type= "com.wooyoo.learning.util.rediscache" /> <select id= "select" resulttype= "product" > select * from products where id = #{id} limit 1 </select> <update id= "update" parametertype= "product" flushcache= "true" > update products set name = #{name}, price = #{price} where id = #{id} limit 1 </update> </mapper> |
<cache type="com.wooyoo.learning.util.rediscache"/>表示開啟基于redis的二級緩存,并且在update語句中,我們設(shè)置flushcache為true,這樣在更新product信息時,能夠自動失效緩存(本質(zhì)上調(diào)用的是clear方法)。
測試
配置h2內(nèi)存數(shù)據(jù)庫
至此我們已經(jīng)完成了所有代碼的開發(fā),接下來我們需要書寫單元測試代碼來測試我們代碼的質(zhì)量。我們剛才開發(fā)的過程中采用的是mysql數(shù)據(jù)庫,而一般我們在測試時經(jīng)常采用的是內(nèi)存數(shù)據(jù)庫。這里我們使用h2作為我們測試場景中使用的數(shù)據(jù)庫。
要使用h2也很簡單,只需要跟使用mysql時配置一下即可。在application.yml文件中:
1
2
3
4
5
6
7
8
9
10
11
|
--- spring: profiles: test # 數(shù)據(jù)庫配置 datasource: url: jdbc:h2:mem:test username: root password: 123456 driver- class -name: org.h2.driver schema: classpath:schema.sql data: classpath:data.sql |
為了避免和默認(rèn)的配置沖突,我們用---另起一段,并且用profiles: test表明這是test環(huán)境下的配置。然后只要在我們的測試類中加上@activeprofiles(profiles = "test")注解來啟用test環(huán)境下的配置,這樣就能一鍵從mysql數(shù)據(jù)庫切換到h2數(shù)據(jù)庫。
在上述配置中,schema.sql用于存放我們的建表語句,data.sql用于存放insert的數(shù)據(jù)。這樣當(dāng)我們測試時,h2就會讀取這兩個文件,初始化我們所需要的表結(jié)構(gòu)以及數(shù)據(jù),然后在測試結(jié)束時銷毀,不會對我們的mysql數(shù)據(jù)庫產(chǎn)生任何影響。這就是內(nèi)存數(shù)據(jù)庫的好處。另外,別忘了在pom.xml中將h2的依賴的scope設(shè)置為test。
使用spring boot就是這么簡單,無需修改任何代碼,輕松完成數(shù)據(jù)庫在不同環(huán)境下的切換。
編寫測試代碼
因為我們是通過spring initializer初始化的項目,所以已經(jīng)有了一個測試類——springbootmybatiswithredisapplicationtests。
spring boot提供了一些方便我們進(jìn)行web接口測試的工具類,比如testresttemplate。然后在配置文件中我們將log等級調(diào)成debug,方便觀察調(diào)試日志。具體的測試代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@runwith (springrunner. class ) @springboottest (webenvironment = springboottest.webenvironment.random_port) @activeprofiles (profiles = "test" ) public class springbootmybatiswithredisapplicationtests { @localserverport private int port; @autowired private testresttemplate resttemplate; @test public void test() { long productid = 1 ; product product = resttemplate.getforobject( "http://localhost:" + port + "/product/" + productid, product. class ); assertthat(product.getprice()).isequalto( 200 ); product newproduct = new product(); long newprice = new random().nextlong(); newproduct.setname( "new name" ); newproduct.setprice(newprice); resttemplate.put( "http://localhost:" + port + "/product/" + productid, newproduct); product testproduct = resttemplate.getforobject( "http://localhost:" + port + "/product/" + productid, product. class ); assertthat(testproduct.getprice()).isequalto(newprice); } } |
在上述測試代碼中:
我們首先調(diào)用get接口,通過assert語句判斷是否得到了預(yù)期的對象。此時該product對象會存入redis中。
然后我們調(diào)用put接口更新該product對象,此時redis緩存會失效。
最后我們再次調(diào)用get接口,判斷是否獲取到了新的product對象。如果獲取到老的對象,說明緩存失效的代碼執(zhí)行失敗,代碼存在錯誤,反之則說明我們代碼是ok的。
書寫單元測試是一個良好的編程習(xí)慣。雖然會占用你一定的時間,但是當(dāng)你日后需要做一些重構(gòu)工作時,你就會感激過去寫過單元測試的自己。
查看測試結(jié)果
我們在intellij中點擊執(zhí)行測試用例,測試結(jié)果如下:
顯示的是綠色,說明測試用例執(zhí)行成功了。
總結(jié)
本篇文章介紹了如何通過spring boot、mybatis以及redis快速搭建一個現(xiàn)代化的web項目,并且同時介紹了如何在spring boot下優(yōu)雅地書寫單元測試來保證我們的代碼質(zhì)量。當(dāng)然這個項目還存在一個問題,那就是mybatis的二級緩存只能通過flush整個db來實現(xiàn)緩存失效,這個時候可能會把一些不需要失效的緩存也給失效了,所以具有一定的局限性。
原文鏈接:http://www.roncoo.com/article/detail/131302