首頁  >  文章  >  後端開發  >  Spring Boot 、 Mybatis 、 Redis快速搭建現代化Web項目

Spring Boot 、 Mybatis 、 Redis快速搭建現代化Web項目

小云云
小云云原創
2017-12-06 09:24:012724瀏覽

SpringBoot因其提供了各種開箱即用的插件,使得它成為了當今最為主流的Java Web開發框架之一。 Mybatis是一個十分輕量好用的ORM框架。 Redis是當今十分主流的分散式key-value型資料庫,在web開發中,我們常用它來快取資料庫的查詢結果。本文將介紹如何透過Spring Boot、Mybatis以及Redis快速建立一個現代化的Web項目,並且同時介紹瞭如何在Spring Boot下優雅地書寫單元測試來保證我們的程式碼品質。

本篇部落格將介紹如何使用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。該類別的程式碼十分簡單:

@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:

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元件,我們可以透過註解的方式快速開發我們的介面類別:

@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檔案:

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
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

配置Redis

同访问数据库一样,我们需要配置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。

将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接口:

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中开启二级缓存:

<?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>

d5062ae6bf3ceaffa87bf8be4bed75d6表示开启基于redis的二级缓存,并且在update语句中,我们设置flushCache为true,这样在更新product信息时,能够自动失效缓存(本质上调用的是clear方法)。

测试

配置H2内存数据库

至此我们已经完成了所有代码的开发,接下来我们需要书写单元测试代码来测试我们代码的质量。我们刚才开发的过程中采用的是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);
 }
}

在上述测试代码中:

我们首先调用get接口,通过assert语句判断是否得到了预期的对象。此时该product对象会存入redis中。

然后我们调用put接口更新该product对象,此时redis缓存会失效。

最后我们再次调用get接口,判断是否获取到了新的product对象。如果获取到老的对象,说明缓存失效的代码执行失败,代码存在错误,反之则说明我们代码是OK的。

书写单元测试是一个良好的编程习惯。虽然会占用你一定的时间,但是当你日后需要做一些重构工作时,你就会感激过去写过单元测试的自己。

查看测试结果

我们在Intellij中点击执行测试用例,测试结果如下:

显示的是绿色,说明测试用例执行成功了。

本篇文章介绍了如何通过Spring Boot、Mybatis以及Redis快速搭建一个现代化的Web项目,并且同时介绍了如何在Spring Boot下优雅地书写单元测试来保证我们的代码质量。当然这个项目还存在一个问题,那就是mybatis的二级缓存只能通过flush整个DB来实现缓存失效,这个时候可能会把一些不需要失效的缓存也给失效了,所以具有一定的局限性。希望本文能帮助到大家。

相关推荐:

Tomcat部署Web项目该如何实现?

微信开发之Maven仓库管理及新建WEB项目的步骤详解

VS中新建网站和新建WEB项目的区别

以上是Spring Boot 、 Mybatis 、 Redis快速搭建現代化Web項目的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn