SpringBoot 项目配置
项目目录
demo
├── common
│ ├── base
│ │ ├── ResultCode.java
│ │ ├── ResultGenerator.java
│ │ └── Result.java
│ ├── security # 安全相关
│ │ ├── CustomUserDetails.java
│ │ ├── jwt
│ │ │ ├── JWTAuthenticationFilter.java
│ │ │ ├── JWTAuthEntryPoint.java
│ │ │ └── JWTUtil.java
│ │ └── UserDetailsServiceImpl.java
│ └── utils # 纯工具类(静态方法)
│ ├── crypto
│ │ └── SM4Util.java
│ ├── IpUtils.java
│ └── RandomUtil.java
├── configuration # 配置类
│ ├── Log4jConfiguration.java
│ ├── MybatisPlusConfig.java
│ ├── OpenApiConfig.java
│ ├── RequestLoggingConfig.java
│ ├── SecurityConfig.java
│ └── WebSocketConfig.java
├── controller # 控制器
│ └── UserController.java
├── enums # 枚举
│ ├── MyEnum.java
│ └── WSMessageEnum.java
├── DemoApplication.java
├── mapper # Mapper
│ └── use
│ └── UserMapper.java
├── pojo
│ ├── dto
│ │ ├── UserCreateDTO.java
│ │ ├── UserLoginDTO.java
│ │ └── UserUpdateDTO.java
│ ├── entity
│ │ └── user
│ │ └── User.java
│ └── vo
│ └── UserVO.java
├── service
│ ├── impl
│ │ └── UserServiceImpl.java
│ └── IUserService.java
└── websocket # websocket模块
├── MessageDispatcher.java
├── MessageEntity.java
├── MessageHandler.java
├── WebSocketService.java
└── WebSocketSHandler.java
pom.xml
<dependencies>
<!-- Web + 排除默认日志 -->
<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-validation</artifactId>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jakarta-web</artifactId>
<version>2.24.3</version> <!-- 或最新版,如 2.24.3 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 如果使用 Lettuce 连接池(可选) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- JDBC 支持(推荐显式添加) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<!-- 此配置在启动 Spring Boot 服务时,需要配置连接数据库 -->
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus for Spring Boot 3 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT (推荐使用 Auth0 的 Java-JWT) -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version> <!-- 最新版,请根据需要调整 -->
</dependency>
<!-- Swagger / OpenAPI UI (Springdoc for Spring Boot 3) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version> <!-- 兼容 Spring Boot 3 -->
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.9</version>
</dependency>
<!-- SM4 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
</dependencies>
applcation.yml
# 应用基本信息
spring:
application:
name: SpringBootDemoBase
profiles:
active: dev
# 数据源配置(MySQL)
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
pool-name: MyHikariCP
# Redis 配置
# data:
# redis:
# host: localhost
# port: 6379
# # password: your_redis_password # 如有密码请取消注释
# database: 0
# timeout: 2000ms
# lettuce:
# pool:
# max-active: 8
# max-idle: 8
# min-idle: 0
# max-wait: -1ms
# 服务器配置
server:
port: 18080
servlet:
context-path: /api # 注意上下文路径
websocket:
config:
endpoint: /ws
allowed-origins: "*"
buffer-size: 8192
timeout: 30000
# application.yml
mybatis-plus:
# mapper-locations: "classpath*:mapper/**/*.xml" # Mapper.xml文件地址,默认值
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
cache-enabled: false # 是否开启二级缓存
# 日志配置(Log4j2)
logging:
config: classpath:log4j2-dev.xml
# 注意:详细日志规则应在 log4j2-spring.xml 中定义,此处仅指定配置文件位置
# Swagger / OpenAPI (Springdoc)
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
packages-to-scan: org.example.springbootdemobase.manage.controller # 扫描位置
#e.g : 访问地址:http://localhost:8080/api/swagger-ui.html(注意上下文路径)
# JWT 自定义配置(供代码中注入使用)
jwt:
secret: yourSuperSecretKeyChangeItInProduction1234567890 # 至少 256 位
expiration: 86400 # 24 小时(秒)
# SM4
sm4:
key: 1234567890abcdef # 16 字节(128 位
统一响应
common/base/Result.java
public class Result <T> {
private int code;
private String message;
private T data;
private LocalDateTime time;
public Result() {
this.time = LocalDateTime.now();
}
public Result setCode(int code) {
this.code = code;
return this;
}
public Result setMessage(String message) {
this.message = message;
return this;
}
public Result setData(T data) {
this.data = data;
return this;
}
public Result setTime(LocalDateTime time) {
this.time = time;
return this;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public T getData() {
return data;
}
public LocalDateTime getTime() {
return time;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
common/base/ResultCode.java
public enum ResultCode {
SUCCESS(200, "成功"),
FAILED(400, "失败"),
UNAUTHORIZED(401, "认证失败"),
NOT_FOUND(404, "未找到"),
FORBIDDEN(403, "无权限访问"),
INTERNAL_SERVER_ERROR(500, "服务端错误");
private final int code;
private final String message;
private ResultCode(int code, String msg) {
this.code = code;
this.message = msg;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
common/base/ResultGenerator.java
public class ResultGenerator {
public static Result success() {
return new Result()
.setCode(ResultCode.SUCCESS.getCode())
.setMessage(ResultCode.SUCCESS.getMessage());
}
public static <T> Result success(T data) {
return new Result<>()
.setCode(ResultCode.SUCCESS.getCode())
.setMessage(ResultCode.SUCCESS.getMessage())
.setData(data);
}
public static Result falied() {
return new Result()
.setCode(ResultCode.FAILED.getCode())
.setMessage(ResultCode.FAILED.getMessage());
}
public static Result falied(String message) {
return new Result()
.setCode(ResultCode.FAILED.getCode())
.setMessage(message);
}
public static Result error() {
return new Result()
.setCode(ResultCode.INTERNAL_SERVER_ERROR.getCode())
.setMessage(ResultCode.INTERNAL_SERVER_ERROR.getMessage());
}
}
日志配置
Log4jConfiguration.java
/**
* Log4j2在web中的过滤器 过滤REQUEST, FORWARD, INCLUDE, ERROR, ASYNC类型请求
*
* @author
* @date
*/
@Configuration
public class Log4jConfiguration {
@Bean
public FilterRegistrationBean log4jServletFilter() {
FilterRegistrationBean<Log4jServletFilter> filterRegistrationBean = new FilterRegistrationBean<>(new Log4jServletFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(REQUEST, FORWARD, INCLUDE, ERROR, ASYNC);
return filterRegistrationBean;
}
}
RequestLoggingConfig.java
@Configuration
public class RequestLoggingConfig {
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true); // 包含客户端 IP 和 session ID
loggingFilter.setIncludeQueryString(true); // 包含查询参数
loggingFilter.setIncludePayload(true); // 包含请求体(谨慎使用,可能泄露敏感数据)
loggingFilter.setMaxPayloadLength(1000); // 设置最大 payload 长度
loggingFilter.setIncludeHeaders(false); // 一般不需要头信息(可选)
return loggingFilter;
}
}
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
status : 这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成TRACE时,会看到log4j2内部各种详细输出
monitorInterval : Log4j能够自动检测修改配置文件和重新配置本身, 设置间隔秒数。此处表示每隔几秒重读一次配置文件.
日志级别:TRACE < DEBUG < INFO < WARN < ERROR < FATAL
如果设置为WARN,则低于WARN的信息都不会输出
-->
<Configuration status="INFO" monitorInterval="30">
<!-- 参数配置 -->
<Properties>
<!-- 配置日志文件输出目录 -->
<Property name="LOG_HOME">./logs</Property>
<!-- 日志输出文件名 -->
<property name="FILE_NAME">dev</property>
<!-- 日志格式化 -->
<property name="console_pattern_layout">
%highlight{%d{yyyy-MM-dd HH:mm:ss.SSS} [%5level][%logger{36}]-(%t)} %m%n
</property>
<property name="pattern_layout">
%d{yyyy-MM-dd HH:mm:ss.SSS} [%5level][%logger{36}]-(%t) %m%n
</property>
</Properties>
<!-- 日志配置Appender -->
<Appenders>
<!-- 输出控制台的配置 -->
<Console name="Console" target="SYSTEM_OUT">
<!-- ThresholdFilter:配置的日志过滤
如果要输出的日志级别在当前级别及以上,则为match,否则走mismatch
ACCEPT: 执行日志输出;DENY: 不执行日志输出,结束过滤;NEUTRAL: 不执行日志输出,执行下一个过滤器 -->
<!--<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>-->
<!-- 日志输出的格式
%d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间,输出到毫秒的时间
%-5p (level) : 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0
%c (logger) : logger的名称(%logger)
%t (thread) : 输出当前线程名称
%m : 日志内容,即 logger.info("message")
%n : 换行符
%C : Java类名(%F)
%L : 行号
%M : 方法名
%l : 输出语句所在的行数, 包括类名、方法名、文件名、行数
hostName : 本地机器名
hostAddress : 本地ip地址
-->
<PatternLayout pattern="${console_pattern_layout}"/>
</Console>
<!-- 文件输出配置,文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用 -->
<!--<File name="log" fileName="log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>-->
<!--
循环日志文件配置:日志文件大于阀值的时候,就开始写一个新的日志文件
这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档
fileName : 指定当前日志文件的位置和文件名称
filePattern : 指定当发生Rolling时,文件的转移和重命名规则
SizeBasedTriggeringPolicy : 指定当文件体积大于size指定的值时,触发Rolling
DefaultRolloverStrategy : 指定最多保存的文件个数
TimeBasedTriggeringPolicy : 这个配置需要和filePattern结合使用
注意filePattern中配置的文件重命名规则是${FILE_NAME}_%d{yyyy-MM-dd}_%i,最小的时间粒度是dd,即天,
TimeBasedTriggeringPolicy指定的size是1,结合起来就是每1天生成一个新文件
-->
<RollingRandomAccessFile name="ALL"
fileName="${LOG_HOME}/${FILE_NAME}.log"
filePattern="${LOG_HOME}/${FILE_NAME}.log.%d{yyyy-MM-dd}_%i">
<!--<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>-->
<PatternLayout pattern="${pattern_layout}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<!--<SizeBasedTriggeringPolicy size="100MB"/>-->
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>
<!-- 异步日志配置 -->
<Async name="AsyncAll">
<AppenderRef ref="Console"/>
<AppenderRef ref="ALL"/>
</Async>
</Appenders>
<!-- 日志记录Logger -->
<Loggers>
<Logger name="druid.sql" level="DEBUG" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<Logger name="druid.sql.Connection" level="INFO" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<Logger name="net.sf.ehcache" level="INFO" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<Logger name="org.springframework.web.filter.CommonsRequestLoggingFilter"
level="DEBUG" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<!--
Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。
level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.
name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点.
AppenderRef:Logger的子节点,用来指定该日志输出到哪个Appender,如果没有指定,就会默认继承自Root.
如果指定了,那么会在指定的这个Appender和Root的Appender中都会输出,
此时我们可以设置Logger的additivity="false"只在自定义的Appender中进行输出。
-->
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="ALL"/>
</Root>
</Loggers>
</Configuration>
application.yml
# 日志配置(Log4j2)
logging:
config: classpath:log4j2-dev.xml
# 注意:详细日志规则应在 log4j2-dev.xml 中定义,此处仅指定配置文件位置
Swagger配置
OpenApiConfig.java
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("springbootdemo API")
.version("1.0.0")
.description("API documentation for springbootdemo")
.contact(new Contact()
.name("Developer")
.email("developer@springbootdemo.com")));
}
}
application.yml
# Swagger / OpenAPI (Springdoc)
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
packages-to-scan: org.example.springbootdemobase.manage.controller # 扫描位置
#e.g : 访问地址:http://localhost:8080/api/swagger-ui.html(注意上下文路径)
MyBatisPlus分页配置
MybatisPlusConfig.java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件 DbType要选择使用的数据库类型,如MySQL、Oracle等。不同数据库的分页语法不同,插件会自动适配
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
使用
1. 创建Page对象,指定当前页和每页记录数
Page<User> page = new Page<>(1, 10); // 第1页,每页10条
2. 执行分页查询
// 创建条件构造器
LambdaQueryWrapper<User> query = Wrappers.<User>lambdaQuery();
query.gt(User::getAge, 20)
.like(User::getName, "张");
Page<User> userPage = userMapper.selectPage(page, query);
3. 获取分页结果
List<User> users = userPage.getRecords(); // 当前页数据
long total = userPage.getTotal(); // 总记录数
long pages = userPage.getPages(); // 总页数
long current = userPage.getCurrent(); // 当前页
long size = userPage.getSize(); // 每页记录数
JWT
mapper/UserMapper.java
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
entity/user/User.java
@Data
@TableName("user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "username")
private String username;
@TableField(value = "password")
private String password;
@TableField(value = "role_id") // 外键
private Long roleId;
/**
* 用户角色信息(非表字段,只做用户角色数据信息存储使用)
*/
// @Transient //注解用于标记实体类的字段为非数据库字段 不会被 JSON 序列化或 Java 序列化包含。
// 与 使用 @TableField(exist = false) - 效果相同
@TableField(exist = false)
private Role role;
@TableField(exist = false)
private String roleName;
}
common/security/jwt/JWTUtil.java
import com.demo.pojo.entity.user.User;
@Component
public class JWTUtil {
@Value("${jwt.secret}")
private String SECRET;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据输入的user生成JWT令牌
* 可以创建一个带有过期时间及指定算法的签名令牌
*
* @param user 包含令牌payload数据的键值对,其中键是claim名称,值是claim值
* @return Token JWT令牌
*/
public String createToken(User user) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
// 创建Token构建器
JWTCreator.Builder builder = JWT.create();
// 设置payload中的claim,使用user的username,password设置
builder.withClaim("username",user.getUsername());
builder.withClaim("id",user.getId());
// 生成带有过期时间的token,并使用HMAC256算法进行签名
String token = builder.withExpiresAt(expiryDate)
.sign(Algorithm.HMAC256(SECRET));
return token;
}
public DecodedJWT verifyToken(String token) {
return JWT.require(Algorithm.HMAC256(SECRET))
.build().verify(token);
}
}
common/security/jwt/JWTInterceptors.java
/**
*
* JWT 拦截器
*
* 与CorsConfig配合使用
*
* 若使用了 springboot Security, 此类可以弃用
*
* */
@Slf4j
public class JWTInterceptors implements HandlerInterceptor {
private JWTUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Result res;
// 获取请求头中令牌
String token = request.getHeader("Authorization");
// log.info("token:{}", token);
try {
// 验证令牌
DecodedJWT jwt = jwtUtil.verifyToken(token);
String username = jwt.getClaim("username").asString();
// ---- 认证通过后,接口中拿到登录用户的两种方法 ----
// 方式一:
// 步骤:1 将 username 放入 request 属性中,供后续 controller 使用
// 步骤:2 controller中使用 @RequestAttribute("username") String username 直接注入即可在接口中拿到当前登录的用户
request.setAttribute("username", username);
// 方式二:使用 ThreadLocal (适合多层调用)
// 步骤:1 创建一个 LoginUserHolder 工具类
// 步骤:2 设置当前登录用户 LoginUserHolder.setUsername(username)
// 步骤:3 在需要获取当前登录用户的地方使用 LoginUserHolder.getUsername() 获取当前登录用户
// 步骤:4 在 afterCompletion 或 finally 中清理
return true; // 放行请求
} catch (SignatureVerificationException e) {
res = ResultGenerator.unauthorized();
} catch (TokenExpiredException e) {
res = ResultGenerator.unauthorized();
} catch (Exception e) {
res = ResultGenerator.unauthorized();
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(res.toString());
return false;
}
// 接口请求离开后做一些清理工作
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// LoginUserHolder.clear(); // 防止内存泄漏
}
}
/**
* ThreadLocal(适合多层调用)
* 存储、获取当前登录的用户
* */
public LoginUserHolder {
private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
public static void setUsername(String username) {
userThreadLocal.set(username);
}
public static String getUsername() {
return userThreadLocal.get();
}
public static void clear() {
userThreadLocal.remove();
}
}
configuration/CorsConfig.java
/**
*
*
* 若使用了 springboot Security, CorsConfig 可弃用
*
* */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
// CORS 配置方法
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
// .allowedOriginPatterns("http://localhost:8000", "http://127.0.0.1:*") // allowedOriginPatterns("*") + allowCredentials(true) 的兼容性问题
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptors())
.addPathPatterns("/**") // 拦截所有 注意: Spring MVC 的拦截器路径匹配 不包含 server.servlet.context-path
.excludePathPatterns(
"/user/login", // 忽略登录接口
"/swagger-ui/**", // Swagger
"/v1/api-docs/**" // Swagger
);
}
}
springboot Security
jwt认证 + 接口权限控制
common/security/CustomUserDetails.java
/**
*
* springboot Security
* 实现 UserDetails
*
* **/
@Slf4j
@Data
public class CustomUserDetails implements UserDetails {
private Long userId;
private String username;
private String password;
private String roleName;
private Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(User user) {
this.userId = user.getId();
this.username = user.getUsername();
this.password = user.getPassword();
// 注意:Spring Security 角色需以 ROLE_ 开头
// 数据库中的 role_name 是小写 "admin" 或 "root"
// 转换为 Spring Security 格式:ROLE_ADMIN 或 ROLE_ROOT
String dbRoleName = user.getRole().getRoleName();
this.roleName = "ROLE_" + dbRoleName.toUpperCase(); // 统一转为大写
log.info("用户角色转换: {} -> {}", dbRoleName, this.roleName);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority(roleName));
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}
}
common/security/UserDetailsServiceImpl.java
/**
* springboot Security
* 实现 UserDetailsService
* **/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* 数据库查询用户
* */
User user = userService.queryByUserName(username);
return new CustomUserDetails(user);
}
}
common/security/jwt/JWTAuthenticationFilter.java
/**
*
* springboot Security
* 实现 OncePerRequestFilter 认证过滤
*
* */
@Slf4j
@Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中令牌
String authorization = request.getHeader("Authorization");
log.info("开始JWT认证过滤,Authorization: {}", authorization);
// 检测Authorization是否为空或格式是否正确
if (authorization == null || !authorization.startsWith("token ")) {
log.info("未找到有效token,跳过JWT认证");
filterChain.doFilter(request, response);
return;
}
// 去掉前缀 "token "
String token = authorization.substring(6);
try {
// 验证令牌
DecodedJWT jwt = jwtUtil.verifyToken(token);
String username = jwt.getClaim("username").asString();
log.info(token);
log.info(username);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 创建Authentication对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("用户 {} 认证成功,角色: {}", username, userDetails.getAuthorities());
// 1 将 username 放入 request 属性中,供后续 controller 使用
// 2 controller中使用 @RequestAttribute("username") String username 直接注入即可在接口中拿到当前登录的用户
request.setAttribute("username", username);
// 继续执行过滤器链
filterChain.doFilter(request, response);
} catch (Exception e) {
log.info(e.getMessage());
filterChain.doFilter(request, response);
}
}
}
common/security/jwt/JWTAuthEntryPoint.java
/**
* springboot Security
* 当未登录或 token 失效时,返回 401
*/
@Component
public class JWTAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Result result = ResultGenerator.unauthorized();
response.getWriter().println(result.toString());
}
}
configuration/SecurityConfig.java
/**
*
* springboot Security: jwt认证 + 接口权限控制
*
* */
@Slf4j
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 启用 @PreAuthorize 方法级安全控制
public class SecurityConfig {
@Autowired
private JWTAuthEntryPoint unauthorizedHandler;
@Autowired
private JWTAuthenticationFilter jwtAuthenticationFilter;
// 添加权限拒绝处理器, 若不加会出现用户权限不足导致静默失败,Spring Security 会拦截请求但可能没有正确返回错误信息
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> {
log.warn("权限不足: {}", accessDeniedException.getMessage());
log.warn("请求路径: {}", request.getRequestURI());
log.warn("当前用户权限: {}", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Result result = ResultGenerator.genFailResult(ResultCode.FORBIDDEN, "权限不足");
response.getWriter().println(result.toString());
};
}
/**
* Spring Security 要创建一个怎样的 安全过滤器链(Security Filter Chain)
* 把 JWTAuthenticationFilter 插入到 UsernamePasswordAuthenticationFilter 之前执行
* */
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF
.csrf(csrf -> csrf.disable())
// 会话管理
.exceptionHandling(eh -> eh
// 认证失败处理
.authenticationEntryPoint(unauthorizedHandler)
// 权限不足处理
.accessDeniedHandler(accessDeniedHandler())
)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 认证配置
.authorizeHttpRequests(authz -> authz
// 公开接口 不包含 server.servlet.context-path
.requestMatchers(
"/user/login",
"/user/register",
"/swagger-ui.html",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/error"
).permitAll()
// ================
// 注意有优先级: 方法级注解 > 配置类中的匹配规则
// 是 “叠加判断” —— 用户必须同时满足 配置类中的权限要求 和 方法层面的权限要求!
// 推荐策略 :统一使用方法级注解(推荐),只使用一种即可
// 管理员接口
//.requestMatchers("/admin/**").hasRole("ROOT")
// 用户接口(需要认证)
//.requestMatchers("/user/**").hasAnyRole("ROOT", "ADMIN")
// ================
// 其他接口需要认证
.anyRequest().authenticated()
)
// 添加JWT过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
方法级注解 控制接口权限案例
// 注意:不需要写 ROLE_ 前缀
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/admin/reset")
public Result resetPassword() { ... }
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
@GetMapping("/profile")
public Result profile() { ... }
认证流程
HTTP Request → Tomcat → DispatcherServlet
↓
Spring Security Filter Chain 执行(按顺序)
↓
[1] CORS Filter?
[2] CSRF Filter (已禁用)
[3] JWTAuthenticationFilter.doFilterInternal() ← 先执行这个
[4] UsernamePasswordAuthenticationFilter
[5] AnonymousAuthenticationFilter
[6] ExceptionTranslationFilter
[7] FilterSecurityInterceptor → 检查权限(.permitAll() 在这里起作用)
↓
到达 Controller
SM4
common/utils/SM4Util.java
@Component
public class SM4Util {
private static String KEY;
static {
// 注册 Bouncy Castle Provider(只需一次)
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
// SM4 密钥必须为 16 字节(128 位)
@Value("${sm4.key}")
private void setKey(String key){
KEY = key;
}
/**
* SM4 加密(ECB 模式,PKCS5Padding)
*/
public static String encrypt(String plainText) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "SM4");
Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted); // 返回 Base64 字符串
}
/**
* SM4 解密(ECB 模式,PKCS5Padding)
*/
public static String decrypt(String cipherText) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "SM4");
Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
}
// 测试
// public static void main(String[] args) throws Exception {
// String original = "Hello, 国密 SM4!";
// System.out.println("原文: " + original);
//
// String encrypted = encrypt(original);
// System.out.println("密文 (Base64): " + encrypted);
//
// String decrypted = decrypt(encrypted);
// System.out.println("解密后: " + decrypted);
//
// assert original.equals(decrypted) : "解密失败!";
// System.out.println("✅ 加解密成功!");
// }
}
Websocket配置
WebSocketConfig.java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Value("${websocket.config.allowed-origins:*}")
private String[] allowedOrigins;
@Value("${websocket.config.endpoint:/ws}")
private String endpoint;
// 声明 WebSocketSHandler 为一个 Spring Bean
@Bean
public WebSocketSHandler webSocketSHandler() {
return new WebSocketSHandler();
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketSHandler(), endpoint + "/v1")
.setAllowedOrigins(allowedOrigins) // 允许跨域
.setAllowedOriginPatterns(allowedOrigins);
// 可以配置多个路径
// registry.addHandler(new WebSocketSHandler(), "/ws/v2")
// .setAllowedOrigins("*") // 允许跨域
// .setAllowedOriginPatterns("*");
}
}
WebSocketSHandler.java
@Slf4j
@Component
public class WebSocketSHandler extends TextWebSocketHandler {
@Autowired
private WebSocketService webSocketService;
/**
* 连接建立后调用
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 从连接参数中获取wsId(设备的disk作为连接通道唯一值)
String wsId = extractwsId(session);
if (wsId != null) {
// 绑定deviceDisk和session
session.getAttributes().put("wsId", wsId);
webSocketService.registerSession(wsId,session);
log.info("设备 " + wsId + " 连接建立,Session ID: " + session.getId());
return;
}
log.info("匿名设备 " + " 连接建立,不做记录,Session ID: " + session.getId());
}
/**
* 连接关闭后调用
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String wsId = (String) session.getAttributes().get("wsId");
if (wsId != null) {
webSocketService.removeSession(wsId);
log.info("设备 " + wsId + " 连接关闭,Session ID: " + session.getId());
}
}
/**
* 接收客户端消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
webSocketService.handleTextMessage(session, message);
}
@Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
super.handlePongMessage(session, message);
}
/**
* 从WebSocket连接中提取wssn
* 有多种方式可以实现:
*/
private String extractwsId(WebSocketSession session) {
// 方式1:从查询参数获取(ws://localhost:18080/ws/v1/?wsId=12333333333333)
String query = session.getUri().getQuery();
if (query != null && query.contains("wsId=")) {
return query.split("wsId=")[1].split("&")[0];
}
// 方式2:从HTTP Session获取(需要前端在连接时携带认证信息)
// 这需要更复杂的认证配置
// 方式3:从Token获取
// 可以实现WebSocket握手拦截器进行JWT验证
return "anonymous_" + session.getId(); // 默认匿名设备
}
}
WebSocketService.java
@Component
public class WebSocketService {
// 存储 设备 -> session 映射 (生成环境使用redis存储)
private final ConcurrentHashMap<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
public void registerSession(String wsId, WebSocketSession session) {
sessionMap.put(wsId, session);
}
public void removeSession(String wsId) {
sessionMap.remove(wsId);
}
public void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
log.info("收到消息: " + payload + " from " + session.getId());
sendMessageToSession(session, "服务器已收到你的消息: " + payload);
}
/**
* 广播
* */
public void sendMessageToAll(String message) {
for (WebSocketSession session : sessionMap.values()) {
if (session != null && session.isOpen()) {
try {
TextMessage textMessage = new TextMessage(message);
session.sendMessage(textMessage);
}catch (IOException e){
// 发送失败的session
log.info("发送消息失败: " + e.getMessage());
}
}
}
}
/**
* 点对点 向目标设备发送消息
* */
public boolean sendMessageToDevice(String wsId, String message) {
WebSocketSession session = sessionMap.get(wsId);
if (session != null && session.isOpen()) {
try {
TextMessage textMessage = new TextMessage(message);
session.sendMessage(textMessage);
return true;
}catch (IOException e){
// 发送失败的session
sessionMap.remove(wsId);
log.info("向设备 " + wsId + " 发送消息失败: " + e.getMessage());
}
}
return false;
}
/**
* 点对点 向目标session发送消息
* */
public boolean sendMessageToSession(WebSocketSession session, String message) {
if (session != null && session.isOpen()) {
try {
TextMessage textMessage = new TextMessage(message);
session.sendMessage(textMessage);
return true;
}catch (IOException e){
log.info("向session " + session.getId() + " 发送消息失败: " + e.getMessage());
}
}
return false;
}
}
案例:ws://127.0.0.1:18080/api/ws/v1?wsId=xxxxxxxxxxxxxxxxxxxxxxxx
注意:security需要放行 /ws/**
全局异常处理
package com.demo.common.exception;
import com.demo.common.base.Result;
import com.demo.common.base.ResultGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
@Slf4j
@RestControllerAdvice
public class GlobalException{
/**
* 400:参数校验错误(常见于 @Valid / BindingResult)
* */
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handleValidationException(MethodArgumentNotValidException e) {
String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
log.warn("请求参数校验错误: {}", errorMsg);
return ResultGenerator.falied(errorMsg);
}
/**
* 处理404错误 - 资源未找到
* 注意:这个方法需要通过 @ControllerAdvice 捕获 NoHandlerFoundException
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<Void> handleNotFound(NoHandlerFoundException e) {
log.warn("请求的资源不存在: {} {}", e.getHttpMethod(), e.getRequestURL());
return ResultGenerator.notFound();
}
/**
* 处理405错误 - Method Not Allowed
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Result<Void> handleMethodNotAllowed(HttpRequestMethodNotSupportedException e) {
log.warn("请求的方法不允许: {}", e.getMessage());
return ResultGenerator.methodNotAllowed();
}
/**
* 处理业务异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Result<Void>> handleBusinessException(HttpMessageNotReadableException e) {
log.warn("业务异常: {}", e.getMessage());
return new ResponseEntity<>(ResultGenerator.falied("业务异常: " + e.getMessage()), HttpStatus.BAD_REQUEST);
}
/**
* 处理所有未捕获的异常 - 500错误
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleGlobalException(Exception e) {
log.error("服务器内部错误: {}", e.getMessage(), e);
return ResultGenerator.error();
}
/**
* 处理Spring Security相关异常
* 这些异常通常已经在SecurityConfig中处理了,这里作为后备
*/
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Result<Void> handleAuthenticationException(AuthenticationException e) {
log.warn("认证失败: {}", e.getMessage());
return ResultGenerator.unauthorized();
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
log.warn("权限不足: {}", e.getMessage());
return ResultGenerator.forbidden();
}
}
Redis配置
Spring Boot 自动配置了 RedisTemplate<Object, Object> 和 StringRedisTemplate。
自定义 RedisTemplate(如需存储对象)
如果要存储 Java 对象(如 User),需要配置序列化方式(默认 JDK 序列化不友好):
RedisConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用 String 序列化 key
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// 使用 JSON 序列化 value(兼容性强,可读)
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
RedisClientTemplate.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Redis客户端操作模板类
* 封装了常用的Redis操作,简化StringRedisTemplate的使用
* 使用@Component注解,让Spring Boot自动扫描并注册为Bean
*/
@Component
public class RedisClientTemplate {
/**
* Redis字符串操作模板
* StringRedisTemplate是Spring Data Redis提供的专门用于字符串操作的模板类
*/
private final StringRedisTemplate stringRedisTemplate;
/**
* Redis 存取对象操作模板 (可选,需要配置RedisConfig)
* */
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 构造函数 - 依赖注入StringRedisTemplate
* Spring Boot会自动注入配置好的StringRedisTemplate实例
* @param stringRedisTemplate Redis字符串操作模板
*/
public RedisClientTemplate(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 设置字符串键值对(永不过期)
* @param key 键
* @param value 值
*/
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
/**
* 设置字符串键值对,并指定过期时间
* @param key 键
* @param value 值
* @param timeout 过期时间,使用Duration对象表示(如Duration.ofSeconds(30))
*/
public void setEx(String key, String value, Duration timeout) {
stringRedisTemplate.opsForValue().set(key, value, timeout);
}
/**
* 获取指定键的值
* @param key 键
* @return 对应的值,如果键不存在则返回null
*/
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 删除指定的键
* @param key 要删除的键
* @return true表示删除成功,false表示键不存在或删除失败
*/
public Boolean delete(String key) {
return stringRedisTemplate.delete(key);
}
// ==================== List(列表)操作 ====================
/**
* 从列表右侧插入一个或多个值(RPUSH命令)
* @param key 列表的键
* @param values 要插入的一个或多个值
* @return 执行操作后列表的长度
*/
public Long rpush(String key, String... values) {
return stringRedisTemplate.opsForList().rightPushAll(key, values);
}
/**
* 获取列表中指定范围内的元素(LRANGE命令)
* @param key 列表的键
* @param start 起始索引(0表示第一个元素)
* @param end 结束索引(-1表示最后一个元素)
* @return 指定范围内的元素列表
*/
public List<String> lrange(String key, long start, long end) {
return stringRedisTemplate.opsForList().range(key, start, end);
}
/**
* 获取列表的长度(LLEN命令)
* @param key 列表的键
* @return 列表的长度,如果键不存在则返回0
*/
public Long llen(String key) {
return stringRedisTemplate.opsForList().size(key);
}
// ==================== Hash(哈希表)操作 ====================
/**
* 设置哈希表中指定字段的值(HSET命令)
* 如果字段已存在,则覆盖旧值
* @param key 哈希表的键
* @param field 字段名
* @param value 字段值
*/
public void hset(String key, String field, String value) {
stringRedisTemplate.opsForHash().put(key, field, value);
}
/**
* 批量设置哈希表的多个字段和值(HMSET命令)
* @param key 哈希表的键
* @param fieldValueMap 字段-值的映射关系Map
*/
public void hmset(String key, Map<String, String> fieldValueMap) {
stringRedisTemplate.opsForHash().putAll(key, fieldValueMap);
}
/**
* 获取哈希表中指定字段的值(HGET命令)
* @param key 哈希表的键
* @param field 字段名
* @return 字段对应的值,如果字段不存在则返回null
*/
public String hget(String key, String field) {
return (String) stringRedisTemplate.opsForHash().get(key, field);
}
/**
* 获取哈希表中多个字段的值(HMGET命令)
* @param key 哈希表的键
* @param fields 要获取的字段名数组
* @return 字段值列表,顺序与传入的字段顺序一致
*/
public List<String> hmget(String key, String... fields) {
return stringRedisTemplate.<String, String>opsForHash()
.multiGet(key, Arrays.asList(fields))
.stream()
.map(value -> (String) value)
.collect(Collectors.toList());
}
/**
* 获取哈希表中所有的字段和值(HGETALL命令)
* @param key 哈希表的键
* @return 包含所有字段和值的Map,如果键不存在则返回空Map
*/
public Map<Object, Object> hgetall(String key) {
return stringRedisTemplate.opsForHash().entries(key);
}
/**
* 删除哈希表中的一个或多个字段(HDEL命令)
* @param key 哈希表的键
* @param fields 要删除的字段名
* @return 成功删除的字段数量
*/
public Long hdel(String key, String... fields) {
return stringRedisTemplate.opsForHash().delete(key, (Object[]) fields);
}
/**
* 判断哈希表中指定的字段是否存在(HEXISTS命令)
* @param key 哈希表的键
* @param field 字段名
* @return true表示字段存在,false表示字段不存在或哈希表不存在
*/
public Boolean hexists(String key, String field) {
return stringRedisTemplate.opsForHash().hasKey(key, field);
}
/**
* 获取哈希表中所有字段名(HKEYS命令)
* @param key 哈希表的键
* @return 字段名的Set集合,如果键不存在则返回空Set
*/
public Set<Object> hkeys(String key) {
return stringRedisTemplate.opsForHash().keys(key);
}
/**
* 获取哈希表中所有字段的值(HVALS命令)
* @param key 哈希表的键
* @return 字段值的List集合,如果键不存在则返回空List
*/
public List<Object> hvals(String key) {
return stringRedisTemplate.opsForHash().values(key);
}
/**
* 获取哈希表中字段的数量(HLEN命令)
* @param key 哈希表的键
* @return 字段的数量,如果键不存在则返回0
*/
public Long hlen(String key) {
return stringRedisTemplate.opsForHash().size(key);
}
/**
* 为哈希表中的字段值增加指定的整数值(HINCRBY命令)
* @param key 哈希表的键
* @param field 字段名
* @param increment 增量值(可以为负数)
* @return 增加后的新值
*/
public Long hincrby(String key, String field, long increment) {
return stringRedisTemplate.opsForHash().increment(key, field, increment);
}
/**
* 为哈希表中的字段值增加指定的浮点数值(HINCRBYFLOAT命令)
* @param key 哈希表的键
* @param field 字段名
* @param increment 增量值(可以为负数)
* @return 增加后的新值
*/
public Double hincrbyFloat(String key, String field, double increment) {
return stringRedisTemplate.opsForHash().increment(key, field, increment);
}
// ==================== 存取对象 操作 ====================
// public void saveUser(String key, User user) {
// redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
// }
//
// public User getUser(String key) {
// return (User) redisTemplate.opsForValue().get(key);
// }
}
使用示例
// 自动注入RedisClientTemplate
@Autowired
private RedisClientTemplate redisClient;
// 设置值(30秒过期)
redisClient.setEx("user:1:name", "张三", Duration.ofSeconds(30));
// 获取值
String name = redisClient.get("user:1:name");
// List操作
redisClient.rpush("messages", "hello", "world");
List<String> messages = redisClient.lrange("messages", 0, -1);
// hash操作
// 1. 设置单个字段
redisClient.hset("user:1", "name", "张三");
redisClient.hset("user:1", "age", "25");
// 2. 批量设置字段
Map<String, String> userMap = new HashMap<>();
userMap.put("name", "李四");
userMap.put("age", "30");
userMap.put("email", "lisi@example.com");
redisClient.hmset("user:2", userMap);
// 3. 获取单个字段
String name = redisClient.hget("user:1", "name");
// 4. 获取多个字段
List<String> values = redisClient.hmget("user:1", "name", "age", "email");
// 5. 获取所有字段和值
Map<Object, Object> allFields = redisClient.hgetall("user:1");
// 6. 判断字段是否存在
boolean exists = redisClient.hexists("user:1", "name");
// 7. 获取所有字段名
Set<Object> keys = redisClient.hkeys("user:1");
// 8. 删除字段
Long deletedCount = redisClient.hdel("user:1", "email");
// 9. 字段值自增
Long newAge = redisClient.hincrby("user:1", "age", 1);
Double newScore = redisClient.hincrbyFloat("user:1", "score", 0.5);
更多推荐
所有评论(0)