1.: redis 종속성 및 사용자 정의 주석 @DataChange를 도입하고 컨트롤러를 구현합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
구성 파일MyCacheConfig
@Configuration
@EnableCaching //开启spring缓存
public class MyCacheConfig extends CachingConfigurerSupport {
/**
* @Description: 使用@Cacheable注解的时候会将返回的对象缓存起来,默认缓存的值是二进制的,
* 为此我们自定义序列化配置,改成JSON格式的
*/
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))// 设置缓存有效期一小时
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
/**
*自己的RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(factory);
//序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);过时
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(om);
//String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Override
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... objects) {
//1.缓冲
StringBuilder sb = new StringBuilder();
//2.类名
sb.append(target.getClass().getSimpleName());
//3.方法名
sb.append(method.getName());
//4.参数值
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}
@Cacheable 주석을 사용할 때 반환된 객체가 캐시되고 , 기본 캐시 값이 바이너리인 문제를 해결합니다 .
2. 맞춤 주석 사용
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
//普通的操作说明
String name() default "";
//spel表达式的操作说明
String spelName() default "";
}
참고: (철자 표현을 사용할 수 있어야 합니다)
3.ClearAndReloadCacheAspect는 측면의 이중 삭제를 지연했습니다.
import cn.hutool.core.util.StrUtil;
import com.redisService.cache.ClearAndReloadCache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Set;
@Aspect
@Component
public class ClearAndReloadCacheAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 切入点
*切入点,基于注解实现的切入点 加上该注解的都是Aop切面的切入点
*
*/
@Pointcut("@annotation(com.redisService.cache.ClearAndReloadCache)")
public void pointCut(){
}
/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
* @param proceedingJoinPoint
*/
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("----------- 环绕通知 -----------");
Signature signature1 = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature)signature1;
Method targetMethod = methodSignature.getMethod();//方法对象
ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定义注解的方法对象
String name = "null";
//获取自定义注解的值,是否使用el表达式
if (annotation != null) {
if (StrUtil.isNotBlank(annotation.name())) {
name = annotation.name();
}
//注解上的描述
if (StrUtil.isNotBlank(annotation.spelName())) {
name = SpelUtil.generateKeyBySpEL(annotation.spelName(), proceedingJoinPoint);
}
}
// Set<String> keys = stringRedisTemplate.keys("*" + name + "*");//模糊定义key的删除
Set<String> keys = stringRedisTemplate.keys(name);//確切刪除
stringRedisTemplate.delete(keys);//模糊删除redis的key值
System.out.println("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName()+",keys="+name);
//执行加入双删注解的改动数据库的业务 即controller中的方法业务
Object proceed = null;
try {
proceed = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//开一个线程 延迟1秒(此处是1秒举例,可以改成自己的业务)
// 在线程中延迟删除 同时将业务代码的结果返回 这样不影响业务代码的执行
String finalName = name;
new Thread(() -> {
try {
Thread.sleep(1000);
// Set<String> keys1 = stringRedisTemplate.keys("*" + finalName + "*");//模糊删除
Set<String> keys1 = stringRedisTemplate.keys(finalName);//確切刪除
stringRedisTemplate.delete(keys1);
System.out.println("-----------1秒钟后,在线程中延迟删除完毕 -----------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
return proceed;//返回业务代码的值
}
}
사용된 SpelUtil 방법
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
public class SpelUtil {
/**
* 用于SpEL表达式解析.
*/
private static SpelExpressionParser parser = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字.
*/
private static DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
public static String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
// 通过joinPoint获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
String[] paramNames = nameDiscoverer.getParameterNames(method);
// 解析过后的Spring表达式对象
Expression expression = parser.parseExpression(spELString);
// spring的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 通过joinPoint获取被注解方法的形参
Object[] args = joinPoint.getArgs();
// 给上下文赋值
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 表达式从上下文中计算出实际参数值
/*如:
@annotation(key="#student.name")
method(Student student)
那么就可以解析出方法形参的某属性值,return “xiaoming”;
*/
return expression.getValue(context).toString();
}
}
DictServiceImpl에 지연된 이중 삭제 주석 추가
//更新:确保机制:实行双删
//重新赋值,只删除key = req.code+':'+#req.dictKey 的缓存项
@ClearAndReloadCache(spelName = "#req.code+':'+#req.dictKey")
@Caching(put ={@CachePut(key = "#req.code+':'+#req.dictKey",unless="#result == null")},
evict = { @CacheEvict(value = "dict", key = "#req.code"),
@CacheEvict(value = "dict", key = "'p:'+#req.parentId")})
@Override
public String doEdit(Dict req) {
QueryWrapper<Dict> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(Dict::getCode, req.getCode());
queryWrapper.lambda().eq(Dict::getDictKey, req.getDictKey());
queryWrapper.lambda().eq(Dict::getParentId, req.getParentId());
//修改时,不能将parentId code和key改为其他已经存在的code和key
queryWrapper.lambda().ne(Dict::getId, req.getId());
List<Dict> list = baseMapper.selectList(queryWrapper);
if (list.size() > 0){
return null;
}
baseMapper.updateById(req);
}
참고: 인터넷의 많은 곳에서는 컨트롤러에 작성해야 한다고 말합니다. 그렇지 않으면 설명할 수 없는 문제가 발생합니다(아직 발생하지 않음).
기타 학습:
1. 주석 기반 지원
Spring은 Spring Cache를 지원하기 위해 여러 주석을 제공합니다. 핵심은 주로 @Cacheable 및 @CacheEvict 입니다 . Spring Cache는 실행 후 @Cacheable로 표시된 메서드의 반환 결과를 캐시하는 반면, @CacheEvict로 표시된 메서드는 메서드 실행 전후에 Spring Cache의 특정 요소를 제거합니다. 아래에서는 Cache에 대한 주석 지원을 기반으로 Spring에서 제공하는 여러 주석을 자세히 소개합니다.
1.1 @캐시 가능
@Cacheable은 메소드 나 클래스 에 표시될 수 있습니다 .
- 메소드 에 표시되면 해당 메소드가 캐싱을 지원함을 나타냅니다.
- class 에 표시되면 해당 클래스의 모든 메서드가 캐싱을 지원한다는 의미입니다 .
캐싱을 지원하는 메소드의 경우, Spring은 다음에 메소드가 동일한 매개변수로 실행될 때 메소드를 다시 실행할 필요 없이 캐시에서 직접 결과를 얻을 수 있도록 호출된 후 반환 값을 캐시합니다. Spring이 메소드의 반환 값을 캐시할 때 이를 키-값 쌍으로 캐시합니다. 값은 메소드의 반환 결과입니다. 키에 관해서 Spring은 기본 전략과 사용자 정의 전략의 두 가지 전략을 지원 합니다 . 나중에 설명하겠습니다. 캐시가 활성화된 메소드가 객체 내부에서 호출되면 캐싱이 트리거되지 않는다는 점에 유의해야 합니다 .
@Cacheable은 값, 키, 조건이라는 세 가지 속성을 지정할 수 있습니다.