背景
Spring-Boot因其提供了各種開箱即用的插件,使得它成為了當今最為主流的Java Web開發架構之一。Mybatis是一個十分輕量好用的ORM架構。Redis是當今十分主流的分布式key-value型資料庫,在web開發中,我們常用它來緩存資料庫的查詢結果。
本篇部落格将介紹如何使用Spring-Boot快速搭建一個Web應用,并且采用Mybatis作為我們的ORM架構。為了提升性能,我們将Redis作為Mybatis的二級緩存。為了測試我們的代碼,我們編寫了單元測試,并且用H2記憶體資料庫來生成我們的測試資料。通過該項目,我們希望讀者可以快速掌握現代化Java Web開發的技巧以及最佳實踐。
本文的示例代碼可在Github中下載下傳: https://github.com/Lovelcp/spring-boot-mybatis-with-redis/tree/master 環境開發環境: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:
建立工程成功之後,我們可以看到項目的初始結構如下圖所示:
Spring Initializer已經幫我們自動生成了一個啟動類——SpringBootMybatisWithRedisApplication。該類的代碼十分簡單:
@SpringBootApplication
publicclassSpringBootMybatisWithRedisApplication{
publicstaticvoidmain(String[] args){
SpringApplication.run(SpringBootMybatisWithRedisApplication.class, args);
}
}
@SpringBootApplication注解表示啟用Spring Boot的自動配置特性。好了,至此我們的項目骨架已經搭建成功,感興趣的讀者可以通過Intellij啟動看看效果。
建立API接口接下來,我們要編寫Web API。假設我們的Web工程負責處理商家的産品(Product)。我們需要提供根據product id傳回product資訊的get接口和更新product資訊的put接口。首先我們定義Product類,該類包括産品id,産品名稱name以及價格price:
publicclassProductimplementsSerializable{
privatestaticfinallongserialVersionUID =1435515995276255188L;
privatelongid;
privateString name;
privatelongprice;
// getters setters
然後我們需要定義Controller類。由于Spring Boot内部使用Spring MVC作為它的Web元件,是以我們可以通過注解的方式快速開發我們的接口類:
@RestController
@RequestMapping("/product")
publicclassProductController{
@GetMapping("/{id}")
publicProductgetProductInfo(
@PathVariable("id")
Long productId) {
// TODO
returnnull;
}
@PutMapping("/{id}")
publicProductupdateProductInfo(
Long productId,
@RequestBody
Product newProduct) {
我們簡單介紹一下上述代碼中所用到的注解的作用:
@RestController:表示該類為Controller,并且提供Rest接口,即所有接口的值以Json格式傳回。該注解其實是@Controller和@ResponseBody的組合注解,便于我們開發Rest API。
@RequestMapping、@GetMapping、@PutMapping:表示接口的URL位址。标注在類上的@RequestMapping注解表示該類下的所有接口的URL都以/product開頭。@GetMapping表示這是一個Get HTTP接口,@PutMapping表示這是一個Put HTTP接口。
@PathVariable、@RequestBody:表示參數的映射關系。假設有個Get請求通路的是/product/123,那麼該請求會由getProductInfo方法處理,其中URL裡的123會被映射到productId中。同理,如果是Put請求的話,請求的body會被映射到newProduct對象中。
這裡我們隻定義了接口,實際的處理邏輯還未完成,因為product的資訊都存在資料庫中。接下來我們将在項目中內建mybatis,并且與資料庫做互動。
內建Mybatis 配置資料源首先我們需要在配置檔案中配置我們的資料源。我們采用mysql作為我們的資料庫。這裡我們采用yaml作為我們配置檔案的格式。我們在resources目錄下建立application.yml檔案:
spring:
# 資料庫配置
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會自動加載配置檔案并且根據配置檔案的資訊建立資料庫的連接配接池,十分便捷。
筆者推薦大家采用yaml作為配置檔案的格式。xml顯得冗長,properties沒有層級結構,yaml剛好彌補了這兩者的缺點。這也是Spring Boot預設就支援yaml格式的原因。
配置Mybatis我們已經通過Spring Initializer在pom.xml中引入了mybatis-spring-boot-starte庫,該庫會自動幫我們初始化mybatis。首先我們在application.yml中填寫mybatis的相關配置:
# mybatis配置
mybatis:
# 配置映射類所在包名
type-aliases-package:com.wooyoo.learning.dao.domain
# 配置mapper xml檔案所在路徑,這裡是一個數組
mapper-locations:
-mappers/ProductMapper.xml
然後,再在代碼中定義ProductMapper類:
@Mapper
publicinterfaceProductMapper{
Productselect(
@Param("id")
longid);
voidupdate(Product product);
這裡,隻要我們加上了@Mapper注解,Spring Boot在初始化mybatis時會自動加載該mapper類。
Spring Boot之是以這麼流行,最大的原因是它自動配置的特性。開發者隻需要關注元件的配置(比如資料庫的連接配接資訊),而無需關心如何初始化各個元件,這使得我們可以集中精力專注于業務的實作,簡化開發流程。
通路資料庫完成了Mybatis的配置之後,我們就可以在我們的接口中通路資料庫了。我們在ProductController下通過@Autowired引入mapper類,并且調用對應的方法實作對product的查詢和更新操作,這裡我們以查詢接口為例:
@Autowired
privateProductMapper productMapper;
returnproductMapper.select(productId);
// 避免篇幅過長,省略updateProductInfo的代碼
然後在你的mysql中插入幾條product的資訊,就可以運作該項目看看是否能夠查詢成功了。
至此,我們已經成功地在項目中內建了Mybatis,增添了與資料庫互動的能力。但是這還不夠,一個現代化的Web項目,肯定會上緩存加速我們的資料庫查詢。接下來,将介紹如何科學地将Redis內建到Mybatis的二級緩存中,實作資料庫查詢的自動緩存。
內建Redis 配置Redis同通路資料庫一樣,我們需要配置Redis的連接配接資訊。在application.yml檔案中增加如下配置:
redis:
# redis資料庫索引(預設為0),我們使用索引為3的資料庫,避免和其他資料庫沖突
database:3
# redis伺服器位址(預設為localhost)
host:localhost
# redis端口(預設為6379)
port:6379
# redis通路密碼(預設為空)
password:
# redis連接配接逾時時間(機關為毫秒)
timeout:0
# redis連接配接池配置
pool:
# 最大可用連接配接數(預設為8,負數表示無限)
max-active:8
# 最大空閑連接配接數(預設為8,負數表示無限)
max-idle:8
# 最小空閑連接配接數(預設為0,該值隻有為正數才有作用)
min-idle:0
# 從連接配接池中擷取連接配接最大等待時間(預設為-1,機關為毫秒,負數表示無限)
max-wait:-1
上述列出的都為常用配置,讀者可以通過注釋資訊了解每個配置項的具體作用。由于我們在pom.xml中已經引入了spring-boot-starter-data-redis庫,是以Spring Boot會幫我們自動加載Redis的連接配接,具體的配置類org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration。通過該配置類,我們可以發現底層預設使用Jedis庫,并且提供了開箱即用的redisTemplate和stringTemplate。
将Redis作為二級緩存
Mybatis的二級緩存原理本文不再贅述,讀者隻要知道,Mybatis的二級緩存可以自動地對資料庫的查詢做緩存,并且可以在更新資料時同時自動地更新緩存。
實作Mybatis的二級緩存很簡單,隻需要建立一個類實作org.apache.ibatis.cache.Cache接口即可。
該接口共有以下五個方法:
String getId():mybatis緩存操作對象的辨別符。一個mapper對應一個mybatis的緩存操作對象。
void putObject(Object key, Object value):将查詢結果塞入緩存。
Object getObject(Object key):從緩存中擷取被緩存的查詢結果。
Object removeObject(Object key):從緩存中删除對應的key、value。隻有在復原時觸發。一般我們也可以不用實作,具體使用方式請參考:org.apache.ibatis.cache.decorators.TransactionalCache。
void clear():發生更新時,清除緩存。
int getSize():可選實作。傳回緩存的數量。
ReadWriteLock getReadWriteLock():可選實作。用于實作原子性的緩存操作。
接下來,我們建立RedisCache類,實作Cache接口:
publicclassRedisCacheimplementsCache{
privatestaticfinalLogger logger = LoggerFactory.getLogger(RedisCache.class);
privatefinalReadWriteLock readWriteLock =newReentrantReadWriteLock();
privatefinalString id;// cache instance id
privateRedisTemplate redisTemplate;
privatestaticfinallongEXPIRE_TIME_IN_MINUTES =30;// redis過期時間
publicRedisCache(String id){
if(id ==null) {
thrownewIllegalArgumentException("Cache instances require an ID");
}
this.id = id;
@Override
publicStringgetId(){
returnid;
/**
* Put query result to redis
*
*@paramkey
*@paramvalue
*/
@SuppressWarnings("unchecked")
publicvoidputObject(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
*@return
publicObjectgetObject(Object key){
logger.debug("Get cached query result from redis");
returnopsForValue.get(key);
* Remove cached query result from redis
publicObjectremoveObject(Object key){
redisTemplate.delete(key);
logger.debug("Remove cached query result from redis");
* Clears this cache instance
publicvoidclear(){
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
});
logger.debug("Clear all the cached query result from redis");
publicintgetSize(){
return0;
publicReadWriteLockgetReadWriteLock(){
returnreadWriteLock;
privateRedisTemplategetRedisTemplate(){
if(redisTemplate ==null) {
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
returnredisTemplate;
講解一下上述代碼中一些
關鍵點:
自己實作的二級緩存,必須要有一個帶id的構造函數,否則會報錯。
我們使用Spring封裝的redisTemplate來操作Redis。
網上所有介紹redis做二級緩存的文章都是直接用jedis庫,但是筆者認為這樣不夠Spring Style,而且,redisTemplate封裝了底層的實作,未來如果我們不用jedis了,我們可以直接更換底層的庫,而不用修改上層的代碼。更友善的是,使用redisTemplate,我們不用關心redis連接配接的釋放問題,否則新手很容易忘記釋放連接配接而導緻應用卡死。需要注意的是,這裡不能通過autowire的方式引用redisTemplate,因為RedisCache并不是Spring容器裡的bean。是以我們需要手動地去調用容器的getBean方法來拿到這個bean,具體的實作方式請參考
Github中的代碼。
我們采用的redis序列化方式是預設的jdk序列化。是以資料庫的查詢對象(比如Product類)需要實作Serializable接口。
這樣,我們就實作了一個優雅的、科學的并且具有Spring Style的Redis緩存類。
開啟二級緩存
接下來,我們需要在ProductMapper.xml中開啟二級緩存:
<?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">
<!-- 開啟基于redis的二級緩存 -->
SELECT * FROM products WHERE id = #{id} LIMIT 1
UPDATE products SET name = #{name}, price = #{price} WHERE id = #{id} LIMIT 1
<cache type="com.wooyoo.learning.util.RedisCache"/>表示開啟基于redis的二級緩存,并且在update語句中,我們設定flushCache為true,這樣在更新product資訊時,能夠自動失效緩存(本質上調用的是clear方法)。
測試
配置H2記憶體資料庫至此我們已經完成了所有代碼的開發,接下來我們需要書寫單元測試代碼來測試我們代碼的品質。我們剛才開發的過程中采用的是mysql資料庫,而一般我們在測試時經常采用的是記憶體資料庫。這裡我們使用H2作為我們測試場景中使用的資料庫。
要使用H2也很簡單,隻需要跟使用mysql時配置一下即可。在application.yml檔案中:
---
profiles:test
url:jdbc:h2:mem:test
username:root
password:123456
driver-class-name:org.h2.Driver
schema:classpath:schema.sql
data:classpath:data.sql
為了避免和預設的配置沖突,我們用---另起一段,并且用profiles: test表明這是test環境下的配置。然後隻要在我們的測試類中加上@ActiveProfiles(profiles = "test")注解來啟用test環境下的配置,這樣就能一鍵從mysql資料庫切換到h2資料庫。
在上述配置中,schema.sql用于存放我們的建表語句,data.sql用于存放insert的資料。這樣當我們測試時,h2就會讀取這兩個檔案,初始化我們所需要的表結構以及資料,然後在測試結束時銷毀,不會對我們的mysql資料庫産生任何影響。這就是記憶體資料庫的好處。另外,别忘了在pom.xml中将h2的依賴的scope設定為test。
使用Spring Boot就是這麼簡單,無需修改任何代碼,輕松完成資料庫在不同環境下的切換。
編寫測試代碼
因為我們是通過Spring Initializer初始化的項目,是以已經有了一個測試類——SpringBootMybatisWithRedisApplicationTests。
Spring Boot提供了一些友善我們進行Web接口測試的工具類,比如TestRestTemplate。然後在配置檔案中我們将log等級調成DEBUG,友善觀察調試日志。具體的測試代碼如下:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(profiles ="test")
publicclassSpringBootMybatisWithRedisApplicationTests{
@LocalServerPort
privateintport;
privateTestRestTemplate restTemplate;
@Test
publicvoidtest(){
longproductId =1;
Product product = restTemplate.getForObject("http://localhost:"+ port +"/product/"+ productId, Product.class);
assertThat(product.getPrice()).isEqualTo(200);
Product newProduct =newProduct();
longnewPrice =newRandom().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);
在上述測試代碼中:
我們首先調用get接口,通過assert語句判斷是否得到了預期的對象。此時該product對象會存入redis中。
然後我們調用put接口更新該product對象,此時redis緩存會失效。
最後我們再次調用get接口,判斷是否擷取到了新的product對象。如果擷取到老的對象,說明緩存失效的代碼執行失敗,代碼存在錯誤,反之則說明我們代碼是OK的。
書寫單元測試是一個良好的程式設計習慣。雖然會占用你一定的時間,但是當你日後需要做一些重構工作時,你就會感激過去寫過單元測試的自己。
檢視測試結果
我們在Intellij中點選執行測試用例,測試結果如下:
真棒,顯示的是綠色,說明測試用例執行成功了。
總結
本篇文章介紹了如何通過Spring Boot、Mybatis以及Redis快速搭建一個現代化的Web項目,并且同時介紹了如何在Spring Boot下優雅地書寫單元測試來保證我們的代碼品質。當然這個項目還存在一個問題,那就是mybatis的二級緩存隻能通過flush整個DB來實作緩存失效,這個時候可能會把一些不需要失效的緩存也給失效了,是以具有一定的局限性。