写点什么

我决定输出一篇文章用于记录一个足足花了四小时才找到的 BUG

用户头像
LSJ
关注
发布于: 2021 年 02 月 26 日

2021 年上班后第一个休息日过后的第四个工作日,伴随着昨日地铁站里大大小小行李箱四个滚轮发出的络绎不绝的声音而缓缓到来,碰巧昨天的气温达到了 21 度之高,这种天气放在整个二月乃至三月份来看,不得不说是一场十足的意外。此时此刻正在返京打工的外地人,心中自是对家里恋恋不舍。可是为了讨生活又无可奈何。同样,三天前的我也带着这种沉闷无比的情绪开始了 2021 年的打工生活。


现在,我决定输出一篇文章用于记录一个足足花了四小时才找到的 BUG,而也正是在解决这个 bug 的同时,隐藏在太平盛世下的 spring aop 才渐渐浮出水面。


这天,怀着无比平静的心情,继续上周没完成之事,一切都在按部就班百无聊赖地进行着,一个接一个的项目启动着,调试着,部署着,最后成功发布到测试环境。然而就在进行到第五个项目的时候,idea 控制台无情地抛出一个异常,这个异常就像夏日午后三点昏昏欲睡之时猛然响起的一声惊雷。我决定把这厮写到这里,供大家观赏。

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'baseController' method public Map BaseController.cityList(HttpServletRequest request)to{[/base/provinceCityAreListApp],methods=[POST || GET],produces=[application/json]}
There is already 'AygServiceImpl' bean methodpublic Map BaseController.provinceCityAreListApp(HttpServletRequest request) mapped
复制代码


这里稍稍将异常信息做了些格式化,要不然不太直观。大概意思就是同一个请求路径被映射到了两个方法上,网上一搜一大堆。


当看到这个异常时,我能很快意识到造成这个异常的浮于表面的原因,因为这次改动只是改了 pom 文件中一个 jar 包的版本号。但是更深层次的本质原因,我无法得知。为了证实自己的猜想,我恢复了原来的版本号,再启动发现正常,我开始纳闷。


拿着关键词兜兜转转在浏览器严格搜索,找到一个突破口—处理器映射器。又经过了漫长的一步一步 debug 后,最终定位到 AbstractHandlerMethodMapping.initHandlerMethods() 方法上,贴一下代码:

protected void initHandlerMethods() {    String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?                          BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :                          getApplicationContext().getBeanNamesForType(Object.class));
for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; try { beanType = getApplicationContext().getType(beanName); } if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } } handlerMethodsInitialized(getHandlerMethods());}
复制代码


当我发现因修改 jar 包版本号而导致的在第 10 行代码处取到的beanType 不一样时,心中大喜,烟瘾油然而生,猛然从椅子上站起来,伸了个懒腰,拿起鼠标旁边的空水杯,稳步走到饮水机旁接了杯水,强行压制住脑子里对香烟的强烈欲望。在一群烟民中戒烟真的是一件难上加难的事,只要你在想抽的时候走到同事跟前说:“来一根”,你就能躲到厕所无比愉悦的抽上几口,这种机会近在咫尺唾手可得,唯一的阻碍便是需要厚着脸皮。不过每次抽完过后我都会牢牢记住尼古丁带来的让人心烦意乱坐立不安的感觉。当下次再想抽烟的时候,我会先用这种记忆说服自己,如果说服失败直接缴械投降。


回到座位上继续刚才惊人的发现:当我使用新版本号启动时, beanType是:EnhancerBySpringCGLIB;当我使用原先版本号时,beanType是:com.sun.proxy.$Proxy165。到这里问题已展露头角,这里有一个知识点:


基于CGlib生成的代理类可以获取到类上的所有注解信息,而JDKProxy生成的代理类拿不到这些信息


有了这个前提后,我开始正式审视AgyServiceImpl的代码,这个类继承BaseController,而BaseController类中部分方法加了@RequestMapping注解。这就导致 spring 在解析由CGLib生成的AgyServiceImpl代理类时,能够识别从BaseController继承下来的带有@RequestMapping注解的方法,然后将这个映射关系加入到路由映射表中。等后续开始解析BaseController类中的这些方法时,发现已经存在映射关系,所以抛了上面提到的异常。当使用JDKProxy生成的代理类时无法识别注解,所以并不会将这些方法提前做映射。


上述代码片段第 12 行的isHandler(beanType)的方法调用便是用来判断是否作路由映射,贴下代码:

@Overrideprotected boolean isHandler(Class<?> beanType) {    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||            AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));}
复制代码


到这儿完全可以断定是因为新版本的 jar 包中引入了如下依赖造成的:

<dependency>    <groupId>org.aspectj</groupId>    <artifactId>aspectjrt</artifactId></dependency><dependency>    <groupId>org.aspectj</groupId>    <artifactId>aspectjweaver</artifactId></dependency>
复制代码


最后为了解决这个问题,不得不将基础组件(就是新版 jar 包)中关于aop的代码砍掉。到此,四个小时消耗完毕,基本可以告一段落,但是我并没有因此停下继续探索的脚步,我开始思考,为什么引入aspectj 后,spring 内部会将生成代理的实现方式切换到CGLib?于是乎我又一次在茫茫源码中苦苦追寻,翻越重重山脉,迷失于无边的沼泽,趟过湍急的河流,遭受猛兽的袭击,绝望情绪和上线日期的逼近险些放弃,最后终于找到一丝希望。


Spring 在初始化 bean 后会执行提前配置好的各种BeanPostProcessorpostProcessAfterInitialization方法,其中有一个被称为AnnotationAwareAspectJAutoProxyCreator 的类。该类的postProcessAfterInitialization方法从父类AbstractAutoProxyCreator中继承,该方法通过调用ProxyFactory.getProxy最终创建出AgyServiceImpl的代理对象。


ProxyFactory作为 spring 最基本的织入器来创建代理对象,而ProxyFactory通过属性proxyTargetClass来决定是否使用CGLib方式创建代理对象


肯定有一个地方将proxyTargetClass属性设置成 true,但是项目里又没有主动设置,然后找到了AopAutoConfiguration自动配置类:

@Configuration@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)public class AopAutoConfiguration {	@Configuration	@EnableAspectJAutoProxy(proxyTargetClass = false)	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)	public static class JdkDynamicAutoProxyConfiguration {	}    	@Configuration	@EnableAspectJAutoProxy(proxyTargetClass = true)	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)	public static class CglibAutoProxyConfiguration {	}}
复制代码


看到这个类,我又迷惑了,项目中没有配置proxy-target-class这个属性,按理说会配置JdkDynamicAutoProxyConfiguration但是,却配的是CglibAutoProxyConfiguration,导致ProxyFactory使用cglib去创建代理。


这个 bug 的终极解决方案就是手动配置 spring.aop.proxy-target-class = false


这里有个大大的疑问:


为什么我没有在项目中配置 proxy-target-class, 却会使用 CglibAutoProxyConfiguration 类作为代理实现 ?


缺陷般的好奇心驱使自己像苦行僧一样在周而复始的 debug 中陷入孤独,在庞大复杂的代码迷宫陷入迷失,每每看到一丝光亮,走近时才发现那只是下一片黑暗的入口。我只想快点找到那个隐藏在黑暗深处的微不足道的答案,以便早早了结这场得不偿失的探寻之旅。所以时间推进到 3 月 2 号,开完早会,坐到工位,找到一个类:ServoEnvironmentPostProcessor 看到这个方法:


public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {  if (ClassUtils.isPresent("com.netflix.servo.monitor.Monitors", (ClassLoader)null)) {    log.debug("Setting 'spring.aop.proxyTargetClass=true' to make spring AOP default to target class so RestTemplates can be customized");    this.addDefaultProperty(environment, "spring.aop.proxyTargetClass", "true");  }}
复制代码

看到这里释然了,同时降临了一股巨大的失落感


发布于: 2021 年 02 月 26 日阅读数: 32
用户头像

LSJ

关注

微笑面对每一天 2018.11.11 加入

一个具有N年编程功力却早已拥有2N年工作经验的boy

评论

发布
暂无评论
我决定输出一篇文章用于记录一个足足花了四小时才找到的BUG