一个普通类就能干趴你的springboot,你信吗?

  先声明本人并不是标题党,如果看了本篇文章并且认为没有得到任何收获,请您随便留言骂我,本人绝不还口,已经对springboot了如指掌大大神,求放过!

  不BB了,直接上代码,请各位在自己的springboot项目随便一个包下复制进去如下类(不要修改什么东西),如果你的springboot还能站起来算我输!

@Component
public class Environment {
}

  运行springboot的启动类会报如下错误,然后你删除这个类,你的springboot又能健步如飞了,你可能就会怀疑人生了,这代码有毒。先说明我的springboot是2.1.7.RELEASE,我也试了最新的2.2,报错基本一致!

2019-11-02 00:42:46.181  INFO 13568 --- [           main] com.rdpaas.platform.demo.RunApplication  : Starting RunApplication on DESKTOP-9KL4U5L with PID 13568 (E:\project2018\platform\demo\target\classes started by 49519 in E:\project2018\platform)
2019-11-02 00:42:46.183  INFO 13568 --- [           main] com.rdpaas.platform.demo.RunApplication  : No active profile set, falling back to default profiles: default
2019-11-02 00:42:48.490  WARN 13568 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]: Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
2019-11-02 00:42:48.499  INFO 13568 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2019-11-02 00:42:48.615 ERROR 13568 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of method methodValidationPostProcessor in org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration required a bean of type 'org.springframework.core.env.Environment' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.core.env.Environment' in your configuration.

  如上很普通,谁都可能加上的类,就这样一个简单的类,居然可以直接导致springboot站不起来了,如果你认命了,其实也很好解决,你可能会换个名字试试,或者你压根就不会用到这个类,或者是你给@Component("env"),加个别名,可能就碰巧解决了这个问题,那么这时候你可能会当成springboot已经规定了你不能使用关键字environment作为bean的名称,那么这个问题就变得一文不值了,因为你已经认命了,不让我用我不用就行了,以后一辈子都不用这个类名就好了。眼不见心不烦,我用简称Env还来的省事点。不过我个人认为我们遇到难题应该迎难而上,不能随便认命,我们都是骄傲的程序员。应该抱着希望遇到难题的心态,积极去面对难题,多解决一些疑难杂症,用知识和经验武装自己,努力成长,走上人生巅峰!如果看到这里觉得不认命的请跟着我一起看看这个问题到底为啥会出现吧!

  接下来我们一步步来找到问题的根源,为啥用了这个类,springboot就不举了?首先我们从启动的错误提示中找到唯一的关键信息:method methodValidationPostProcessor in org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration required a bean of type 'org.springframework.core.env.Environment' that could not be found。从这句话可以看出来在:org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration这个类中的这个方法:methodValidationPostProcessor 中需要一个:org.springframework.core.env.Environment类的对象作为参数,但是他找不到,看这个名字和我们自己定义的一样,先在idea中找到如上类的methodValidationPostProcessor方法的源码所在:

   为了验证图中的猜想,由于我这不是源码编译的,所以只能自己模仿这个类同样使用@Bean修饰一个方法看看是不是里面的参数都是完全按照参数名称注入的(可以先注释掉之前的Environment类排除那个类的影响),如下

package com.rdpaas.platform.demo.env;
import org.springframework.stereotype.Component;
/**
 * 用作测试的bean
 * @author: rongdi
 * @date: 2019-11-2 0:12
 */
@Component
public class TestBean {
}
package com.rdpaas.platform.demo.env;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 模仿org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
 * 使用@Configuration注解,并且提供一个static的@Bean修饰的方法
 * @author: rongdi
 * @date: 2019-11-2 0:11
 */
@Configuration
public class Config {

    @Bean
    public static TestBean create(TestBean tb) {
        System.out.println(tb);
        return tb;
    }

}

  如上我们使用默认的beanName为testBean的bean,然后Config类中注入的名称是tb运行springboot发现可以正常打印出tb对象,说明名称不一致同样可以注入成功,所i有我们大概可以排除之前的猜想,想想顶顶大名的springboot也不可能这么low逼吧!

  之前的猜测被推翻,我们只能老老实实的使用debug一步步从springboot的入口一步步跟踪进去看看到底啥时候开始报错的,这一步如果不熟悉spring代码的一定要耐心一步步找找,如下

   之前对spring底层源码有一定了解的应该知道spring是先把那些注解和xml声明的类加载到一个map里然后再进行初始化的,这个map就是beanDefinitionMap,一步步断点到spring最核心的方法refresh中

   当执行到上图蓝色位置时,也就是执行完invokeBeanFactoryPostProcessors(beanFactory)方法后,当前beanFactory里的beanDefinitionMap对象中找到了我们声明的environment对象的身影,如下

  断点的过程中发现代码太多要是一步步找过去,很容易就放弃,所以我们再从上面的错误日志找找有用信息,然后通过全局搜索看看到底时哪里报出的错误,如通过报错里的警告信息:expected at least 1 bean which qualifies as autowire candidate. Dependency annotations直接使用全局搜索(前提时先断点跑一边,然后根据idea提示下载好spring的源代码)

   点击上述方法后如下一如既往的打个断点重新跑一边看看

   这时候可以把断点打在if那行再进去看看是啥情况导致进入了这里,然后我们可以确定只要是if返回了true,就必然会导致报错,然后我们注释掉自己的Environment类看看还能否进入到这里,通过注释和不注释的对比我们发现两种情况断点之后有如下差别

   所以问题的关键就是加入了自己的Environment类导致matchingBeans的map为空而产生了本例中的报错信息。

   接下来我们为了调试的效率,在每个出现beanName参数的方法打断点都使用这个条件断点,现在问题就回归到为啥加上了自己的Environment类后给matchingBeans提供数据的方法findAutowireCandidates为空了。一如既往的条件断点打到里面

   使用同样的方式在如下方法也加上条件断点,再次重复执行断点直到进去如下

   耐心的再用如上同样方式进入这个方法,这里由于有多个类请使用F5(断点进入方法,可能快捷键不一样)

 

 

   从上看出,刚进去循环的数组中明显有environment,但是结果为啥就成了空数组,进一步断点发现

   对比以上两个结果,很明显当我们自己添加了Environment类后,singletonObjects肯定有一个移除操作,然后我们找到所有singletonObjects.remove()的地方打一个条件断点:beanName.equals("environment"),很明显从逻辑上看,只要springboot不是全部清空,必然会有一个 remove("environment")才能解释以上两者的差别。

  然后我们再在singletonObjects.put()相关的方法都打上同样的条件断点,放心大胆的继续重新断点执行一遍,第一次进入断点如下

   上图如果执行过addSingleton方法后this.singletonObjects中确实会放入以environment为key,以spring的StandardServetEnvironment为value的键值对进去,这里就不截图了,免得又要重新跑一次断点,直接点击左边调用栈那个679行后如下:

  这里可以先记录下左边环境对象到底是在spring最重要的refresh方法的那一步

  根据上面得出的结论,之所以报错最根本的原因就是这个singletonObjects找不到这个environment了,而这里有,所以肯定有地方删除了这个key,因为这个map看起来如此重要,spring不会无缘无故直接clear吧,所以只要找到唯一的删除key的方式singletonObjects.remove(),并打上上面说的条件断点,这一点上面其实说过了,那我们继续跑断点,直到找到在哪删除了这个key

 

 其实复盘一下整个调试过程,发现其实源头如下

   其实我也不知道这算不算是springboot的bug,还是其实只是一个关键字的限定,因为最终解释权不在于我,就像mybatis中的xml里大于符号要用>不然别人根本解析不了,从这一点来说mybatis使用xml存放sql实际上限制了我们使用大于小于等等这些符号的权力,只能用转义字符类似别名的东西替代。其实这里也是类似,也可以理解成人家系统需要,你要用这个请改个名字或者取个别名,比如@Component("env)。不过我还是希望springboot能还我们使用单词的自由,希望英文好的朋友可以发发邮件让springboot团队考虑下,哈哈!

最后来个篇中总结:

  1) 从文笔上来说,一如既往的没有文笔,请各位大大海涵,真的尽力了,奈何胸无点墨!

  2) 从排版来说,一如既往的没有排版,我是个纯技术人,这些花里胡哨的东西,真的一点不会,同样请各位包涵

  3) 从知识点来说,其实这篇博客主要是给小白们分享一下看源码的技巧和基本的调试能力,还有遇到问题的处理态度。首先从这篇文章中应该能清晰的get到逆向思考一步步找的问题的方法,其次应该能获取到一些断点调试源码的技巧,最后也应该能学会方法调用栈的作用。其实懂这三点基本就够了,spring这些源码是否看过也不会影响你最终能找到这个问题的根源这一结果,最多会影响你找到根源的时间。

  4) 从用心程度来说,这篇博客自认为是足够用心,周五晚上从下班回家一边一步步断点一遍写这篇博客,直到凌晨三点多才匆忙洗洗睡。文章里基本上把我知道的关于这个知识点的所有东西通过清晰的图文方式一步步展现,本人热爱技术,也喜欢分享技术,希望与广大程序猿们相濡以沫,共同进步!

  5) 从文章质量来说,对大牛一文不值,对小白有一定帮助,希望大牛们多多包涵,不要喷我,有错误之处请多多指正。

posted on 2019-11-04 09:52  码小D  阅读(2578)  评论(18编辑  收藏  举报