SpirngBoot整合Shiro+Redis时出现缓存$$EnhancerBySpringCGLIB$$代理重复生成问题

1.RedisConfig配置如下:

@Configuration
public class RedisConfig {
    /**
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 配置序列化
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheConfiguration redisCacheConfiguration = config
                // 键序列化方式 redis字符串序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
                // 值序列化方式 简单json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
                //不缓存Null值
                .disableCachingNullValues()
                //缓存失效 3天
                .entryTtl(Duration.ofDays(3));
        return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();
    }
 
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        return template;
    }
 
    /**
     * 重写缓存key的生成方式: 类名.方法名字&[参数列表]
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName()).append(".");//执行类名
                sb.append(method.getName()).append("&");//方法名
                sb.append(Arrays.toString(params));//参数
                return sb.toString();
            }
        };
    }
}

2.@Cacheable注解使用如下:

    @Cacheable(cacheNames = "blog",keyGenerator = "keyGenerator")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public BlogVo getBlogById(int blogId) {
        Blog blog = blogDao.selectBlogById(blogId);
        if(blog == null || blog.getBlogStatus()==0){
            return null;
        }
        BlogVo blogVo = new BlogVo();
        BeanUtils.copyProperties(blog,blogVo);
        User user = userDao.selectUserById(blog.getBlogUserid());
        Type type = typeDao.selectTypeById(blog.getBlogTypeid());
        blogVo.setBlogUser(user);
        blogVo.setBlogType(type);
        return blogVo;
    }

3.问题描述以及现有的线索:

在使用@Cacheable过程中,Redis缓存中会同时生产两个value重复的key,其样式如下:

可见:二者的区别只在于方法名上有无 **$EnhancerBySpringCGLIB$$37f4ba31**
现有原因分析:
(1)如果SpringBoot单独整合Redis使用@Cacheable注解,一切正常;整合shiro-spring-starter后,出现该问题。所以猜测问题就出现在Shiro的整合,可能是自动配置类配置了什么?
(2)如果在yml中声明:spring.aop.proxy-target-class=false则问题解决。该配置含义为SpringBoot默认使用JDK动态代理。
(3)经了解得,SpringBoot2.x均默认使用CGLib进行动态代理。而上面那个重复key就是使用CGLib动态代理的对象,但是@EnableCaching开启注解支持时,属性默认proxyTargetClass = false即采用JDK动态代理,那为什么这里会出现CGLib呢?猜测是二者配置冲突,进行了两次代理缓存操作?

4.运行环境
(1)SpringBoot版本:2.6.3
(2)Shiro-spring-starter版本:1.8.0
(3)getBlogById()方法在ServiceImpl中实现,ServiceImpl实现了Service接口
5.求助
希望能有人帮忙分析一下原因,感激不尽,过个好年!

阅读 4.7k
2 个回答

@EnableCaching 是默认使用 InfrastructureAdvisorAutoProxyCreator 创建代理的,这个是专门给 spring 自己用的;而我们开发时使用的代理使用 AnnotationAwareAspectJAutoProxyCreator 创建的;@EnableCaching 用 jdk,自己的用 cglib 不冲突。cglib 的类产生是 @Transactional 代理出来的。

是不是没有这个注解调用了一次。之后又加上了有调用了一次这个方法?把 redis 清空,再试试?

2022/2/2 最新补充:
被这个问题困扰了很久,于是我DEBUG了一下,发现了一些问题如下:

1.首先,这个缓存操作在执行过程中被执行了两次,第一次是正常的,由CacheInterceptor拦截增强,key=xxx.BlogServiceImpl.getBlogById$[1],正常存入Redis缓存。
2.第二次它又自己执行了一次这个缓存操作,不过这次的key变成了xxx.BlogServiceImpl.$EnhancerBySpringCGLIB$$d3191427.getBlogById&[1],在图中this.collectPutRequests方法执行完毕后,该代理类的缓存被存入redis,出现了上述的情况!
3.楼主的dependencies如下图,也就是说出现这个问题的原因是一次操作访问了两次缓存??但是我只进行了一次缓存处理阿,为什么代理类自己又进行了一次呢?

2022/2/2 再次补充:
1.二次执行时DEBUG截图:

2.第一次正常缓存是在cglibAopProxy,第二次是从MethodProxy开始

2022/2/5 最新进展:
这个问题的源头找到了,源自于这个自动代理配置:

    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        return advisorAutoProxyCreator;
    }

1.这个配置的含义是注入一个自动代理的配置对象,用于给shiro注解进行代理。如果不加这个配置(springboot-shiro-starter以及自动配置了),那么shiro注解不会生效;如果加了这个配置,则会产生上述问题!
2.对于setUsePrefix(true)可以解决,但是这个配置的含义是寻找一个相应前缀的Advisor来代理通知,这里是null。所以加入这段配置就使得shiro注解失效了,无法代理也就没有重复问题!
3.其他配置比如setProxyTargetClass()不论怎么设置都无法解决,这个配置是设置代理模式的,不知道为何失效?
4.综上所述,猜测这个问题的产生来自于shiro的注解代理方式和springboot的代理产生了冲突,使得在执行方法时产生了两次代理对象,执行了两次方法!具体是如何产生的现在还不知道(本菜鸟debug太垃圾了)~建议就是shiro与spring的兼容性太差了,很容易出现莫名其妙的bug,不建议在springboot中使用shiro来进行权限控制,还是学学springSecurity吧!

注意:如果有大佬分析出来了原因和完美的解决方案,可以继续在本帖子中回复!这个问题暂时只能用spring.aop.proxy=false来解决了(全局统一proxy代理模式),谢谢大家的帮忙!!

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏