← 返回
未分类

springboot的缓存功能,参数aes加密方式

为Spring Boot项目添加基于AOP + Redis的缓存功能,支持SpEL表达式和AES参数加密。使用@CacheDataAnnotation注解标注方法,自动实现缓存读写。适用于需要快速集成Redis缓存的场景。
为Spring Boot项目添加基于AOP + Redis的缓存功能,支持SpEL表达式和AES参数加密。使用@CacheDataAnnotation注解标注方法,自动实现缓存读写。适用于需要快速集成Redis缓存的场景。
巧凤
未分类 community v1.0.0 1 版本 100000 Key: 无需
★ 0
Stars
📥 109
下载
💾 4
安装
1
版本
#latest

概述

Spring Boot Cache with AES Encryption

为Spring Boot项目快速集成基于AOP + Redis的缓存功能,支持SpEL表达式动态生成缓存key,并对方法参数进行AES加密保护。

快速开始

1. 创建目录结构

在项目中新建以下文件:

src/main/java/com/xxx/config/
├── cache/
│   ├── CacheDataAnnotation.java      # 缓存注解定义
│   └── CacheDataAnnotationAop.java   # AOP切面实现
└── RedisConfig.java                   # Redis配置类

2. 添加Maven依赖

pom.xml 中添加:

<dependencies>
    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Spring AOP(通常已包含在starter-web中) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- Hutool工具类(AES加密) -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-crypto</artifactId>
        <version>5.8.25</version>
    </dependency>
</dependencies>

3. 配置Redis

application.yml 中配置:

spring:
  redis:
    host: localhost
    port: 6379
    password: your_password  # 如果没有密码则删除此行
    database: 0
    timeout: 3000ms

4. 复制核心代码

将以下三个文件复制到项目中(记得修改包名):

CacheDataAnnotation.java

package com.xxx.config.cache;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * 缓存注解
 * @author YourName
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheDataAnnotation {

    /**
     * 缓存key,优先使用
     */
    String key() default "";

    /**
     * 缓存spElKey,el表达式
     * 使用方式:
     * 1. 获取方法参数: @CacheDataAnnotation(spElKey="#userId+'_info'")
     * 2. 调用静态方法: @CacheDataAnnotation(spElKey="T(com.xxx.Util).encrypt(#userId)")
     */
    String spElKey() default "";

    /**
     * key前缀,默认"haicode:"
     */
    String cacheKeyPrefix() default "haicode:";

    /**
     * 缓存时间,默认60分钟
     */
    long expire() default 60L;

    TimeUnit timeUnit() default TimeUnit.MINUTES;
}

CacheDataAnnotationAop.java

package com.xxx.config.cache;

import cn.hutool.crypto.symmetric.AES;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * 缓存AOP切面
 * @author YourName
 */
@Aspect
@Component
public class CacheDataAnnotationAop {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // SpEL解析器
    private static final ExpressionParser PARSER = new SpelExpressionParser();

    // AES密钥(建议改为配置项)
    private static final AES AES_UTIL = new AES("1234567890abcdef".getBytes(StandardCharsets.UTF_8));

    @Around("@annotation(cacheDataAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint, CacheDataAnnotation cacheDataAnnotation) throws Throwable {
        String cacheKey = parseKey(joinPoint, cacheDataAnnotation);

        // 尝试从缓存获取
        Object data = redisTemplate.opsForValue().get(cacheKey);
        if (null != data) {
            return data;
        }

        // 执行原方法
        Object returnData = joinPoint.proceed();
        
        // 存入缓存(null值不缓存)
        if (null != returnData) {
            redisTemplate.opsForValue().set(cacheKey, returnData, 
                cacheDataAnnotation.expire(), cacheDataAnnotation.timeUnit());
        }
        
        return returnData;
    }

    /**
     * 解析缓存key
     */
    private String parseKey(ProceedingJoinPoint joinPoint, CacheDataAnnotation cacheDataAnnotation) {
        String keyPrefix = cacheDataAnnotation.cacheKeyPrefix();
        String key = cacheDataAnnotation.key();
        String spElKey = cacheDataAnnotation.spElKey();
        
        if (StringUtils.isNotBlank(key)) {
            return keyPrefix + key;
        }
        
        if (StringUtils.isNotBlank(spElKey)) {
            String spElKeyValue = parseSpElKey(spElKey, joinPoint);
            return keyPrefix + spElKeyValue;
        }
        
        // 自动生成key:类名.方法名:参数AES加密
        String methodKey = useMethodKey(joinPoint);
        return keyPrefix + methodKey;
    }

    /**
     * 使用方法和参数生成key
     */
    private String useMethodKey(ProceedingJoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getName();
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        String methodName = method.getName();
        Object[] args = joinPoint.getArgs();

        StringBuilder argsBuilder = new StringBuilder();
        if (ArrayUtils.isNotEmpty(args)) {
            argsBuilder.append(":");
            for (Object arg : args) {
                // 对参数进行AES加密
                String aesArg = AES_UTIL.encryptHex(arg.toString());
                argsBuilder.append(aesArg);
            }
        }
        return className + "." + methodName + argsBuilder;
    }

    /**
     * 解析SpEL表达式
     */
    private String parseSpElKey(String spElKey, ProceedingJoinPoint joinPoint) {
        StandardEvaluationContext context = new StandardEvaluationContext();
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = getParameterNames(joinPoint);

        for (int i = 0; i < args.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        return PARSER.parseExpression(spElKey).getValue(context, String.class);
    }

    /**
     * 获取方法参数名
     */
    private String[] getParameterNames(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        return Arrays.stream(method.getParameters())
                .map(Parameter::getName)
                .toArray(String[]::new);
    }
}

RedisConfig.java

package com.xxx.config;

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;

/**
 * Redis配置类
 * @author YourName
 */
@Configuration
public class RedisConfig {

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

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();

        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(jsonSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        template.setHashValueSerializer(jsonSerializer);

        return template;
    }
}

5. 使用示例

示例1:固定key

@Service
public class TokenService {

    @CacheDataAnnotation(key = "huawei:token", expire = 23L, timeUnit = TimeUnit.HOURS)
    public String getToken() {
        // 调用API获取token
        return callApi();
    }
}

示例2:SpEL表达式

@Service
public class DeptService {

    @CacheDataAnnotation(
        spElKey = "'ali:dept:' + #deptId", 
        expire = 1L, 
        timeUnit = TimeUnit.DAYS
    )
    public Department getDeptInfo(String deptId) {
        return queryFromApi(deptId);
    }
}

示例3:多参数SpEL

@Service
public class UserService {

    @CacheDataAnnotation(
        spElKey = "'user:info:' + #userId + ':' + #type",
        expire = 30L,
        timeUnit = TimeUnit.MINUTES
    )
    public UserInfo getUserInfo(Long userId, String type) {
        return queryUser(userId, type);
    }
}

示例4:调用静态方法

@Service
public class SecureService {

    @CacheDataAnnotation(
        spElKey = "'secure:data:' + T(com.xxx.util.EncryptUtil).md5(#sensitiveData)",
        expire = 10L,
        timeUnit = TimeUnit.MINUTES
    )
    public String getSensitiveData(String sensitiveData) {
        return fetchData(sensitiveData);
    }
}

Key生成策略

优先级从高到低:

  1. 固定keykey = "user:info"haicode:user:info
  2. SpEL表达式spElKey = "#userId"haicode:12345
  3. 自动生成:类名.方法名:参数AES加密 → haicode:com.xxx.Service.getUser:aes(...)

注意事项

1. AES密钥配置化(推荐)

当前AES密钥硬编码在代码中,建议改为配置项:

@Value("${cache.aes.key:1234567890abcdef}")
private String aesKey;

private AES getAesUtil() {
    return new AES(aesKey.getBytes(StandardCharsets.UTF_8));
}

2. 返回值可序列化

缓存的对象必须可序列化,建议使用:

  • 基本类型及其包装类
  • String
  • 实现了Serializable接口的对象
  • List、Map等集合(元素也需可序列化)

3. 空值不缓存

当前实现中,返回null不会存入缓存。如需缓存null值,需修改AOP逻辑。

4. Redis连接池配置

生产环境建议配置连接池:

spring:
  redis:
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms

5. 缓存穿透防护

对于可能返回null的场景,建议:

  • 缓存空值(设置较短过期时间)
  • 使用布隆过滤器
  • 参数校验

验证缓存是否生效

创建测试方法:

@Service
public class TestService {

    @CacheDataAnnotation(key = "test:hello", expire = 5L, timeUnit = TimeUnit.MINUTES)
    public String getHello() {
        System.out.println("=== 执行了方法体 ==="); // 第一次会打印,第二次不会
        return "Hello World";
    }
}

调用两次,观察控制台输出和Redis中的数据:

# 查看Redis中的key
redis-cli keys "test:hello"

# 查看value
redis-cli get "test:hello"

常见问题

Q1: 缓存不生效?

检查:

  1. Redis服务是否正常运行
  2. @EnableAspectJAutoProxy 是否启用(Spring Boot默认启用)
  3. 方法是否为public且非static
  4. 是否通过Spring容器调用(自调用不触发AOP)

Q2: SpEL表达式报错?

确保:

  1. 参数名正确(编译时需保留参数名:-parameters
  2. 表达式语法正确
  3. 调用的静态方法存在且可访问

Q3: 缓存数据乱码?

检查 RedisConfig 是否正确配置了序列化器。

性能优化建议

  1. 合理设置过期时间:避免缓存永久有效
  2. 使用合适的key前缀:便于管理和清理
  3. 监控缓存命中率:评估缓存效果
  4. 大对象谨慎缓存:考虑内存占用
  5. 热点数据预热:系统启动时加载常用数据

扩展方向

  • 添加缓存更新策略(主动更新、被动失效)
  • 支持二级缓存(本地缓存 + Redis)
  • 添加缓存统计和监控
  • 支持缓存注解的组合使用

版本历史

共 1 个版本

  • v1.0.0 Initial release 当前
    2026-04-22 12:13 安全 安全

安全检测

腾讯云安全 (Keen)

安全,无风险
查看报告

腾讯云安全 (Sanbu)

安全,无风险
查看报告

🔗 相关推荐

dev-programming

Mcporter

steipete
使用 mcporter CLI 直接列出、配置、认证及调用 MCP 服务器/工具(支持 HTTP 或 stdio),涵盖临时服务器、配置编辑及 CLI/类型生成功能。
★ 195 📥 67,819
dev-programming

Github

steipete
使用 `gh` CLI 与 GitHub 交互,通过 `gh issue`、`gh pr`、`gh run` 和 `gh api` 管理议题、PR、CI 运行及高级查询。
★ 679 📥 328,537
dev-programming

YouTube

byungkyu
使用托管OAuth集成YouTube Data API,支持搜索视频、管理播放列表、获取频道数据及评论互动,适用于用户需要时使用此技能。
★ 142 📥 41,758