依赖 | 版本 | 描述 |
---|---|---|
Java(JDK) | 17 | Java开发环境 |
Spring Boot | 3.2.0 | Spring Boot版本 |
MySQL | 8.x | 数据库 |
Redisson | 3.25.0 | Redis工具库 |
MyBaits-Plus | 3.5.4.1 | Mybatis 增强工具 |
java-jwt | 4.4.0 | JWT 工具库 |
SpringDoc | 2.3.0 | 接口文档 |
<properties>
<mybatis-spring.version>3.0.3</mybatis-spring.version>
<mybatis-plus.version>3.5.4.1</mybatis-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml
配置文件# ======== 数据源配置 ========
spring:
datasource:
driver-class-name: org.h2.Driver
username: root
password: test
sql:
init:
schema-locations: classpath:db/schema-h2.sql
data-locations: classpath:db/data-h2.sql
# ======== MybatisPlus配置 ========
mybatis-plus:
# 搜索指定包别名
type-aliases-package: com.yiyan.study.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapper-locations: classpath:mapper/**/*xml
# 配置驼峰
configuration:
# 是否开启自动驼峰命名规则(camel case)映射。默认值:true
map-underscore-to-camel-case: true
# 开启 Mybatis 二级缓存,默认为 true。
cache-enabled: true
# 日志输出实现。StdOutImpl 标准日志控制台输出
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 全局默认主键类型
id-type: assign_id
# 表名是否使用驼峰转下划线命名,只对表名生效。 默认true
table-underline: true
-- data-h2.sql
DELETE
FROM `user`;
INSERT INTO `user` (id, name, age, email)
VALUES (1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
-- schema-h2.sql
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`
(
id BIGINT NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
实体类
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@TableName(value = "`user`")
@Data
public class User implements Serializable {
@TableId
private Long id;
private String name;
private Integer age;
private String email;
@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
DAO(mapper)
import com.yiyan.study.domain.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface IUserDao extends BaseMapper<User> {
}
Service
import com.yiyan.study.domain.User;
import com.baomidou.mybatisplus.extension.service.IService;
public interface IUserService extends IService<User> {
}
ServiceImpl
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yiyan.study.domain.User;
import com.yiyan.study.service.IUserService;
import com.yiyan.study.dao.IUserDao;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<IUserDao, User>
implements IUserService{
}
mapper.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.yiyan.study.dao.IUserDao">
</mapper>
package com.yiyan.study;
import com.yiyan.study.dao.IUserDao;
import com.yiyan.study.domain.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@Slf4j
@SpringBootTest
@DisplayName("MyBatis Plus Test")
public class MyBatisPlusTest {
@Autowired
private IUserDao userDao;
@Test
@DisplayName("MyBatis Plus Select Test")
public void testSelect() {
log.info(("----- selectAll method test ------"));
List<User> userList = userDao.selectList(null);
for (User user : userList) {
log.info(user.toString());
}
}
@Test
@DisplayName("MyBatis Plus Insert Test")
public void testInsert() {
log.info(("----- insert method test ------"));
User user = new User();
user.setName("Alex");
user.setAge(18);
user.setEmail("alex@gmail.com");
int result = userDao.insert(user);
log.info("insert result: {}", result);
User selectedById = userDao.selectById(user.getId());
log.info("insert user info: {}", selectedById);
}
@Test
@DisplayName("MyBatis Plus Update Test")
public void testUpdate() {
log.info(("----- update method test ------"));
User user = userDao.selectList(null).get(0);
log.info("before update: {}", user);
user.setName("Updated Name");
int result = userDao.updateById(user);
log.info("update result: {}", result);
User selectedById = userDao.selectById(user.getId());
log.info("after update: {}", selectedById);
}
@Test
@DisplayName("MyBatis Plus Delete Test")
public void testDelete() {
log.info("----- delete method test ------");
List<User> users = userDao.selectList(null);
log.info("before delete user list size: {}", users.size());
User user = users.get(0);
userDao.deleteById(user.getId());
log.info("delete user id: {}", user.getId());
List<User> usersAfterDelete = userDao.selectList(null);
log.info("after delete user list size: {}", usersAfterDelete.size());
}
}
<properties>
<redisson.version>3.25.0</redisson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
# application.yml
spring:
# ======== Redis配置 ========
redis:
redisson:
file: classpath:redisson.yaml
# redisson.yaml
# 编码。默认值: org.redisson.codec.JsonJacksonCodec
codec: !<org.redisson.codec.Kryo5Codec> {}
# 线程池数量。默认值: 当前处理核数量 * 2
threads: 16
# Netty线程池数量。默认值: 当前处理核数量 * 2
nettyThreads: 32
# 传输模式。默认值: NIO
transportMode: "NIO"
# 监控锁的看门狗超时,单位:毫秒。默认值: 30000
lockWatchdogTimeout: 30000
# 是否保持订阅发布顺序。默认值: true
keepPubSubOrder: true
# Redisson 单实例配置
singleServerConfig:
# 节点地址。格式:redis://host:port
address: "redis://127.0.0.1:6379"
# 密码。默认值: null
password: null
# 数据库编号。默认值: 0
database: 0
# 客户端名称(在Redis节点里显示的客户端名称)。默认值: null
clientName: null
# 连接超时,单位:毫秒。默认值: 10000
connectTimeout: 10000
# 命令等待超时,单位:毫秒。默认值: 3000
timeout: 3000
# 命令失败重试次数。默认值: 3
retryAttempts: 3
# 命令重试发送时间间隔,单位:毫秒。默认值: 1500
retryInterval: 1500
# 最小空闲连接数。默认值: 32
connectionMinimumIdleSize: 24
# 连接池大小。默认值: 64
connectionPoolSize: 64
# 单个连接最大订阅数量。默认值: 5
subscriptionsPerConnection: 5
# 发布和订阅连接的最小空闲连接数。默认值: 1
subscriptionConnectionMinimumIdleSize: 1
# 发布和订阅连接池大小。默认值: 50
subscriptionConnectionPoolSize: 50
# DNS监测时间间隔,单位:毫秒。默认值: 5000
dnsMonitoringInterval: 5000
# 连接空闲超时,单位:毫秒。默认值: 10000
idleConnectionTimeout: 10000
package com.yiyan.study;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RAtomicDouble;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RBucket;
import org.redisson.api.RList;
import org.redisson.api.RMap;
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
*/
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class RedisService {
private final RedissonClient redissonClient;
// ============================= String类型操作 ============================
/**
* 将值存储到Redis中
*
* @param key 键
* @param value 值
*/
public <T> void setString(String key, T value) {
RBucket<T> bucket = redissonClient.getBucket(key);
bucket.set(value);
}
/**
* 将值存储到Redis中
*
* @param key 键
* @param value 值
* @param timeout 过期时间
* @param timeUnit 时间单位
*/
public <T> void setString(String key, T value, long timeout, TimeUnit timeUnit) {
RBucket<T> bucket = redissonClient.getBucket(key);
bucket.set(value, timeout, timeUnit);
}
/**
* 根据键获取Redis中的值
*
* @param key 键
* @return 值
*/
public <T> T getString(String key) {
RBucket<T> bucket = redissonClient.getBucket(key);
return bucket.get();
}
// ============================= Hash类型操作 ============================
/**
* 将值存储到Redis中
*
* @param key 键
* @param field hash键
* @param value 值
*/
public <T> boolean addToHash(String key, Object field, T value) {
RMap<Object, T> hash = redissonClient.getMap(key);
return hash.fastPut(field, value);
}
/**
* 将值存储到Redis中
*
* @param key 键
* @param field hash键
* @param value 值
* @param timeout 过期时间
* @param timeUnit 时间单位
*/
public <T> boolean addToHash(String key, Object field, T value, long timeout, ChronoUnit timeUnit) {
RMap<Object, T> hash = redissonClient.getMap(key);
boolean fastPut = hash.fastPut(field, value);
boolean expire = hash.expire(Instant.now().plus(timeout, timeUnit));
return fastPut && expire;
}
/**
* 根据键和Hash键获取Redis中的值
*
* @param key 键
* @param field hash键
* @return 值
*/
public <T> T getFromHash(String key, Object field) {
RMap<Object, T> hash = redissonClient.getMap(key);
return hash.get(field);
}
/**
* 根据键获取Redis中的值
*
* @param key 键
* @return 值
*/
public <T> Map<Object, T> getFromHash(String key) {
RMap<Object, T> hash = redissonClient.getMap(key);
return hash.readAllMap();
}
/**
* 根据键和Hash键更新Redis中的值
*
* @param key 键
* @param field hash键
* @param value 值
* @return 更新成功返回true,否则返回false
*/
public <T> boolean updateToHash(String key, Object field, T value) {
RMap<Object, T> hash = redissonClient.getMap(key);
return hash.fastReplace(field, value);
}
/**
* 根据Key,删除Hash类型的数据
*
* @param key 键
* @param hashKeys hash键
* @return 删除成功的数量
*/
public <T> long removeFromHash(String key, T... hashKeys) {
RMap<Object, T> hash = redissonClient.getMap(key);
return hash.fastRemove(hashKeys);
}
// ============================= List类型操作 ============================
/**
* 向List数据类型中添加值
*
* @param key 键
* @param value 值
*/
public <T> boolean addToList(String key, T value) {
RList<T> list = redissonClient.getList(key);
return list.add(value);
}
/**
* 向List数据类型中添加值
*
* @param key 键
* @param value 值
*/
public <T> boolean addToList(String key, List<T> value) {
RList<T> list = redissonClient.getList(key);
return list.addAll(value);
}
/**
* 向List数据类型中添加值
*
* @param key 键
* @param value 值
* @param timeout 过期时间
* @param timeUnit 时间单位
*/
public <T> boolean addToList(String key, T value, long timeout, ChronoUnit timeUnit) {
RList<T> list = redissonClient.getList(key);
list.add(value);
return list.expire(Instant.now().plus(timeout, timeUnit));
}
/**
* 从List数据类型中获取值
*
* @param key 键
* @param start 起始位置
* @param end 结束位置
* @return 值
*/
public <T> List<T> getFromList(String key, int start, int end) {
RList<T> list = redissonClient.getList(key);
return list.range(start, end);
}
/**
* 获取List数据类型中的所有值
*
* @param key 键
* @return 值
*/
public <T> List<T> getFromList(String key) {
RList<T> list = redissonClient.getList(key);
return list.readAll();
}
/**
* 移除集合左侧第一个元素
*
* @param key 键
*/
public void removeListLeft(String key) {
RList<Object> list = redissonClient.getList(key);
list.fastRemove(0);
}
/**
* 移除集合右侧第一个元素
*
* @param key 键
*/
public void removeListRight(String key) {
RList<Object> list = redissonClient.getList(key);
list.fastRemove(list.size() - 1);
}
/**
* 移除集合指定位置元素
*
* @param key 键
* @param index 索引
*/
public void removeFromList(String key, int index) {
RList<Object> list = redissonClient.getList(key);
list.fastRemove(index);
}
/**
* 移除集合指定元素
*
* @param key 键
* @param value 值
*/
public <T> boolean removeFromList(String key, T value) {
RList<T> list = redissonClient.getList(key);
return list.removeIf(o -> o.equals(value));
}
// ============================= Set类型操作 ============================
/**
* 添加值到Set数据类型中
*
* @param key 键
* @param value 值
*/
public <T> boolean addToSet(String key, T value) {
RSet<T> set = redissonClient.getSet(key);
return set.add(value);
}
/**
* 添加值到Set数据类型中
*
* @param key 键
* @param value 值
* @param timeout 过期时间
* @param timeUnit 时间单位
* @return 是否成功
*/
public <T> boolean addToSet(String key, T value, long timeout, ChronoUnit timeUnit) {
RSet<T> set = redissonClient.getSet(key);
boolean add = set.add(value);
boolean expire = set.expire(Instant.now().plus(timeout, timeUnit));
return add && expire;
}
/**
* 添加值到Set数据类型中
*
* @param key 键
* @param values 值
* @return 是否成功
*/
public <T> boolean addToSet(String key, List<T> values) {
RSet<T> set = redissonClient.getSet(key);
return set.addAll(values);
}
/**
* 添加值到Set数据类型中
*
* @param key 键
* @param values 值
* @param timeout 过期时间
* @param timeUnit 时间单位
* @return 是否成功
*/
public <T> boolean addToSet(String key, List<T> values, long timeout, ChronoUnit timeUnit) {
RSet<T> set = redissonClient.getSet(key);
set.addAllCounted(values);
return set.expire(Instant.now().plus(timeout, timeUnit));
}
/**
* 获取Set的所有元素。
*
* @param key 键
* @return 所有值
*/
public <T> Set<T> getFromSet(String key) {
RSet<T> set = redissonClient.getSet(key);
return set.readAll();
}
/**
* 从Set数据类型中删除值
*
* @param key 键
* @param values 值
*/
public <T> void removeFromSet(String key, List<T> values) {
RSet<T> set = redissonClient.getSet(key);
values.forEach(set::remove);
}
/**
* 从Set数据类型中删除值
*
* @param key 键
* @param value 值
*/
public <T> boolean removeFromSet(String key, T value) {
RSet<T> set = redissonClient.getSet(key);
return set.remove(value);
}
// ============================= ZSet类型操作 ============================
/**
* 添加值到ZSet数据类型中
*
* @param key 键
* @param value 值
* @param score 分值
*/
public <T> void addToZSet(String key, T value, double score) {
RScoredSortedSet<T> sortedSet = redissonClient.getScoredSortedSet(key);
sortedSet.add(score, value);
}
/**
* 在ZSet数据类型中添加值
*
* @param key 键
* @param value 值
* @param score 分值
* @param timeout 过期时间
* @param timeUnit 时间单位
*/
public <T> void addToZSet(String key, T value, double score, long timeout, ChronoUnit timeUnit) {
RScoredSortedSet<T> sortedSet = redissonClient.getScoredSortedSet(key);
sortedSet.add(score, value);
sortedSet.expire(Instant.now().plus(timeout, timeUnit));
}
/**
* 获取ZSet的范围元素。
*
* @param key 键
* @param start 起始位置
* @param end 结束位置
* @return Set类型的值
*/
public <T> Set<Object> getFromZSet(String key, int start, int end) {
RScoredSortedSet<T> sortedSet = redissonClient.getScoredSortedSet(key);
return new HashSet<>(sortedSet.valueRange(start, end));
}
/**
* 删除ZSet数据类型中的值
*
* @param key 键
* @param values 值
*/
public <T> void removeFromZSet(String key, List<T> values) {
RScoredSortedSet<T> sortedSet = redissonClient.getScoredSortedSet(key);
sortedSet.removeAll(values);
}
/**
* 删除ZSet数据类型中的值
*
* @param key 键
* @param value 值
*/
public <T> void removeFromZSet(String key, T value) {
RScoredSortedSet<T> sortedSet = redissonClient.getScoredSortedSet(key);
sortedSet.remove(value);
}
// ============================= Common ============================
/**
* 判断Key是否存在
*
* @param key 键
* @return 存在返回true,否则返回false
*/
public boolean exists(String key) {
return redissonClient.getBucket(key).isExists();
}
/**
* 删除Key
*
* @param key 键
*/
public boolean remove(String key) {
long delete = redissonClient.getKeys().delete(key);
return delete > 0;
}
/**
* 设置Key的过期时间
*
* @param key 键
* @param timeout 过期时间
* @param timeUnit 时间单位
* @return 设置成功返回true,否则返回false
*/
public boolean expire(String key, long timeout, ChronoUnit timeUnit) {
return redissonClient.getBucket(key).expire(Instant.now().plus(timeout, timeUnit));
}
/**
* 获取Key的过期时间
*
* @param key 键
* @return 过期时间
*/
public Long getExpire(String key) {
return redissonClient.getBucket(key).getExpireTime();
}
/**
* 递增操作
*
* @param key 键
* @param delta 增加的值
* @return 递增后的值,如果键不存在,则返回-1
*/
public long increment(String key, long delta) {
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
return atomicLong.addAndGet(delta);
}
/**
* 递减操作
*
* @param key 键
* @param delta 减少的值
* @return 递减后的值,如果键不存在,则返回-1
*/
public long decrement(String key, long delta) {
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
return atomicLong.decrementAndGet();
}
/**
* 递增操作
*
* @param key 键
* @param delta 增加的值
* @return 递增后的值,如果键不存在,则返回-1
*/
public double increment(String key, double delta) {
RAtomicDouble atomicDouble = redissonClient.getAtomicDouble(key);
return atomicDouble.addAndGet(delta);
}
/**
* 递减操作
*
* @param key 键
* @param delta 减少的值
* @return 递减后的值,如果键不存在,则返回-1
*/
public double decrement(String key, double delta) {
RAtomicDouble atomicDouble = redissonClient.getAtomicDouble(key);
return atomicDouble.decrementAndGet();
}
}
package com.yiyan.study;
import jakarta.annotation.Resource;
import jodd.util.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@SpringBootTest
@DisplayName("Redis 集成测试类")
public class RedisApplicationTest {
@Resource
private RedisService redisService;
private static final String STRING_KEY = "redis:string";
private static final String LIST_KEY = "redis:list";
private static final String SET_KEY = "redis:set";
private static final String HASH_KEY = "redis:hash";
@Test
@DisplayName("Redis String 数据类型测试")
public void redisStringTest() {
log.info("Redis String 数据类型测试");
redisService.setString(STRING_KEY, LocalDateTime.now().toString());
String redisGetStringData = redisService.getString(STRING_KEY);
log.info("Redis String Get:{}", redisGetStringData);
boolean remove = redisService.remove(STRING_KEY);
log.info("Redis String Remove:{}", remove);
redisGetStringData = redisService.getString(STRING_KEY);
log.info("Redis String Get After Delete:{}", redisGetStringData);
}
@Test
@DisplayName("Redis List 数据类型测试")
public void redisListTest() {
log.info("Redis List 数据类型测试");
// 填充数据
List<Integer> list = Lists.newArrayList();
for (int i = 0; i < 5; i++) {
list.add(LocalDateTime.now().getNano());
ThreadUtil.sleep(5);
}
boolean addItemResult = redisService.addToList(LIST_KEY, LocalDateTime.now().getNano());
log.info("Redis List Add item:{}", addItemResult);
redisService.getFromList(LIST_KEY)
.forEach(s -> log.info("Redis List Get After Add Item:{}", s));
boolean addListDataResult = redisService.addToList(LIST_KEY, list);
log.info("Redis List Add List:{}", addListDataResult);
redisService.getFromList(LIST_KEY)
.forEach(s -> log.info("Redis List Get After Add List:{}", s));
redisService.getFromList(LIST_KEY, 0, 2)
.forEach(s -> log.info("Redis List Get By Index:{}", s));
log.info("Redis List Size Before Delete:{}", redisService.getFromList(LIST_KEY).size());
redisService.removeFromList(LIST_KEY, 0);
log.info("Redis List Size After Delete:{}", redisService.getFromList(LIST_KEY).size());
boolean remove = redisService.remove(LIST_KEY);
log.info("Redis List Remove:{}", remove);
}
@Test
@DisplayName("Redis Set 数据类型测试")
public void redisSetTest() {
log.info("Redis Set 数据类型测试");
// 填充数据
List<Integer> list = Lists.newArrayList();
for (int i = 0; i < 5; i++) {
list.add(LocalDateTime.now().getNano());
ThreadUtil.sleep(5);
}
boolean addItemResult = redisService.addToSet(SET_KEY, LocalDateTime.now().getNano());
log.info("Redis Set Add item:{}", addItemResult);
redisService.getFromSet(SET_KEY)
.forEach(s -> log.info("Redis Set Get After Add Item:{}", s));
boolean addListDataResult = redisService.addToSet(SET_KEY, list);
log.info("Redis Set Add List:{}", addListDataResult);
redisService.getFromSet(SET_KEY)
.forEach(s -> log.info("Redis Set Get After Add List:{}", s));
log.info("Redis Set Size Before Delete:{}", redisService.getFromSet(SET_KEY).size());
redisService.removeFromSet(SET_KEY, LocalDateTime.now().getNano());
log.info("Redis Set Size After Delete:{}", redisService.getFromSet(SET_KEY).size());
boolean remove = redisService.remove(SET_KEY);
log.info("Redis Set Remove:{}", remove);
}
@Test
@DisplayName("Redis Hash 数据类型测试")
public void redisHashTest() {
log.info("Redis Hash 数据类型测试");
Integer key = LocalDateTime.now().getNano();
boolean addItemResult = redisService
.addToHash(HASH_KEY, key, LocalDateTime.now().toString());
log.info("Redis Hash Add item:{}", addItemResult);
redisService.getFromHash(HASH_KEY)
.forEach((k, v) -> log.info("Redis Hash Get After Add Item:{} - {}", k, v.toString()));
log.info("Redis Hash Get By Key:{}", redisService.getFromHash(HASH_KEY, key).toString());
log.info("Redis Hash Size Before Delete:{}", redisService.getFromHash(HASH_KEY).size());
redisService.removeFromHash(HASH_KEY, key);
log.info("Redis Hash Size After Delete:{}", redisService.getFromHash(HASH_KEY).size());
boolean remove = redisService.remove(HASH_KEY);
log.info("Redis Hash Remove:{}", remove);
}
}
为了能够不需要额外配置就能启动项目,看到配置效果。用例采用模拟数据,可自行修改为对应的ORM操作
<properties>
<java-jwt.version>4.4.0</java-jwt.version>
<guava.version>33.0.0-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
默认配置下,Spring Security form表单登录的用户名为user,密码启动时在控制台输出。
编写测试Controller
package com.yiyan.study.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试接口
*/
@RestController
public class SecurityController {
@GetMapping("/hello")
public String hello() {
return "hello spring security";
}
}
访问接口测试
package com.yiyan.study.config;
import lombok.Getter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Spring Security 模拟数据
*/
public class SecurityConstant {
/**
* 模拟用户数据。key:用户名,value:密码
*/
public static final Map<String, String> USER_MAP = new ConcurrentHashMap<>();
/**
* 模拟权限数据。key:接口地址,value:所需权限
*/
public static final Map<String, ConfigAttribute> PERMISSION_MAP = new ConcurrentHashMap<>();
/**
* 用户权限数据。key:用户名,value:权限
*/
public static final Map<String, List<PERMISSION>> USER_PERMISSION_MAP = new ConcurrentHashMap<>();
/**
* 白名单
*/
public static final String[] WHITELIST = {"/login"};
static {
// 填充模拟用户数据
USER_MAP.put("admin", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy");
USER_MAP.put("user", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy");
// 填充用户权限
USER_PERMISSION_MAP.put("admin", List.of(PERMISSION.ADMIN, PERMISSION.USER));
USER_PERMISSION_MAP.put("user", List.of(PERMISSION.USER));
// 填充接口权限
PERMISSION_MAP.put("/user", new SecurityConfig(PERMISSION.USER.getValue()));
PERMISSION_MAP.put("/admin", new SecurityConfig(PERMISSION.ADMIN.getValue()));
}
/**
* 模拟权限
*/
@Getter
public enum PERMISSION {
ADMIN("admin"), USER("user");
private final String value;
private PERMISSION(String value) {
this.value = value;
}
}
}
package com.yiyan.study.config;
import lombok.Builder;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* Spring Security用户信息
*/
@Data
@Builder
public class SecurityUserDetails implements UserDetails {
private String username;
private String password;
private List<SecurityConstant.PERMISSION> permissions;
public SecurityUserDetails(String username, String password, List<SecurityConstant.PERMISSION> permissions) {
this.username = username;
this.password = password;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getValue()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsService
,重写loadUserByUsername
()方法package com.yiyan.study.config;
import io.micrometer.common.util.StringUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SecurityUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 获取用户信息
String password = SecurityConstant.USER_MAP.get(username);
if (StringUtils.isBlank(password)) {
throw new UsernameNotFoundException("用户名或密码错误");
}
// 获取用户权限
List<SecurityConstant.PERMISSION> permission = SecurityConstant.USER_PERMISSION_MAP.get(username);
// 返回SecurityUserDetails
return SecurityUserDetails.builder()
.username(username)
.password(password)
.permissions(permission)
.build();
}
}
package com.yiyan.study.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* JWT工具类
*/
public class JwtUtils {
/**
* 默认JWT标签头
*/
public static final String HEADER = "Authorization";
/**
* JWT配置信息
*/
private static JwtConfig jwtConfig;
private JwtUtils() {
}
/**
* 初始化参数
*
* @param header JWT标签头
* @param tokenHead Token头
* @param issuer 签发者
* @param secretKey 密钥 最小长度:4
* @param expirationTime Token过期时间 单位:秒
* @param issuers 签发者列表 校验签发者时使用
* @param audience 接受者
*/
public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime, List<String> issuers, String audience) {
jwtConfig = new JwtConfig();
jwtConfig.setHeader(StringUtils.isNotBlank(header) ? header : HEADER);
jwtConfig.setTokenHead(tokenHead);
jwtConfig.setIssuer(issuer);
jwtConfig.setSecretKey(secretKey);
jwtConfig.setExpirationTime(expirationTime);
if (CollectionUtils.isEmpty(issuers)) {
issuers = Collections.singletonList(issuer);
}
jwtConfig.setIssuers(issuers);
jwtConfig.setAudience(audience);
jwtConfig.setAlgorithm(Algorithm.HMAC256(jwtConfig.getSecretKey()));
}
/**
* 初始化参数
*/
public static void initialize(String header, String issuer, String secretKey, long expirationTime) {
initialize(header, null, issuer, secretKey, expirationTime, null, null);
}
/**
* 初始化参数
*/
public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime) {
initialize(header, tokenHead, issuer, secretKey, expirationTime, null, null);
}
/**
* 生成 Token
*
* @param subject 主题
* @return Token
*/
public static String generateToken(String subject) {
return generateToken(subject, jwtConfig.getExpirationTime());
}
/**
* 生成 Token
*
* @param subject 主题
* @param expirationTime 过期时间
* @return Token
*/
public static String generateToken(String subject, long expirationTime) {
Date now = new Date();
Date expiration = new Date(now.getTime() + expirationTime * 1000);
return JWT.create()
.withSubject(subject)
.withIssuer(jwtConfig.getIssuer())
.withAudience(jwtConfig.getAudience())
.withIssuedAt(now)
.withExpiresAt(expiration)
.sign(jwtConfig.getAlgorithm());
}
/**
* 获取Token数据体
*/
public static String getTokenContent(String token) {
if (StringUtils.isNotBlank(jwtConfig.getTokenHead())) {
token = token.substring(jwtConfig.getTokenHead().length()).trim();
}
return token;
}
/**
* 验证 Token
*
* @param token token
* @return 验证通过返回true,否则返回false
*/
public static boolean isValidToken(String token) {
try {
token = getTokenContent(token);
Algorithm algorithm = Algorithm.HMAC256(jwtConfig.getSecretKey());
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
// Token验证失败
return false;
}
}
/**
* 判断Token是否过期
*
* @param token token
* @return 过期返回true,否则返回false
*/
public static boolean isTokenExpired(String token) {
try {
token = getTokenContent(token);
Algorithm algorithm = Algorithm.HMAC256(jwtConfig.secretKey);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
Date expirationDate = JWT.decode(token).getExpiresAt();
return expirationDate != null && expirationDate.before(new Date());
} catch (JWTVerificationException exception) {
// Token验证失败
return false;
}
}
/**
* 获取 Token 中的主题
*
* @param token token
* @return 主题
*/
public static String getSubject(String token) {
token = getTokenContent(token);
return JWT.decode(token).getSubject();
}
/**
* 获取当前Jwt配置信息
*/
public static JwtConfig getCurrentConfig() {
return jwtConfig;
}
@Data
public static class JwtConfig {
/**
* JwtToken Header标签
*/
private String header;
/**
* Token头
*/
private String tokenHead;
/**
* 签发者
*/
private String issuer;
/**
* 密钥
*/
private String secretKey;
/**
* Token 过期时间
*/
private long expirationTime;
/**
* 签发者列表
*/
private List<String> issuers;
/**
* 接受者
*/
private String audience;
/**
* 加密算法
*/
private Algorithm algorithm;
}
}
application.yml 添加配置
server:
port: 8080
# ======== JWT配置 ========
jwt:
secret: 1234567890123456
expirationTime: 604800
issuer: springboot3-security
header: Authorization
tokenHead: Bearer
配置JWT启动时加载配置项
package com.yiyan.study.config;
import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* JWT 配置
*/
@Slf4j
@Component
public class JwtConfig {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.issuer}")
private String issuer;
@Value("${jwt.expirationTime}")
private long expirationTime;
@Value("${jwt.header}")
private String header;
@Value("${jwt.tokenHead}")
private String tokenHead;
@PostConstruct
public void jwtInit() {
JwtUtils.initialize(header, tokenHead, issuer, secretKey, expirationTime);
log.info("JwtUtils初始化完成");
}
}
package com.yiyan.study.config;
import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 自定义过滤器
*/
@Component
public class MyAuthenticationFilter extends OncePerRequestFilter {
@Resource
private SecurityUserDetailsService securityUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestToken = request.getHeader(JwtUtils.getCurrentConfig().getHeader());
// 读取请求头中的token
if (StringUtils.isNotBlank(requestToken)) {
// 判断token是否有效
boolean verifyToken = JwtUtils.isValidToken(requestToken);
if (!verifyToken) {
filterChain.doFilter(request, response);
}
// 解析token中的用户信息
String subject = JwtUtils.getSubject(requestToken);
if (StringUtils.isNotBlank(subject) && SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityUserDetails userDetails = (SecurityUserDetails) securityUserDetailsService.loadUserByUsername(subject);
// 保存用户信息到当前会话
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
// 将authentication填充到安全上下文
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
package com.yiyan.study.controller;
import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试接口
*/
@RestController
public class SecurityController {
@Resource
private AuthenticationManager authenticationManager;
@GetMapping("/hello")
public String hello() {
return "hello spring security";
}
@GetMapping("/user")
public String helloUser() {
return "Hello User";
}
@GetMapping("/admin")
public String helloAdmin() {
return "Hello Admin";
}
@PostMapping("/login")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 判断是否验证成功
if (null == authentication) {
throw new UsernameNotFoundException("用户名或密码错误");
}
return JwtUtils.generateToken(username);
}
}
Spring Security 升级到6.x后,配置方式与前版本不同,多个旧的配置类被启用。新版本采用lambda表达式的方式进行配置,核心配置项没变化。
package com.yiyan.study.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Spring Security配置类
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Resource
private UserDetailsService userDetailsService;
@Resource
private MyAuthenticationFilter myAuthenticationFilter;
/**
* 鉴权管理类
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* 加密类
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Spring Security 过滤链
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 禁用明文验证
.httpBasic(AbstractHttpConfigurer::disable)
// 关闭csrf
.csrf(AbstractHttpConfigurer::disable)
// 禁用默认登录页
.formLogin(AbstractHttpConfigurer::disable)
// 禁用默认登出页
.logout(AbstractHttpConfigurer::disable)
// 禁用session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置拦截信息
.authorizeHttpRequests(authorization -> authorization
// 允许所有的OPTIONS请求
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 放行白名单
.requestMatchers(SecurityConstant.WHITELIST).permitAll()
// 根据接口所需权限进行动态鉴权
.anyRequest().access((authentication, object) -> {
// 获取当前的访问路径
String requestURI = object.getRequest().getRequestURI();
PathMatcher pathMatcher = new AntPathMatcher();
// 白名单请求直接放行
for (String url : SecurityConstant.WHITELIST) {
if (pathMatcher.match(url, requestURI)) {
return new AuthorizationDecision(true);
}
}
// 获取访问该路径所需权限
Map<String, ConfigAttribute> permissionMap = SecurityConstant.PERMISSION_MAP;
List<ConfigAttribute> apiNeedPermissions = new ArrayList<>();
for (Map.Entry<String, ConfigAttribute> config : permissionMap.entrySet()) {
if (pathMatcher.match(config.getKey(), requestURI)) {
apiNeedPermissions.add(config.getValue());
}
}
// 如果接口没有配置权限则直接放行
if (apiNeedPermissions.isEmpty()) {
return new AuthorizationDecision(true);
}
// 获取当前登录用户权限信息
Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
// 判断当前用户是否有足够的权限访问
for (ConfigAttribute configAttribute : apiNeedPermissions) {
// 将访问所需资源和用户拥有资源进行比对
String needAuthority = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authorities) {
if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
// 权限匹配放行
return new AuthorizationDecision(true);
}
}
}
return new AuthorizationDecision(false);
})
)
// 注册重写后的UserDetailsService实现
.userDetailsService(userDetailsService)
// 注册自定义拦截器
.addFilterBefore(myAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
JDK 17
Spring Boot 3.2.0
日志门面(如Slf4j)就是一个标准,同JDBC一样来制定“规则”,把不同的日志系统的实现进行了具体的抽象化,只提供了统一的日志使用接口。而Logback、log4j等具体的日志系统就如同MySQL驱动、PGSQL驱动一样,才是日志功能的真正实现。
SpringBoot使用Slf4j作为日志门面,Logback作为默认的日志实现。在SpringBoot的pom.xml中,依赖为:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
此依赖在SpringBoot Starter包中包含,所以在SpringBoot项目中导入 spring-boot-starter
后可直接使用:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
LoggerFactory
创建日志记录器实例import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LoggingApplication {
private static final Logger logger = LoggerFactory.getLogger(LoggingApplication.class);
public static void main(String[] args) {
logger.info("LoggingApplication start...");
SpringApplication.run(LoggingApplication.class, args);
logger.info("LoggingApplication end...");
}
}
lombok
依赖,通过注解@Slf4j
创建日志记录器实例<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
public class LoggingApplication {
public static void main(String[] args) {
log.info("LoggingApplication start...");
SpringApplication.run(LoggingApplication.class, args);
log.info("LoggingApplication end...");
}
}
SpringBoot推荐将配置文件名称命名为logback-spring.xml
表示这是SpringBoot下Logback专用的配置,可以使用SpringBoot
的高级Profile功能,它的内容类似于这样:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 配置信息 -->
</configuration>
最外层由configuration
包裹,一旦编写,那么就会替换默认的配置,所以如果内部什么都不写的话,那么会导致我们的SpringBoot项目没有配置任何日志输出方式,控制台也不会打印日志。
也可在配置文件中指定要使用的日志配置,如
application.yml
中通过配置logging.config
指定要使用的日志配置。
logging:
config: classpath:logback-spring.xml
SpringBoot 的默认Logback文件可在依赖项中找到。路径为:
org/springframework/boot/logging/logback/defaults.xml
在配置文件中对默认配置进行引用,及编写定制配置覆盖默认配置。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引用Spring Boot 默认日志配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 控制台日志打印格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="MyLogPattern %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr(${LOGGED_APPLICATION_NAME:-}[%15.15t]){faint} %clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 日志输出到控制台 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${CONSOLE_LOG_THRESHOLD}</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>${CONSOLE_LOG_CHARSET}</charset>
</encoder>
</appender>
<!-- 指定日志输出级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
logback-spring.xml
添加如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引用Spring Boot 默认日志配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 日志文件输出位置 -->
<property name="LOG_PATH" value="./logs"/>
<!-- 日志文件名 -->
<property name="LOG_FILE" value="${LOG_PATH}/spring-boot.log"/>
<!-- 日志输出到文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${FILE_LOG_THRESHOLD}</level>
</filter>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${FILE_LOG_CHARSET}</charset>
</encoder>
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}
</fileNamePattern>
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7}</maxHistory>
</rollingPolicy>
</appender>
<!-- 指定日志输出级别,以及启动的Appender -->
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
MDC
全称Mapped Diagnostic Context
(映射诊断上下文),“映射诊断上下文” 本质上是由日志记录框架维护的映射,其中应用程序代码提供键值对,然后可以由日志记录框架将其插入日志消息中。MDC数据在过滤消息或触发某些操作时也非常有用。通俗的说就是可以将特定的信息(如请求Id,链路Id),通过MDC机制注入到当前日志线程的上下文中,将信息在日志中记录、输出。
MDC 常用的三个方法:
MDC注入属性配置:
在logback-spring.xml
文件的输出格式中添加需要注入的KEY信息与代码注入的信息相对应。假定我需要注入当前用户的Id信息,且设置key为user-id
代码:
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.UUID;
@Slf4j
@SpringBootApplication
public class LoggingApplication {
public static void main(String[] args) {
SpringApplication.run(LoggingApplication.class, args);
String mdcKey = "user-id";
log.info("Logging before MDC put...");
MDC.put("mdcKey", UUID.randomUUID().toString());
log.info("Logging after MDC put...");
MDC.clear();
log.info("Logging after MDC clear...");
}
}
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引用Spring Boot 默认日志配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 控制台日志打印格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="[%X{user-id}] %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr(${LOGGED_APPLICATION_NAME:-}[%15.15t]){faint} %clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 日志输出到控制台 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${CONSOLE_LOG_THRESHOLD}</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>${CONSOLE_LOG_CHARSET}</charset>
</encoder>
</appender>
<!-- 指定日志输出级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
使用方式:
- 通过注册拦截器或过滤器,在业务开始前获取到相关信息(请求Id,操作人Id),将相关信息通过MDC自动注入到当前线程中,实现关键信息记录。
- 在创建子线程时,需要通过
MDC.getCopyOfContextMap()
方法获取到当前线程的MDC注入相关信息,传递给子线程,才能在子线打印日志信息时获取到父线程MDC的注入信息。
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- Spring Boot 默认日志配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 日志文件输出位置 -->
<property name="LOG_PATH" value="./logs"/>
<!-- 日志文件名 -->
<property name="LOG_FILE" value="${LOG_PATH}/spring-boot.log"/>
<!-- 归档日志名 -->
<property name="FILE_NAME_PATTERN" value="${LOG_FILE}-%d{yyyyMMdd}.%i.gz"/>
<!-- 在启动时清除历史日志 -->
<property name="LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START" value="false"/>
<!-- 单个日志文件最大大小 -->
<property name="LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE" value="100MB"/>
<!-- 日志总大小 -->
<property name="LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP" value="0"/>
<!-- 日志保留天数 -->
<property name="LOGBACK_ROLLINGPOLICY_MAX_HISTORY" value="7"/>
<!-- 控制台日志编码格式 -->
<property name="CONSOLE_LOG_CHARSET" value="UTF-8"/>
<!-- 控制台日志打印格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} [%X{user-id}] [%X{request-id}] [%X{client-ip}] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr(${LOGGED_APPLICATION_NAME:-}[%15.15t]){faint} %clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 控制台日志输出级别 -->
<property name="CONSOLE_LOG_THRESHOLD" value="INFO"/>
<!-- 日志文件输出打印格式 -->
<property name="FILE_LOG_PATTERN"
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} [%X{user-id}] [%X{request-id}] [%X{client-ip}] ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- ${LOGGED_APPLICATION_NAME:-}[%t] ${LOG_CORRELATION_PATTERN:-}%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- 日志文件输出编码格式 -->
<property name="FILE_LOG_CHARSET" value="UTF-8"/>
<!-- 日志文件输出级别 -->
<property name="FILE_LOG_THRESHOLD" value="INFO"/>
<!-- 日志输出到控制台 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${CONSOLE_LOG_THRESHOLD}</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>${CONSOLE_LOG_CHARSET}</charset>
</encoder>
</appender>
<!-- 日志输出到文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${FILE_LOG_THRESHOLD}</level>
</filter>
<!-- 日志打印配置 -->
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${FILE_LOG_CHARSET}</charset>
</encoder>
<!-- 日志文件名 -->
<file>${LOG_FILE}</file>
<!-- 日志输出配置 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志归档名 -->
<fileNamePattern>${FILE_NAME_PATTERN}</fileNamePattern>
<!-- 在启动时清除日志 -->
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START}</cleanHistoryOnStart>
<!-- 单个文件大小 -->
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE}</maxFileSize>
<!-- 日志总大小 -->
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP}</totalSizeCap>
<!-- 日志保留天数 -->
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY}</maxHistory>
</rollingPolicy>
</appender>
<!-- 指定日志输出级别,以及启动的Appender -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
工程源码:Gitee
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<springdoc.version>2.3.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
server:
port: 8080
# ======== SpringDocs文档配置 ========
springdocs:
title: SpringBoot3.2.0 API Docs
description: SpringBoot3 + OpenAPI Docs
version: 0.0.1
scheme: Bearer
header: Authorization
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "springdocs")
@Setter
@Getter
public class SpringDocsProperties {
private String title;
private String description;
private String version;
private String header;
private String scheme;
}
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* SpringDoc配置文件
*/
@Configuration
public class SpringDocConfig {
@Resource
private SpringDocsProperties springDocsProperties;
@Bean
public OpenAPI openApi() {
return new OpenAPI()
// 文档描述信息
.info(new Info()
.title(springDocsProperties.getTitle())
.description(springDocsProperties.getDescription())
.version(springDocsProperties.getVersion())
)
// 添加全局的header参数
.addSecurityItem(new SecurityRequirement()
.addList(springDocsProperties.getHeader()))
.components(new Components()
.addSecuritySchemes(springDocsProperties.getHeader(), new SecurityScheme()
.name(springDocsProperties.getHeader())
.scheme(springDocsProperties.getScheme())
.bearerFormat("JWT")
.type(SecurityScheme.Type.HTTP))
);
}
}
@Api
→ @Tag
@ApiIgnore
→ @Parameter(hidden = true)
or @Operation(hidden = true)
or @Hidden
@ApiImplicitParam
→ @Parameter
@ApiImplicitParams
→ @Parameters
@ApiModel
→ @Schema
@ApiModelProperty(hidden = true)
→ @Schema(accessMode = READ_ONLY)
@ApiModelProperty
→ @Schema
@ApiOperation(value = "foo", notes = "bar")
→ @Operation(summary = "foo", description = "bar")
@ApiParam
→ @Parameter
@ApiResponse(code = 404, message = "foo")
→ @ApiResponse(responseCode = "404", description = "foo")
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。