七、Redis缓存
对于如今的一个中小型系统来说,至少也需要一个缓存来缓存热点数据,加快数据的访问数据,这里选用Redis做缓存数据库。在以后可以使用Redis做分布式缓存、做Session共享等。
1、SpringBoot的缓存支持
Spring定义了org.springframework.cache.CacheManager和org.springframework.cache.Cache接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口,Cache接口包含缓存的各种操作。
针对不同的缓存技术,需要实现不同的CacheManager,Redis缓存则提供了RedisCacheManager的实现。
我将redis缓存功能放到sunny-starter-cache模块下,cache模块下可以有多种缓存技术,同时,对于其它项目来说,缓存是可插拔的,想用缓存直接引入cache模块即可。
首先引入Redis的依赖:
SpringBoot已经默认为我们自动配置了多个CacheManager的实现,在autoconfigure.cache包下。在Spring Boot 环境下,使用缓存技术只需在项目中导入相关的依赖包即可。
在 RedisCacheConfiguration 里配置了默认的 CacheManager;SpringBoot提供了默认的redis配置,RedisAutoConfiguration 是Redis的自动化配置,比如创建连接池、初始化RedisTemplate等。
2、Redis 配置及声明式缓存支持
Redis 默认配置了 RedisTemplate 和 StringRedisTemplate ,其使用的序列化规则是 JdkSerializationRedisSerializer,缓存到redis后,数据都变成了下面这种样式,非常不易于阅读。
因此,重新配置RedisTemplate,使用 Jackson2JsonRedisSerializer 来序列化 Key 和 Value。同时,增加HashOperations、ValueOperations等Redis数据结构相关的操作,这样比较方便使用。
package com.lyyzoo.cache.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Redis配置.
*
* 使用@EnableCaching开启声明式缓存支持. 之后就可以使用 @Cacheable/@CachePut/@CacheEvict 注解缓存数据.
*
* @author bojiangzhou 2018-02-11
* @version 1.0
*/
@Configuration
@EnableCaching
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;
/**
* 覆盖默认配置 RedisTemplate,使用 String 类型作为key,设置key/value的序列化规则
*/
@Bean
@SuppressWarnings("unchecked")
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper(false).build();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和key的序列化规则
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
cacheManager.setUsePrefix(true);
return cacheManager;
}
}
同时,使用@EnableCaching开启声明式缓存支持,这样就可以使用基于注解的缓存技术。注解缓存是一个对缓存使用的抽象,通过在代码中添加下面的一些注解,达到缓存的效果。
- @Cacheable:在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;没有则调用方法并将方法返回值放进缓存。
- @CachePut:将方法的返回值放到缓存中。
- @CacheEvict:删除缓存中的数据。
Redis服务器相关的一些配置可在application.properties中进行配置:
3、Redis工具类
添加一个Redis的统一操作工具,主要是对redis的常用数据类型操作类做了一个归集。
ValueOperations用于操作String类型,HashOperations用于操作hash数据,ListOperations操作List集合,SetOperations操作Set集合,ZSetOperations操作有序集合。
关于redis的key命令和数据类型可参考我的学习笔记:
package com.lyyzoo.cache.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Redis 操作工具
*
* @version 1.0
* @author bojiangzhou 2018-02-12
*/
@Component
public class RedisOperator {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ValueOperations<String, String> valueOperator;
@Autowired
private HashOperations<String, String, Object> hashOperator;
@Autowired
private ListOperations<String, Object> listOperator;
@Autowired
private SetOperations<String, Object> setOperator;
@Autowired
private ZSetOperations<String, Object> zSetOperator;
/**
* 默认过期时长,单位:秒
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
/** 不设置过期时长 */
public final static long NOT_EXPIRE = -1;
/**
* Redis的根操作路径
*/
@Value("${redis.root:sunny}")
private String category;
public RedisOperator setCategory(String category) {
this.category = category;
return this;
}
/**
* 获取Key的全路径
*
* @param key key
* @return full key
*/
public String getFullKey(String key) {
return this.category + ":" + key;
}
//
// key
// ------------------------------------------------------------------------------
/**
* 判断key是否存在
*
* <p>
* <i>exists key</i>
*
* @param key key
*/
public boolean existsKey(String key) {
return redisTemplate.hasKey(getFullKey(key));
}
/**
* 判断key存储的值类型
*
* <p>
* <i>type key</i>
*
* @param key key
* @return DataType[string、list、set、zset、hash]
*/
public DataType typeKey(String key){
return redisTemplate.type(getFullKey(key));
}
/**
* 重命名key. 如果newKey已经存在,则newKey的原值被覆盖
*
* <p>
* <i>rename oldKey newKey</i>
*
* @param oldKey oldKeys
* @param newKey newKey
*/
public void renameKey(String oldKey, String newKey){
redisTemplate.rename(getFullKey(oldKey), getFullKey(newKey));
}
/**
* newKey不存在时才重命名.
*
* <p>
* <i>renamenx oldKey newKey</i>
*
* @param oldKey oldKey
* @param newKey newKey
* @return 修改成功返回true
*/
public boolean renameKeyNx(String oldKey, String newKey){
return redisTemplate.renameIfAbsent(getFullKey(oldKey), getFullKey(newKey));
}
/**
* 删除key
*
* <p>
* <i>del key</i>
*
* @param key key
*/
public void deleteKey(String key){
redisTemplate.delete(key);
}
/**
* 删除key
*
* <p>
* <i>del key1 key2 ...</i>
*
* @param keys 可传入多个key
*/
public void deleteKey(String ... keys){
Set<String> ks = Stream.of(keys).map(k -> getFullKey(k)).collect(Collectors.toSet());
redisTemplate.delete(ks);
}
/**
* 删除key
*
* <p>
* <i>del key1 key2 ...</i>
*
* @param keys key集合
*/
public void deleteKey(Collection<String> keys){
Set<String> ks = keys.stream().map(k -> getFullKey(k)).collect(Collectors.toSet());
redisTemplate.delete(ks);
}
/**
* 设置key的生命周期,单位秒
*
* <p>
* <i>expire key seconds</i><br>
* <i>pexpire key milliseconds</i>
*
* @param key key
* @param time 时间数
* @param timeUnit TimeUnit 时间单位
*/
public void expireKey(String key, long time, TimeUnit timeUnit){
redisTemplate.expire(key, time, timeUnit);
}
/**
* 设置key在指定的日期过期
*
* <p>
* <i>expireat key timestamp</i>
*
* @param key key
* @param date 指定日期
*/
public void expireKeyAt(String key, Date date){
redisTemplate.expireAt(key, date);
}
/**
* 查询key的生命周期
*
* <p>
* <i>ttl key</i>
*
* @param key key
* @param timeUnit TimeUnit 时间单位
* @return 指定时间单位的时间数
*/
public long getKeyExpire(String key, TimeUnit timeUnit){
return redisTemplate.getExpire(key, timeUnit);
}
/**
* 将key设置为永久有效
*
* <p>
* <i>persist key</i>
*
* @param key key
*/
public void persistKey(String key){
redisTemplate.persist(key);
}
/**
*
* @return RedisTemplate
*/
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
/**
*
* @return ValueOperations
*/
public ValueOperations<String, String> getValueOperator() {
return valueOperator;
}
/**
*
* @return HashOperations
*/
public HashOperations<String, String, Object> getHashOperator() {
return hashOperator;
}
/**
*
* @return ListOperations
*/
public ListOperations<String, Object> getListOperator() {
return listOperator;
}
/**
*
* @return SetOperations
*/
public SetOperations<String, Object> getSetOperator() {
return setOperator;
}
/**
*
* @return ZSetOperations
*/
public ZSetOperations<String, Object> getZSetOperator() {
return zSetOperator;
}
}
八、Swagger支持API文档
1、Swagger
做前后端分离,前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架。
Swagger是一个简单又强大的能为你的Restful风格的Api生成文档的工具。在项目中集成这个工具,根据我们自己的配置信息能够自动为我们生成一个api文档展示页,可以在浏览器中直接访问查看项目中的接口信息,同时也可以测试每个api接口。
2、配置
我这里直接使用别人已经整合好的swagger-spring-boot-starter,快速方便。
参考:spring-boot-starter-swagger
新建一个sunny-starter-swagger模块,做到可插拔。
根据文档,一般只需要做些简单的配置即可:
但如果想要显示swagger-ui.html文档展示页,还必须注入swagger资源:
package com.lyyzoo.swagger.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.spring4all.swagger.EnableSwagger2Doc;
/**
* @version 1.0
* @author bojiangzhou 2018-02-19
*/
@Configuration
@EnableSwagger2Doc
@PropertySource(value = "classpath:application-swagger.properties")
public class SunnySwaggerConfig extends WebMvcConfigurerAdapter {
/**
* 注入swagger资源文件
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
3、使用
一般只需要在Controller加上swagger的注解即可显示对应的文档信息,如@Api、@ApiOperation、@ApiParam等。
常用注解参考:swagger-api-annotations
package com.lyyzoo.admin.system.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.lyyzoo.admin.system.dto.Menu;
import com.lyyzoo.admin.system.service.MenuService;
import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.util.Results;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@Api(tags = "菜单管理")
@RequestMapping
@RestController
public class MenuController extends BaseController {
@Autowired
private MenuService service;
/**
* 查找单个用户
*
* @param menuId 菜单ID
* @return Result
*/
@ApiOperation("查找单个用户")
@ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path")
@GetMapping("/sys/menu/get/{menuId}")
public Result get(@PathVariable Long menuId){
Menu menu = service.selectById(menuId);
return Results.successWithData(menu);
}
/**
* 保存菜单
*
* @param menu 菜单
* @return Result
*/
@ApiOperation("保存菜单")
@PostMapping("/sys/menu/save")
public Result save(@ApiParam(name = "menu", value = "菜单")@RequestBody Menu menu){
menu = service.save(menu);
return Results.successWithData(menu);
}
/**
* 删除菜单
*
* @param menuId 菜单ID
* @return Result
*/
@ApiOperation("删除菜单")
@ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path")
@PostMapping("/sys/menu/delete/{menuId}")
public Result delete(@PathVariable Long menuId){
service.deleteById(menuId);
return Results.success();
}
}
之后访问swagger-ui.html页面就可以看到API文档信息了。
如果不需要swagger,在配置文件中配置swagger.enabled=false,或移除sunny-starter-swagger的依赖即可。
九、项目优化调整
到这里,项目最基础的一些功能就算完成了,但由于前期的一些设计不合理及未考虑周全等因素,对项目做一些调整。并参考《阿里巴巴Java开发手册》对代码做了一些优化。
1、项目结构
目前项目分为5个模块:
最外层的Sunny作为聚合模块负责管理所有子模块,方便统一构建。并且继承 spring-boot-starter-parent ,其它子模块则继承该模块,方便统一管理 Spring Boot 及本项目的版本。这里已经把Spring Boot的版本升到 1.5.10.RELEASE。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lyyzoo</groupId>
<artifactId>sunny</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Sunny</name>
<description>Lyyzoo Base Application development platform</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<sunny.version>0.0.1-SNAPSHOT</sunny.version>
<springboot.version>1.5.10.RELEASE</springboot.version>
</properties>
<modules>
<module>sunny-starter</module>
<module>sunny-starter-core</module>
<module>sunny-starter-cache</module>
<module>sunny-starter-security</module>
<module>sunny-starter-admin</module>
<module>sunny-starter-swagger</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
sunny-starter 则引入了其余几个模块,在开发项目时,只需要继承或引入sunny-starter即可,而无需一个个引入各个模块。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lyyzoo</groupId>
<artifactId>sunny</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.lyyzoo.parent</groupId>
<artifactId>sunny-starter</artifactId>
<packaging>jar</packaging>
<name>sunny-starter</name>
<description>Sunny Parent</description>
<dependencies>
<!-- core -->
<dependency>
<groupId>com.lyyzoo.core</groupId>
<artifactId>sunny-starter-core</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- cache -->
<dependency>
<groupId>com.lyyzoo.cache</groupId>
<artifactId>sunny-starter-cache</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- security -->
<dependency>
<groupId>com.lyyzoo.security</groupId>
<artifactId>sunny-starter-security</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- admin -->
<dependency>
<groupId>com.lyyzoo.admin</groupId>
<artifactId>sunny-starter-admin</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>com.lyyzoo.swagger</groupId>
<artifactId>sunny-starter-swagger</artifactId>
<version>${sunny.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
对于一个Spring Boot项目,应该只有一个入口,即 @SpringBootApplication 注解的类。经测试,其它的模块的配置文件application.properties的配置不会生效,应该是引用了入口模块的配置文件。
所以为了让各个模块的配置文件都能生效,只需使用 @PropertySource 引入该配置文件即可,每个模块都如此。在主模块定义的配置会覆盖其它模块的配置。
2、开发规范
十、结语
到此,基础架构篇结束!学习了很多新东西,如Spring Boot、Mapper、Druid;有些知识也深入地学习了,如MyBatis、Redis、日志框架、Maven等等。
在这期间,看完两本书,可参考:《MyBatis从入门到精通》、《JavaEE开发的颠覆者 Spring Boot实战》,另外,开发规范遵从《阿里巴巴Java开发手册》,其它的参考资料都在文中有体现。
紧接着,后面会完成 sunny-starter-security 模块的开发,主要使用spring-security技术,开发用户登录及权限控制等。