为Spring Boot项目快速集成基于AOP + Redis的缓存功能,支持SpEL表达式动态生成缓存key,并对方法参数进行AES加密保护。
在项目中新建以下文件:
src/main/java/com/xxx/config/
├── cache/
│ ├── CacheDataAnnotation.java # 缓存注解定义
│ └── CacheDataAnnotationAop.java # AOP切面实现
└── RedisConfig.java # Redis配置类
在 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>
在 application.yml 中配置:
spring:
redis:
host: localhost
port: 6379
password: your_password # 如果没有密码则删除此行
database: 0
timeout: 3000ms
将以下三个文件复制到项目中(记得修改包名):
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;
}
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);
}
}
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;
}
}
@Service
public class TokenService {
@CacheDataAnnotation(key = "huawei:token", expire = 23L, timeUnit = TimeUnit.HOURS)
public String getToken() {
// 调用API获取token
return callApi();
}
}
@Service
public class DeptService {
@CacheDataAnnotation(
spElKey = "'ali:dept:' + #deptId",
expire = 1L,
timeUnit = TimeUnit.DAYS
)
public Department getDeptInfo(String deptId) {
return queryFromApi(deptId);
}
}
@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);
}
}
@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 = "user:info" → haicode:user:infospElKey = "#userId" → haicode:12345haicode:com.xxx.Service.getUser:aes(...)当前AES密钥硬编码在代码中,建议改为配置项:
@Value("${cache.aes.key:1234567890abcdef}")
private String aesKey;
private AES getAesUtil() {
return new AES(aesKey.getBytes(StandardCharsets.UTF_8));
}
缓存的对象必须可序列化,建议使用:
当前实现中,返回null不会存入缓存。如需缓存null值,需修改AOP逻辑。
生产环境建议配置连接池:
spring:
redis:
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
对于可能返回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"
检查:
@EnableAspectJAutoProxy 是否启用(Spring Boot默认启用)确保:
-parameters)检查 RedisConfig 是否正确配置了序列化器。
共 1 个版本