基于SpringBoot搭建应用开发框架3

387 阅读4分钟

书接上回 基于SpringBoot搭建应用开发框架2

七、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命令和数据类型可参考我的学习笔记:

Redis 学习(一) —— 安装、通用key操作命令

Redis 学习(二) —— 数据类型及操作

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技术,开发用户登录及权限控制等。