背景
springboot因其提供了各种开箱即用的插件,使得它成为了当今最为主流的java web开发框架之一。mybatis是一个十分轻量好用的orm框架。redis是当今十分主流的分布式key-value型数据库,在web开发中,我们常用它来缓存数据库的查询结果。
本篇博客将介绍如何使用springboot快速搭建一个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。该类的代码十分简单:
1
2
3
4
5
6
|
@springbootapplication
public class springbootmybatiswithredisapplication {
public static void main(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:
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内部使用spring mvc作为它的web组件,所以我们可以通过注解的方式快速开发我们的接口类:
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的组合注解,便于我们开发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文件:
1
2
3
4
5
6
7
|
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的相关配置:
1
2
3
4
5
6
7
|
# mybatis配置
mybatis:
# 配置映射类所在包名
type-aliases- package : com.wooyoo.learning.dao.domain
# 配置mapper xml文件所在路径,这里是一个数组
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之所以这么流行,最大的原因是它自动配置的特性。开发者只需要关注组件的配置(比如数据库的连接信息),而无需关心如何初始化各个组件,这使得我们可以集中精力专注于业务的实现,简化开发流程。
访问数据库
完成了mybatis的配置之后,我们就可以在我们的接口中访问数据库了。我们在productcontroller下通过@autowired引入mapper类,并且调用对应的方法实现对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的信息,就可以运行该项目看看是否能够查询成功了。
至此,我们已经成功地在项目中集成了mybatis,增添了与数据库交互的能力。但是这还不够,一个现代化的web项目,肯定会上缓存加速我们的数据库查询。接下来,将介绍如何科学地将redis集成到mybatis的二级缓存中,实现数据库查询的自动缓存。
集成redis
配置redis
同访问数据库一样,我们需要配置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数据库索引(默认为 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接口:
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;
}
}
|
讲解一下上述代码中一些关键点:
自己实现的二级缓存,必须要有一个带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中开启二级缓存:
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语句中,我们设置flushcache为true,这样在更新product信息时,能够自动失效缓存(本质上调用的是clear方法)。
测试
配置h2内存数据库
至此我们已经完成了所有代码的开发,接下来我们需要书写单元测试代码来测试我们代码的质量。我们刚才开发的过程中采用的是mysql数据库,而一般我们在测试时经常采用的是内存数据库。这里我们使用h2作为我们测试场景中使用的数据库。
要使用h2也很简单,只需要跟使用mysql时配置一下即可。在application.yml文件中:
1
2
3
4
5
6
7
8
9
10
11
|
---
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,方便观察调试日志。具体的测试代码如下:
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);
}
}
|
在上述测试代码中:
我们首先调用get接口,通过assert语句判断是否得到了预期的对象。此时该product对象会存入redis中。
然后我们调用put接口更新该product对象,此时redis缓存会失效。
最后我们再次调用get接口,判断是否获取到了新的product对象。如果获取到老的对象,说明缓存失效的代码执行失败,代码存在错误,反之则说明我们代码是ok的。
书写单元测试是一个良好的编程习惯。虽然会占用你一定的时间,但是当你日后需要做一些重构工作时,你就会感激过去写过单元测试的自己。
查看测试结果
我们在intellij中点击执行测试用例,测试结果如下:
显示的是绿色,说明测试用例执行成功了。
总结
本篇文章介绍了如何通过spring boot、mybatis以及redis快速搭建一个现代化的web项目,并且同时介绍了如何在spring boot下优雅地书写单元测试来保证我们的代码质量。当然这个项目还存在一个问题,那就是mybatis的二级缓存只能通过flush整个db来实现缓存失效,这个时候可能会把一些不需要失效的缓存也给失效了,所以具有一定的局限性。
原文链接:http://www.roncoo.com/article/detail/131302