文章目录

  • 创建Spring Boot工程
  • 引入依赖库和插件
  • 编写配置文件
  • Spring Boot 配置文件
  • 日志配置文件
  • API 文档生成插件配置文件
  • 设计项目包结构
  • 编写 Java 配置类
  • 程序入口加注解 `@EnableCaching`
  • 配置 Mybatis-Plus
  • 配置跨域
  • 配置 Redis
  • 配置 Sa-Token 拦截器
  • 统一后端返回格式
  • 返回码定义
  • 返回结果统一封装
  • 返回结果统一处理
  • 全局异常拦截
  • 引入工具类
  • 操作 Redis 的 RedisUtil
  • 连接 COS 的工具类
  • 实现权限认证功能
  • 编写实体
  • 根据实体编写对应的ORM层和Service层
  • 实现 Sa-Token 的权限认证接口
  • 实现 Redis 缓存功能
  • 实体和对象映射
  • Service 集成 Redis 缓存机制
  • 编写单元测试


在学spring boot的过程中慢慢整合常用的组件,想着汇总成一个项目,方便下次开发拿来即用。

创建Spring Boot工程

使用 idea 的 Spring Initializr 创建一个 Spring Boot 工程,这一步全部默认即可。

SpringBoot集成Flowable modeler设计器 springboot组件集成_mybatis

引入依赖库和插件

pom.xml 里的 dependenciesbuild 添加以下内容,刷新等待依赖全部导进来即可。

<!-- pom.xml -->
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.28.0</version>
        </dependency>
        <dependency>
            <groupId>com.qcloud</groupId>
            <artifactId>cos_api</artifactId>
            <version>5.6.61</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.github.shalousun</groupId>
                <artifactId>smart-doc-maven-plugin</artifactId>
                <version>2.2.9</version>
                <configuration>
                    <configFile>./src/main/resources/smart-doc.json</configFile>
                </configuration>
            </plugin>
        </plugins>
    </build>

编写配置文件

Spring Boot 配置文件

我习惯使用 yml 文件格式,可以右键默认的 properties 文件把后缀名改成 yml

SpringBoot集成Flowable modeler设计器 springboot组件集成_java_02

然后填入以下内容

# application.yml
server:
  port: 8088
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    hikari:
      minimum-idle: 1
      maximum-pool-size: 20
      initialization-fail-timeout: 60000
  jpa:
    show-sql: false
    database-platform: org.hibernate.dialect.MySQL8Dialect
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
  redis:
    host: localhost
    port: 6379
    database: 1
    password:
    lettuce:
      pool:
        max-idle: 8
        max-active: 8
        max-wait: -1ms
        min-idle: 0
sa-token:
  token-name: satoken
  timeout: 86400
  activity-timeout: -1
  is-share: false
  token-style: uuid
  is-log: true

这里自行修改 MySQL 和 Redis 配置

日志配置文件

我们使用 log4j2 作为我们的日志框架,虽然说最近曝出来有大漏洞,但对于我们来说问题不大,学生跟着学就行了,可以不管这些安全性问题。

/resources 目录下新建 log4j2.xml 文件,填入以下内容

<!-- log4j2.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration monitorInterval="5">
    <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
    <!--变量配置-->
    <Properties>
        <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
        <!-- %logger{36} 表示 Logger 名字最长36个字符 -->
        <property name="LOG_PATTERN" value="[%date{YYYY-MM-dd HH:mm:ss}][%thread] %-5level %logger{36} - %msg%n" />
        <!-- 定义日志存储的路径 -->
        <property name="FILE_PATH" value="log/" />
        <property name="FILE_NAME" value="example" />
    </Properties>
    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <!--            <PatternLayout pattern="${LOG_PATTERN}"/>-->
            <PatternLayout
                    pattern="[%date{YYYY-MM-dd HH:mm:ss}]%style{[%t]}{bright,magenta} %highlight{%-5level}{ERROR=Bright RED, WARN=Bright Yellow, INFO=Bright Green, DEBUG=Bright Cyan, TRACE=Bright White} %style{%c{1.}.%M(%L)}{cyan}: %msg%n"/>
            <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
        </console>
        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
        <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </File>
        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是1 hour-->
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
        <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是1 hour-->
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
        <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!--interval属性用来指定多久滚动一次,默认是1 hour-->
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
    </appenders>
    <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
    <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
        <logger name="org.mybatis" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </logger>
        <!--监控系统信息-->
        <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
        <Logger name="org.springframework" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <root level="info">
            <appender-ref ref="Console"/>
            <appender-ref ref="Filelog"/>
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="RollingFileWarn"/>
            <appender-ref ref="RollingFileError"/>
        </root>
    </loggers>
</configuration>

这里记得修改项目名称和日志文件存放路径,也可以选择默认

API 文档生成插件配置文件

我们使用 smart-doc 作为项目的API文档生成器,主要优点是无侵入性,只要在接口上写上标准的 javadoc 即可,很方便。

/resources 目录下新建 smart-doc.json 文件,填入以下内容

// smart-doc.json
{
  "serverUrl": "http://localhost:8088/",
  "isStrict": false,
  "allInOne": true,
  "outPath": "src/main/resources/docs",
  "coverOld": true,
  "createDebugPage": true,
  "md5EncryptedHtmlName": false,
  "projectName": "example",
  "showAuthor": true
}

这里想改的话建议参考官方给的文档。

设计项目包结构

这里参考羊哥公众号发的那个,根据我的理解改了一点

SpringBoot集成Flowable modeler设计器 springboot组件集成_java_03

编写 Java 配置类

程序入口加注解 @EnableCaching

加这个注解启用缓存

@EnableCaching
@SpringBootApplication
public class SpringBootExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootExampleApplication.class, args);
    }

}

配置 Mybatis-Plus

有一说一,这玩意儿挺香的,反正我是真香了

package com.wjl.example.config;
 
@Configuration
@MapperScan("com.wjl.example.repository")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

配置跨域

因为一般写的是前后端分离项目,所以要进行跨域处理

package com.wjl.example.config;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedOriginPatterns()
                .maxAge(3600)
                .allowedHeaders("*");
    }

}

配置 Redis

进行必要的配置,这里我下午还卡了以下,不是很懂

package com.wjl.example.config;

@EnableCaching
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer<?> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(objectMapper);

        template.setKeySerializer(redisSerializer);
        template.setHashKeySerializer(redisSerializer);
        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }

}

配置 Sa-Token 拦截器

我们使用的这个Java权限认证框架很轻量级,官网文档也比较齐全

package com.wjl.example.config;

@Configuration
public class SaIntercepConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaAnnotationInterceptor())
                .addPathPatterns("/**");
    }

}

统一后端返回格式

SpringBoot 提供的不能满足我们的需求,需要我们自己封装返回结果,并进行配置

返回码定义

package com.wjl.example.common.enums;

public enum HttpStatus {
    // 2xx Success
    OK(200, "OK"),
    // --- 4xx Client Error ---
    BAD_REQUEST(400, "Bad Request"),
    UNAUTHORIZED(401, "Unauthorized"),
    PAYMENT_REQUIRED(402, "Payment Required"),
    FORBIDDEN(403, "Forbidden"),
    NOT_FOUND(404, "Not Found"),
    METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
    NOT_ACCEPTABLE(406, "Not Acceptable"),
    PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
    REQUEST_TIMEOUT(408, "Request Timeout"),
    CONFLICT(409, "Conflict"),
    // --- 5xx Server Error ---
    INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
    NOT_IMPLEMENTED(501, "Not Implemented"),
    BAD_GATEWAY(502, "Bad Gateway"),
    private final int code;
    private final String msg;

    HttpStatus(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

}

返回结果统一封装

package com.wjl.example.common.constants;

@Data
public class Result<T> {

    private int status;
    private String msg;
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> Result = new Result<>();
        Result.setStatus(HttpStatus.OK.getCode());
        Result.setMsg(HttpStatus.OK.getMsg());
        Result.setData(data);
        return Result;
    }

    public static <T> Result<T> success(String msg) {
        Result<T> Result = new Result<>();
        Result.setStatus(HttpStatus.OK.getCode());
        Result.setMsg(msg);
        return Result;
    }

    public static <T> Result<T> fail(int code, String msg) {
        Result<T> Result = new Result<>();
        Result.setStatus(code);
        Result.setMsg(msg);
        return Result;
    }

}

返回结果统一处理

package com.wjl.example.component;

@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter parameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter parameter, MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (o instanceof String) {
            return objectMapper.writeValueAsString(Result.success(o));
        }
        if (o instanceof Result) {
            return o;
        }
        return Result.success(o);
    }

}

全局异常拦截

package com.wjl.example.exception;

@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<String> exception(Exception e) {
        log.error("全局异常信息 ex={}", e.getMessage(), e);
        return Result.fail(HttpStatus.INTERNAL_SERVER_ERROR.getCode(), e.getMessage());
    }

}

引入工具类

操作 Redis 的 RedisUtil

package com.wjl.example.util;

@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // ============================== common ==================================

    /**
     * set expire time
     *
     * @param key  key
     * @param time time
     * @return state
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    // ============================== string ==================================

    /**
     * get value by key
     *
     * @param key key
     * @return value
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    // ================================ map ===================================

    /**
     * HashGet
     *
     * @param key  key - Notnull
     * @param item item - NotNull
     * @return data
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    // ================================ set ===================================

    /**
     * get value in set by key
     *
     * @param key key
     * @return set
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
 
}

连接 COS 的工具类

我一般倾向于使用腾讯云,所以这里是腾讯云的COS对象存储,阿里云OSS同理,根据官方文档来就行。

这里可以先定义全局常量,关于COS存储桶的信息,注意这些信息不能上传到GitHub,我上次没注意,直接 git push 一把梭,结果腾讯云立马给我打电话说密钥泄露,千万要注意。

package com.wjl.example.common.constants;

public class GlobalConstants {

    public static final String AVATAR = "https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png";

    public static final String SECRET_ID = "";

    public static final String SECRET_KEY = "";

    public static final String REGION = "";

    public static final String BUCKET = "";

    public static final String FOLDER = "";

}
package com.wjl.example.util;

public class CosClientUtil {

    public static COSClient createClient() {
        String secretId = GlobalConstants.SECRET_ID;
        String secretKey = GlobalConstants.SECRET_KEY;
        COSCredentials credentials = new BasicCOSCredentials(secretId, secretKey);
        Region region = new Region(GlobalConstants.REGION);
        ClientConfig clientConfig = new ClientConfig(region);
        return new COSClient(credentials, clientConfig);
    }

}

实现权限认证功能

这里我们使用RBAC认证模式

编写实体

这里我们选择使用 Spring Data JPA 作为 ORM 框架,定义 UserRolePermissionUserRoleRolePermission 五个实体

package com.wjl.example.model.entity;

@Getter
@Setter
@Entity
@Table(name = "user")
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    @Column(name = "username")
    private String username;
    @Column(name = "password")
    private String password;
    @Column(name = "token")
    private String token;

}
package com.wjl.example.model.entity;

@Getter
@Setter
@Entity
@Table(name = "role")
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    @Column(name = "role")
    private String role;

}
package com.wjl.example.model.entity;

@Getter
@Setter
@Entity
@Table(name = "permission")
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
public class Permission {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    @Column(name = "permission")
    private String permission;

}
package com.wjl.example.model.entity;

@Getter
@Setter
@Entity
@Table(name = "user_role")
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
public class UserRole {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    private Long uid;
    private Long rid;

}
package com.wjl.example.model.entity;

@Getter
@Setter
@Entity
@Table(name = "role_permission")
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
public class RolePermission {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    private Long rid;
    private Long pid;

}

根据实体编写对应的ORM层和Service层

这里我就放一起了,因为单个代码比较少

package com.wjl.example.repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findUserById(Long id);
    User findUserByUsername(String username);
    User findUserByUsernameAndPassword(String username, String password);
}

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    Role findRoleById(Long id);
}

@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {
    Permission findPermissionById(Long id);
}

@Repository
public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
    List<UserRole> findAllByUid(Long uid);
}

@Repository
public interface RolePermissionRepository extends JpaRepository<RolePermission, Long> {
    List<RolePermission> findAllByRid(Long rid);
}
package com.wjl.example.service.impl;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserRepository userRepository;

    @Override
    public User findUserById(Long id) {
        return userRepository.findUserById(id);
    }

    @Override
    public User findUserByUsername(String username) {
        return userRepository.findUserByUsername(username);
    }

    @Override
    public User findUserByUsernameAndPassword(String username, String password) {
        return userRepository.findUserByUsernameAndPassword(username, password);
    }

}

@Service
public class RoleServiceImpl implements RoleService {

    @Resource
    private RoleRepository roleRepository;

    @Override
    public Role findRoleById(Long rid) {
        return roleRepository.findRoleById(rid);
    }

}

@Service
public class PermissionServiceImpl implements PermissionService {

    @Resource
    private PermissionRepository permissionRepository;

    @Override
    public Permission findPermissionById(Long pid) {
        return permissionRepository.findPermissionById(pid);
    }

}

@Service
public class UserRoleServiceImpl implements UserRoleService {

    @Resource
    private UserRoleRepository userRoleRepository;

    @Override
    public List<UserRole> findRolesByUser(User user) {
        return userRoleRepository.findAllByUid(user.getId());
    }

}

@Service
public class RolePermissionServiceImpl implements RolePermissionService {

    @Resource
    private RolePermissionRepository rolePermissionRepository;

    @Override
    public List<RolePermission> findAllByRole(Role role) {
        return rolePermissionRepository.findAllByRid(role.getId());
    }

}

实现 Sa-Token 的权限认证接口

根据官方文档,只要实现 StpInterface 接口里的两个方法即可

package com.wjl.example.component;

@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    private UserService userService;
    
    @Resource
    private RoleService roleService;

    @Resource
    private PermissionService permissionService;

    @Resource
    private UserRoleService userRoleService;

    @Resource
    private RolePermissionService rolePermissionService;

    @Override
    public List<String> getPermissionList(Object loginId, String s) {
        User user = userService.findUserByUsername(loginId.toString());
        List<UserRole> userRoles = userRoleService.findRolesByUser(user);
        Set<String> permissions = new HashSet<>();
        for (UserRole userRole : userRoles) {
            Role role = roleService.findRoleById(userRole.getRid());
            List<RolePermission> rolePermissions = rolePermissionService.findAllByRole(role);
            for (RolePermission rolePermission : rolePermissions) {
                Permission permission = permissionService.findPermissionById(rolePermission.getPid());
                permissions.add(permission.getPermission());
            }
        }
        return new ArrayList<>(permissions);
    }

    @Override
    public List<String> getRoleList(Object loginId, String s) {
        User user = userService.findUserByUsername(loginId.toString());
        List<UserRole> userRoles = userRoleService.findRolesByUser(user);
        List<String> roles = new ArrayList<>();
        for (UserRole userRole : userRoles) {
            roles.add(roleService.findRoleById(userRole.getRid()).getRole());
        }
        return roles;
    }

}

登录注册接口什么的就先不写了。

实现 Redis 缓存功能

实体和对象映射

让我们先定义一个 Todo 实体类,并使用 Mybatis 的注解进行对象映射,不要忘了先创建表

package com.wjl.example.model.entity;

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Todo implements Serializable {

    private Long id;
    private String description;
    private String details;
    private boolean done;

}
package com.wjl.example.repository;

@Mapper
@Repository
public interface TodoRepository {

    @Select("select count(*) from todo")
    int countTodo();

    @Select("select * from todo")
    List<Todo> findAll();

    @Select("select * from todo where id=#{id}")
    Todo findTodoById(@Param(value = "id") Long id);

    @Insert("insert into todo(id, description, details, done) values (#{id}, #{description}, #{details}, #{done})")
    int addTodo(Todo todo);

    @Update("update todo set description=#{description}, details=#{details}, done=#{done} where id=#{id}")
    int updateTodo(Todo todo);

    @Delete("delete from todo where id=#{id}")
    int deleteTodo(@Param(value = "id") Long id);

}

Service 集成 Redis 缓存机制

这里就直接贴代码了,因为我现在对这个还不是很熟

package com.wjl.example.service.impl;

@Slf4j
@Service
@CacheConfig(cacheNames = "todo")
public class TodoServiceImpl implements TodoService {

    @Resource
    private TodoRepository todoRepository;

    @Resource
    private RedisUtil redisUtil;

    public static final String CACHE_KEY_TODO = "todo:";

    @Override
    public int countTodo() {
        return todoRepository.countTodo();
    }

    @Override
    public List<Todo> findAll() {
        int i = countTodo();
        Object o = redisUtil.get(CACHE_KEY_TODO + "number:" + i);
        if (!Objects.isNull(o)) {
            return (List<Todo>) o;
        }
        List<Todo> todos = todoRepository.findAll();
        redisUtil.set(CACHE_KEY_TODO + "number:" + i, todos);
        log.info("cache not exist, do this method");
        return todos;
    }

    @Override
    public Todo findTodoById(Long id) {
        Object o = redisUtil.get(CACHE_KEY_TODO + id);
        if (!Objects.isNull(o)) {
            log.info("get from redis: {}", o);
            return (Todo) o;
        }
        Todo todo = todoRepository.findTodoById(id);
        redisUtil.set(CACHE_KEY_TODO + id, todo);
        log.info("get from mysql, and set into redis: {}", todo);
        return todo;
    }

    @Override
    public int addTodo(Todo todo) {
        int i = todoRepository.addTodo(todo);
        redisUtil.set(CACHE_KEY_TODO + todo.getId(), todo);
        log.info("write into redis: {}", todo);
        return i;
    }

    @Override
    public int updateTodoById(Todo todo) {
        int i = todoRepository.updateTodo(todo);
        redisUtil.set(CACHE_KEY_TODO + todo.getId(), todo);
        log.info("update data and delete cache");
        return i;
    }

    @Override
    public int deleteTodoById(Long id) {
        int i = todoRepository.deleteTodo(id);
        redisUtil.del(CACHE_KEY_TODO + id);
        return i;
    }

}

编写单元测试

这是 TodoService 的单元测试

package com.wjl.example.service;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TodoServiceTest {

    @Resource
    private TodoService todoService;

    @Test
    public void test() {
        List<Todo> todos = todoService.findAll();
        System.out.println(todos);
    }

    @Test
    public void selectById() {
        Todo todo = todoService.findTodoById(1L);
        System.out.println(todo);
    }

    @Test
    public void add() {
        todoService.addTodo(new Todo(2L, "test3", "rveref", false));
    }

    @Test
    public void update() {
        todoService.updateTodoById(new Todo(3L, "test33", "rveref", false));
    }

    @Test
    public void delete() {
        todoService.deleteTodoById(2L);
    }

}

这是 RedisUtil 的单元测试

package com.wjl.example.service;

@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisServiceTest {

    @Resource
    private RedisUtil redisUtil;

    @Test
    public void save() {
        Todo todo = new Todo(1L, "test1", "test111111111111111111", false);
        redisUtil.set("todo:" + todo.getId(), todo);
        System.out.println(redisUtil.get("todo:" + todo.getId()));
        todo.setDetails("test123221122141");
        redisUtil.del("todo:" + todo.getId());
        redisUtil.set("todo:" + todo.getId(), todo);
        System.out.println(redisUtil.get("todo:" + todo.getId()));
    }

    /**
     * test redisService common part
     */
    @Test
    public void commonTest() {
        System.out.println(redisUtil.expire("todo:1", 1000));
        System.out.println(redisUtil.getExpire("todo:1"));
        System.out.println(redisUtil.hasKey("todo:1"));
        System.out.println(redisUtil.hasKey("todo:4"));
        redisUtil.del("todo:1", "todo:4");
        System.out.println(redisUtil.hasKey("todo:1"));
    }

    /**
     * test redisService string part
     */
    @Test
    public void stringTest() {
        redisUtil.set("test:1", "test1111");
        System.out.println(redisUtil.get("test:1"));
        redisUtil.set("test:2", "test2222", 1000);
        System.out.println(redisUtil.getExpire("test:2"));
        redisUtil.set("test1", 1);
        System.out.println(redisUtil.incr("test1", 1));
        redisUtil.set("test2", 100);
        System.out.println(redisUtil.decr("test2", 1));
    }

    /**
     * test redisService map part
     */
    @Test
    public void mapTest() {
        Map<String, Object> map = new HashMap<>();
        map.put(String.valueOf(1L), "aaa");
        map.put(String.valueOf(2L), "bbb");
        map.put(String.valueOf(3L), "ccc");
        System.out.println(redisUtil.hmset("map-test1", map));
        System.out.println(redisUtil.hmset("map-test2", map, 1000));
        System.out.println(redisUtil.hmget("map-test1"));
        System.out.println(redisUtil.hmget("map-test2"));
        System.out.println(redisUtil.hget("map-test1", String.valueOf(1L)));
        System.out.println(redisUtil.hset("map-test1", "4", "ddd"));
        System.out.println(redisUtil.hset("map-test3", "4", "ddd", 30));
        redisUtil.hdel("map-test2", "1", "2");
        System.out.println(redisUtil.hHasKey("map-test2", "2"));
        System.out.println(redisUtil.hHasKey("map-test2", "3"));
        System.out.println(redisUtil.hincr("maptest1", "123", 1));
        System.out.println(redisUtil.hdecr("maptest2", "231", 2));
    }

    /**
     * test redisService set part
     */
    @Test
    public void setTest() {
        System.out.println(redisUtil.sSet("set-test1", "set1", "set2", "set3", "set4"));
        System.out.println(redisUtil.sGet("set-test1"));
        System.out.println(redisUtil.sSetAndTime("set-test2", 100, "set1", "set2"));
        System.out.println(redisUtil.sGetSetSize("set-test1"));
        System.out.println(redisUtil.setRemove("set-test1", "set3"));
        System.out.println(redisUtil.sGetSetSize("set-test1"));
        System.out.println(redisUtil.sHashKey("set-test1", "set2"));
    }

    /**
     * test redisService list part
     */
    @Test
    public void listTest() {
        List<Object> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("aaa");
        list.add("ccc");
        System.out.println(redisUtil.lSet("list-test1", "aaaa"));
        System.out.println(redisUtil.lSet("list-test3", "aaaa", 10));
        System.out.println(redisUtil.lSet("list-test2", list));
        System.out.println(redisUtil.lSet("list-test3", list, 100));
        System.out.println(redisUtil.lGet("list-test2", 2, 4));
        System.out.println(redisUtil.lGetListSize("list-test2"));
        System.out.println(redisUtil.lGetIndex("list-test2", 2));
        System.out.println(redisUtil.lUpdateIndex("list-test2", 2, "123"));
        System.out.println(redisUtil.lGetListSize("list-test1"));
        System.out.println(redisUtil.lRemove("list-test1", 4, "aaaa"));
        System.out.println(redisUtil.lGetListSize("list-test1"));
    }

}

个人觉得单元测试覆盖面还是很广的。

–完–