Spring自定义注解以aop注入方式实现延时双删功能

1.:引入redis依赖和自定义注解@DataChange和实现controller:

 
            org.springframework.boot
            spring-boot-starter-data-redis
 
  
            org.springframework.boot
            spring-boot-starter-aop
            2.2.6.RELEASE

配置文件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 redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate 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 "";
}

注意:(其中需要能使用spel表达式)

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 keys = stringRedisTemplate.keys("*" + name + "*");//模糊定义key的删除
        Set 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 keys1 = stringRedisTemplate.keys("*" + finalName + "*");//模糊删除
                Set 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 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 list = baseMapper.selectList(queryWrapper);
        if (list.size() > 0){
            return null;
        }
        baseMapper.updateById(req);
    }

注:网上很多地方说需要写在Controller里面,不然会有莫名其妙的问题(目前还没遇到过)

其他学习:

1. 基于注解的支持

Spring为我们提供了几个注解来支持Spring Cache。其核心主要是@Cacheable@CacheEvict。使用@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果,而使用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素。下面我们将来详细介绍一下Spring基于注解对Cache的支持所提供的几个注解。

1.1 @Cacheable

@Cacheable可以标记在一个方法上,也可以标记在一个类上。

  • 当标记在一个方法上时表示该方法是支持缓存的
  • 当标记在一个上时则表示该类所有的方法都是支持缓存的

对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的
@Cacheable可以指定三个属性,value、key和condition。

你可能感兴趣的:(redis,spring,java,mybatis,redis)