百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

一个 AOP 缓存失效问题的排查

toyiye 2024-06-21 12:18 9 浏览 0 评论

起因

起因是线上的一个 bug :项目某个列表页面的分页功能不生效。该列表数据大致有 300 多个,按照每页 100 的方式,至少也有三页,但是页数展示只有一页。

可能性分析

1.首先查看是不是页面处理问题,这只需要确认后端传来的数据是否有分页信息。(因为没有权限查看,所以被告知,是后端返回数据的问题)

2.后端返回的总页数只有 1 页,说明分页没有起作用。

3.定位问题代码,该列表数据的分页管理是基于内部封装的一个切面处理实现的,说明这个地方出现了问题。嗯,由于造数据需要涉及多个服务,直接本地 debug 定位出来的。

代码分析

1.vjooq的PaginationAspect


可以看到,这么一个切面设置,处理过程大致就是,获取事先存放在 PageBuilder中的 dsl 对象和 sql 语句, dsl 执行 sql 获取到一个临时表,并统计总数。

* com.xingren..*.*ByPage(..),理论上是可以应用于当前项目中所有以 ByPage 结尾的方法。

然后以返回值类型 PageBuilder 作为校验条件,仅作用于 Repository 的方法。

题外话:由于 PageBuilder 的特殊性,该类应该只能应用于 Repository 层返回给 service ,不允许 service 层之间或者再往上传递。

2.调用的数据分页列表方法

再看看实际调用的分页方法

除去路径不谈,方法名的格式是符合切面表达式的,并且该方法以及调用方的逻辑最近都无改动。却在近几个月内导致分页失效。

程序员的直觉告诉我:这没有问题。(能有什么问题,这段代码都用了几年了,就没怎么改过。。。)

可惜,它这回就是不好使,于是我 debug 了一下。

3. ClinicDoctorUserRepository

由于存在会应用切面的方法,所以作为一个注定要被加 buff 的 Repository 类,它应该是长上面这样子的。(切面增强的话,产生代理类会将原方法和 advice 的方法,根据 advice 的类型组成一个调用链(责任链模式)。实际调用时就是一个被代理的链式方法的调用过程。比如说 Before -> method -> After。)

但是在这次实际调用过程中,它是下面这样子的。

很遗憾,没有产生动态代理类。所以很自然的,也无法被加 buff。

因为前段时间刚好研究了 Spring 容器启动的源码,所以想探究一下为什 么 ClinicDoctorUserRepository 初始化时没有被切面增强而产生代理类。

初始化分析

1.断点设置

由于切面或者 Spring 的缓存 @Cacheable (是的,项目中的缓存就是基于这个注解做的)都是基于 Spring Aop,所以在项目依赖中一定会存在 Spring Aop 的 jar 包。

那么如果在 Spring Boot 中开启 Aop 的功能,Spring 在启动时给容器注入一个BeanPostProcessor 以增强相应的对象。这个

BeanPostProcessor

就是AnnotationAwareAspectJAutoProxyCreator。

这个注入的方式,大致就是 @EnableAspectJAutoProxy ->@Import(AspectJAutoProxyRegistrar.class) ->AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

那 debug 到 Spring 容器启动时,增强原对象以生成动态代理类的地方看看。

内部过程比较复杂,直接说明这个地方大致在AbstractAutowireCapableBeanFactory#createBean(String,RootBeanDefinition, Object[]) 中。

可以看到,打断点的两个地方是两种生成 bean 的途径:前一种是允许特殊 BeanPostProcessor 事先生成代理的 bean ;后一种是在正常初始化流程中,在应用 BeanPostProcessor 时,封装传入初始化的对象,以生成代理的 bean 。

断点加上条件,只关注:

ClinicDoctorUserRepository , BeanName 一般就是类名的首字母小写。

2.开始 debug !!

启动项目,首先进到第一个断点。

然后跳过这一步,得到 null ,说明该类可能在这一步不会生成,继续到下一个断点。

DoCreateBean 是一个 bean 完整的初始化过程,对应如下的生命周期(图源来自网上,具体参考的地方我忘了):

而 bean 如果还有机会被 BeanPostProcessor增强,那么只有在对象初始化以后,应用 BeanPostProcessor 后置处理的时候了。

所以,跳过其他生命周期的步骤,直接到关键位置:应用 BeanPostProcessor 的后置处理。

进入到这个方法,就是获取当前容器中注册的所有 BeanPostProcessor ,并应用他们的 postProcessAfterInitialization 方法。当前容器的BeanPostProcessor 是存放在只存储 BeanPostProcessor 的集合中。

后面讲到的注册 BeanPostProcessor 步骤,除了注册到统一的集合中外,也会注册到这个特殊的集合中。

先直接跳过循环,直接查看处理完的结果,是否会生成代理类。然而,结果显示只是原有的对象。

这说明要么不满足被切面增强的条件;这个可能性排除了,对于该类以及切面的设置最近不存在改动,不会因此突然在近几个月失效。

或者 AnnotationAwareAspectJAutoProxyCreator 的一些条件没有满足,比如内部的一些 Bean 还没被初始化,因为依赖一些工具对象去进行增强的过程;或者当前容器还没注册 AnnotationAwareAspectJAutoProxyCreator (理论上,这个可能性最小)。

不过一一排查吧。

于是我先在

AnnotationAwareAspectJAutoProxyCreator#postProcessAfterInitialization

的方法内第一行打了一个断点,准备 debug 对象的增强过程。重新启动应用,然后它就这么过去了。。。。

定位获取容器的 BeanPostProcessor 看一下,如下:

意外发现,当前容器中并没有 AnnotationAwareAspectJAutoProxyCreator 。

一般来说,大部分单例对象都会在所有的 BeanPostProcessor 都注入容器之后才会初始化。

除非这些对象是被 BeanPostProcessor 所依赖的,才会被关联导致提前初始化。

所以是不是 ClinicDoctorUserRepository 被提前初始化了?

顺着这个思路,看一下调用栈,当前方法是在AbstractApplicationContext#refresh 中的 registerBeanPostProcessor 方法:

结合下图看一下,一般来说,容器初始化会调用AbstractApplicationContext#refresh ,内部会有几个步骤。而会大部分对象的初始化在第二个红框的方法中 ( finishiBeanFactoryInitialization ) ,但是 ClinicDoctorUserRepository 却在第一个红框 (registerBeanPostProcessor ) 就被初始化,这说明在注册BeanPostProcessor ,项目工程的类被依赖导致提前初始化了。

但是即使这样,对象依然是有机会被 AnnotationAwareAspectJAutoProxyCreator 增强的,只要在依赖项目工程类的BeanPostProcessor 在初始化时,AnnotationAwareAspectJAutoProxyCreator 已经被注册到容器中。

Spring 即使在第一个红框注册所有的 BeanPostProcessor 时,内部也会根据规则,对所有的 BeanPostProcessor 进行分组、排序。然后按照既定的顺序,初始化一组类后统一注册到容器中。


顺着调用栈 ,继续进入

registerBeanPostProcessor 方法查看。

可以看到下图情况,当前是正在在初始化 MethodValidationPostProcessor 。

而 Spring 初始化 BeanPostProcessor 时,是根据是否实现

PriorityOrdered 、 Ordered 接口,分为三组。

每一组都是全部初始化完成才会统一注册容器中上述所说的特殊集合中。

而 MethodValidationPostProcessor 和AnnotationAwareAspectJAutoProxyCreator 都实现了 Ordered ,所以在初始化 MethodValidationPostProcessor 及依赖的 Bean 时,是无法应用AnnotationAwareAspectJAutoProxyCreator 的。

但是为什么我们的 Repository 会被这个 MethodValidationPostProcessor 依赖呢 ,毕竟这个 MethodValidationPostProcessor 是依赖的开源类。那么就有可能是声明的原因。

继续顺着调用栈,就发现依赖初始化了 ClinciWebConfig,而初始化ClinciWebConfig 会初始化其中依赖的很多 Interceptor ,其中有些 Interceptor 就间接依赖了这个 ClinicDoctorUserRepository。

是的,MethodValidationPostProcessor 就声明在这个 ClinciWebConfig中

3.结论

到了这里,基本就真相大白了

根本原因 : 由于

MethodValidationPostProcessor 和 AOP 功能的

AnnotationAwareAspectJAutoProxyCreator 是会一起初始化完成后才注册到容器中,所以被 MethodValidationPostProcessor 所依赖的 Bean 都是无法被AOP 增强的,甚至无法被 ( MethodValidationPostProcessor ) 增强。

而由于

MethodValidationPostProcessor 在 ClinicWebConfig 中声明,所以会间接初始化 ClinicWebConfig 以及其中依赖的 bean。很不幸的是,该列表分页查询的 ClinciDoctorUserRepository 就在其中。

讲道理,我不禁在想,不止这些被依赖的 Bean 的分页功能、缓存功能,估计甚至方法级别的校验功能可能都是摆设。

但是这个情况实际以前是好的,这说明之前肯定有过处理。

所以,又看了看,于是发现。

直接原因:ClinicWebConfig的 Interceptor 依赖的 service 或者 Repository 都是懒加载的,但是最近几个月添加的却落下了 @Lazy。

处理方法

1.补上 @Lazy,并且将 MethodValidationPostProcessor的声明独立为一个 Config 类

补上 @Lazy 基本能解决问题了,但是我觉得为了防止下次可能遗忘,不如将这个配置独立出去。

@Configurationpublic class MethodValidationConfig {

@Bean

public MethodValidationPostProcessor methodValidationPostProcessor() {

// 启用方法层面(参数,返回值)的校验,跟@Validated注解配合使用

MethodValidationPostProcessor processor = new MethodValidationPostProcessor();

processor.setProxyTargetClass(true);

return processor;

}

}

然后登录就会失败。。。。。原因就是用户信息的缓存生效了

/** * 查找loginId对应的User并缓存

*/

@Cacheable(value = CACHE_USER, key = KEY_LOGINID + "#loginId", unless = UNLESS_NULL)

public User findByLoginId(@NotNull Long loginId) {}

原因是用户信息反序列化时,反序列 User 类型的 created 或者 updated 会报错。

jodatime 的 DateTime 序列化的信息很特殊,会序列化为下面这串东西

{

"millisOfDay": 47936000,

"secondOfDay": 47936,

"minuteOfDay": 798,

"centuryOfEra": 20,

"yearOfEra": 2016,

"yearOfCentury": 16,

"weekyear": 2016,

"monthOfYear": 3,

"weekOfWeekyear": 10,

"hourOfDay": 13,

"minuteOfHour": 18,

"secondOfMinute": 56,

"millisOfSecond": 0,

"era": 1,

"dayOfYear": 70,

"dayOfWeek": 4,

"dayOfMonth": 10,

"year": 2016,

"chronology": {

"@class": "org.joda.time.chrono.ISOChronology",

"zone": {

"@class": "org.joda.time.tz.CachedDateTimeZone",

"uncachedZone": {

"@class": "org.joda.time.tz.DateTimeZoneBuilder$PrecalculatedZone",

"cachable": true,

"fixed": false,

"id": "Asia/Shanghai"

},

"fixed": false,

"id": "Asia/Shanghai"

}

},

"zone": {

"@class": "org.joda.time.tz.CachedDateTimeZone",

"uncachedZone": {

"@class": "org.joda.time.tz.DateTimeZoneBuilder$PrecalculatedZone",

"cachable": true,

"fixed": false,

"id": "Asia/Shanghai"

},

"fixed": false,

"id": "Asia/Shanghai"

},

"millis": 1457587136000,

"afterNow": false,

"beforeNow": true,

"equalNow": false

}

而 Redis 使用的 Jackson 反序列的默认方式是,根据无参构造器生成对象,再按照 json 的 key ,一一设置值,但是上面这串东西在DateTime是没有相应的属性值的。

这个情况是好的,是因为原先代码中 Redis 的 valueSerializer 使用的是 RedisTemplate 的默认序列化器,即 JdkSerializationRedisSerializer :

if (defaultSerializer == null) {

defaultSerializer = new JdkSerializationRedisSerializer(

classLoader != null ? classLoader : this.getClass().getClassLoader());

}

if (enableDefaultSerializer) {

if (keySerializer == null) {

keySerializer = defaultSerializer;

defaultUsed = true;

}

if (valueSerializer == null) {

valueSerializer = defaultSerializer;

defaultUsed = true;

}

if (hashKeySerializer == null) {

hashKeySerializer = defaultSerializer;

defaultUsed = true;

}

if (hashValueSerializer == null) {

hashValueSerializer = defaultSerializer;

defaultUsed = true;

}

}

2.解决 Redis 反序列化问题

第一种:自定义 ObjectMapper,增加处理 jodatime 的配置

需要依赖 jar,并且自定义 Redis 序列化器的 ObjectMapper

<dependency><groupId>com.fasterxml.jackson.datatype</groupId>

<artifactId>jackson-datatype-joda</artifactId>

<version>2.8.10</version>

</dependency>

ObjectMapper mapper = new ObjectMapper();

mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

mapper.registerModule(new JodaModule());

redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(mapper));

这样的处理方式,会将时间默认反序列化一个时间戳的值。

{ "@class": "com.xxxx.clinic.domain.tables.pojos.User",

"id": -1,

"loginid": -1,

"type": "DOCTOR",

"name": "\\xe5\\xb0\\x8fD",

"password": "",

"thumbnail": "http://img.xxxx.com/7xxxxx",

"serial": "xxxxx",

"test": false,

"created": 1457587136000,

"updated": 1565580579000,

"deleted": false

}

由于这个缓存使用时,是会直接反序列化为 pojo 。但是 jackson 默认是反序列化 LinkedHashMap ,因此在使用自定义 ObjectMapper ,需要有这段配置,这也是

GenericJackson2JsonRedisSerializer 内部的默认配置。

mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

这段配置会在生成的 json ,存储一份 pojo 的类型信息,就是上面 json 中的 @class。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码