Spring-Boot因其提供了各類開箱即用的插件,使得它成爲了當今最爲主流的Java Web開發框架之一。Mybatis是一個十分輕量好用的ORM框架。Redis是當今十分主流的分佈式key-value型數據庫,在web開發中,咱們經常使用它來緩存數據庫的查詢結果。php
本篇博客將介紹如何使用Spring-Boot快速搭建一個Web應用,而且採用Mybatis做爲咱們的ORM框架。爲了提高性能,咱們將Redis做爲Mybatis的二級緩存。爲了測試咱們的代碼,咱們編寫了單元測試,而且用H2內存數據庫來生成咱們的測試數據。經過該項目,咱們但願讀者能夠快速掌握現代化Java Web開發的技巧以及最佳實踐。html
本文的示例代碼可在Github中下載:github.com/Lovelcp/spr…java
首先,咱們須要初始化咱們的Spring-Boot工程。經過Intellij的Spring Initializer,新建一個Spring-Boot工程變得十分簡單。首先咱們在Intellij中選擇New一個Project:mysql
而後在選擇依賴的界面,勾選Web、Mybatis、Redis、Mysql、H2:git
新建工程成功以後,咱們能夠看到項目的初始結構以下圖所示:github
Spring Initializer已經幫咱們自動生成了一個啓動類——SpringBootMybatisWithRedisApplication
。該類的代碼十分簡單:web
@SpringBootApplication
public class SpringBootMybatisWithRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisWithRedisApplication.class, args);
}
}複製代碼
@SpringBootApplication
註解表示啓用Spring Boot的自動配置特性。好了,至此咱們的項目骨架已經搭建成功,感興趣的讀者能夠經過Intellij啓動看看效果。redis
接下來,咱們要編寫Web API。假設咱們的Web工程負責處理商家的產品(Product)。咱們須要提供根據product id返回product信息的get接口和更新product信息的put接口。首先咱們定義Product類,該類包括產品id,產品名稱name以及價格price:spring
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內部使用Spring MVC做爲它的Web組件,因此咱們能夠經過註解的方式快速開發咱們的接口類:sql
@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
的組合註解,便於咱們開發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,而且與數據庫作交互。
首先咱們須要在配置文件中配置咱們的數據源。咱們採用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格式的緣由。
咱們已經經過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
public interface ProductMapper {
Product select( @Param("id") long id);
void update(Product product);
}複製代碼
這裏,只要咱們加上了@Mapper
註解,Spring Boot在初始化mybatis時會自動加載該mapper類。
Spring Boot之因此這麼流行,最大的緣由是它自動配置的特性。開發者只須要關注組件的配置(好比數據庫的鏈接信息),而無需關心如何初始化各個組件,這使得咱們能夠集中精力專一於業務的實現,簡化開發流程。
完成了Mybatis的配置以後,咱們就能夠在咱們的接口中訪問數據庫了。咱們在ProductController
下經過@Autowired
引入mapper類,而且調用對應的方法實現對product的查詢和更新操做,這裏咱們以查詢接口爲例:
@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的信息,就能夠運行該項目看看是否可以查詢成功了。
至此,咱們已經成功地在項目中集成了Mybatis,增添了與數據庫交互的能力。可是這還不夠,一個現代化的Web項目,確定會上緩存加速咱們的數據庫查詢。接下來,將介紹如何科學地將Redis集成到Mybatis的二級緩存中,實現數據庫查詢的自動緩存。
同訪問數據庫同樣,咱們須要配置Redis的鏈接信息。在application.yml文件中增長以下配置:
spring:
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
。
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
接口:
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;
}
}複製代碼
講解一下上述代碼中一些關鍵點:
redisTemplate
來操做Redis。網上全部介紹redis作二級緩存的文章都是直接用jedis庫,可是筆者認爲這樣不夠Spring Style,並且,redisTemplate
封裝了底層的實現,將來若是咱們不用jedis了,咱們能夠直接更換底層的庫,而不用修改上層的代碼。更方便的是,使用redisTemplate
,咱們不用關心redis鏈接的釋放問題,不然新手很容易忘記釋放鏈接而致使應用卡死。redisTemplate
,由於RedisCache
並非Spring容器裏的bean。因此咱們須要手動地去調用容器的getBean
方法來拿到這個bean,具體的實現方式請參考Github中的代碼。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">
<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語句中,咱們設置flushCache
爲true
,這樣在更新product信息時,可以自動失效緩存(本質上調用的是clear方法)。
至此咱們已經完成了全部代碼的開發,接下來咱們須要書寫單元測試代碼來測試咱們代碼的質量。咱們剛纔開發的過程當中採用的是mysql數據庫,而通常咱們在測試時常常採用的是內存數據庫。這裏咱們使用H2做爲咱們測試場景中使用的數據庫。
要使用H2也很簡單,只須要跟使用mysql時配置一下便可。在application.yml文件中:
---
spring:
profiles: test
# 數據庫配置
datasource:
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")
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);
}
}複製代碼
在上述測試代碼中:
書寫單元測試是一個良好的編程習慣。雖然會佔用你必定的時間,可是當你往後須要作一些重構工做時,你就會感激過去寫過單元測試的本身。
咱們在Intellij中點擊執行測試用例,測試結果以下:
真棒,顯示的是綠色,說明測試用例執行成功了。
本篇文章介紹瞭如何經過Spring Boot、Mybatis以及Redis快速搭建一個現代化的Web項目,而且同時介紹瞭如何在Spring Boot下優雅地書寫單元測試來保證咱們的代碼質量。固然這個項目還存在一個問題,那就是mybatis的二級緩存只能經過flush整個DB來實現緩存失效,這個時候可能會把一些不須要失效的緩存也給失效了,因此具備必定的侷限性。
但願經過本篇文章,可以給讀者帶來一些收穫和啓發。有任何的意見或者建議請在本文下方評論。謝謝你們的閱讀,祝你們端午節快樂!!!
本文首發於www.kissyu.org/2017/05/29/…
歡迎評論和轉載~
訂閱下方微信公衆號,獲取第一手資訊!