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/…
欢迎评论和转载~
订阅下方微信公众号,获取第一手资讯!