项目目录

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);

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐