Java进阶篇——springboot2源码探究

1.@EnableAutoConfiguration

除了元注解之外,EnableAutoConfiguration包含了两大重要部分:

1)@AutoConfigurationPackage注解

该注解只导入了一个内部类:AutoConfigurationPackages.Registrar.class

类中有两个方法

从名字上看,registerBeanDefinitions方法注册了定义好的一些Bean,determineImports方法决定这些要不要导入

registerBeanDefinitions调用了register方法,并传入了registry参数和一个元数据名字数组。registry参数是一个接口,
实际场景中必然是使用的实现类,可以在该方法打断点debug

发现实际上他的实现类是一个名为DefaultListableBeanFactory,并且可以清晰的看到该类的一些基本属性的值

可以看到,registry中存储的是一些关于项目程序的基本配置和bean实例名。比如一些网页支持组件和springContext以及一些加载器

还有用户自定义的类,这些类组件中存储了bean名字、作用域、懒加载等等


再来看另一个参数:new PackageImports(metadata).getPackageNames().toArray(new String[0])

看起来是将元数据中的包名提取成数组,我们打开看看该类在初始化的时候具体干了什么,可以看到实际上在初始化时调用了一个ClassUtils.getPackageName,
传入了一个什么呢,metadata.getclassname,他是什么呢,打断点!
,实际上就是启动类的全名,com.***.application,而这个方法则是将classname的前缀提取出来,即提取出我们的包名com.&&&


即这个AutoConfigurationPackages.register方法传入了我们的一大堆初始bean的名字、配置和总包名com.**,我们研究研究他做了什么

首先是判断定义的bean中有没有Bean,这个bean是类的属性,存着当前类的全名:org.springframework.boot.autoconfigure.AutoConfigurationPackages

大概想处理的是用户自定义了AutoConfigurationPackages类的情况。当前是没有定义的,所以直接走else逻辑

new了一个GenericBeanDefinition,暂且叫他通用的bean定义工具,set一大堆东西。之后调用了注册方法。把工具丢了进去。

这个注册方法里大概是将AutoConfigurationPackages类同样注册进了定义bean的map中,然后将map中的所有值加入到了这个默认的bean工厂中,之后这个类就走完了。

因此该注解的作用即是将自定义的bean以及一些基本类型、原始组件注册到bean工厂中

2)AutoConfigurationImportSelector类

该类中点进去映入眼帘的就是selectImports方法。听起来名字是选择导入,也就是该方法决定了要加载什么依赖组件

而该方法调用了并且只调用了getAutoConfigurationEntry方法来获取要加载的组件。

我们来看看这个方法,先是调用getAttributes获取了某个东西,打断点发现是
这样就很熟悉了,是EnableAutoConfiguration注解的两个属性值,虽然默认值为null。点进方法体发现确实是这样。

getCandidateConfigurations(annotationMetadata, attributes)方法调用了一大串,最终调用了这个方法loadSpringFactories,
该方法的大致内容是,从当前所有的依赖包中加载META-INF/目录下的spring.factories文件中寻找一些组件。
可以看到他先尝试从缓存中拿,如果为空,则去依赖包中的META-INF/目录下的spring.factories中加载。


拿到这个组件列表之后,还要进行一层过滤,抽取含有factoryTypeName的组件列表。
这个name即是org.springframework.boot.autoconfigure.EnableAutoConfiguration自动配置注解

再回到getEntry方法,之后对获取到的组件列表进行去重,然后试图从列表中拿出排除项,
也就是attributes中获取到的EnableAutoConfiguration注解的两个属性值的内容。很容易理解,属性值中配置了要排除的内容将在这里进行排除。

而后调用filter方法对列表进行筛选,而筛选使用的是autoConfigurationMetadata这个类,
由此可见,这个类是某种筛选规则,它里面存储了501个properties,所以筛选规则可能就是逐一比对,筛选出Metadata中有的组件。

筛选出来的结果即是最终要加载的组件bean。

也就是说,@EnableAutoConfiguration的两个重要成员,一个决定了要加载默认的哪些组件(用户自定义bean、数值包装类、字符串类等等)
和配置,另一个决定了要加载哪些外部依赖类,即通过starter等通过pom引入的组件。

2.请求处理

如何知道spring是怎么、在哪处理请求的呢,有个很简单的方法,在properties中将日志级别设置为debug,即dubug=true。而后运行程序,发送任意一个请求

就会发现spring打印出了关于处理该请求的一些细节,比如处理请求是从初始化DispatcherServlet开始的,
由此可见请求处理最重要的便是DispatcherServlet。而后AbstractHandlerMapping识别到了处理该请求的具体方法。

我们进入到DispatcherServlet中,查看他的方法。

学过mvc原生web开发的都知道,servlet有两大重要方法,doGet和doPost。而DispatcherServlet继承了FrameworkServlet继承了HttpServletBean,
HttpServletBean继承了HttpServlet,由此可知,DispatcherServlet也是一个httpServlet。那么我们就有思路了,从他的do**方法开始探究。

1)doDispatch

从名字看,这个方法似乎是为了做转发。并且他的参数几乎和doGet方法是一模一样的

最开始是初始化了一大堆东西,包括处理异步请求的异步管理器以及检查是否是文件上传的请求巴拉巴拉,而后这个getHandler方法,直接获取到了处理这个请求的具体方法。

它是如何处理的呢,他遍历了一个handlerMappings的集合,这个mapping里面存储的是spring一些专处理映射的类
,比如欢迎页的映射,以及我们使用requestMapping标注的url。这样就打通了,他会在requestMappingHandlerMapping中查找到/hello请求并且找到他映射的方法。
具体查找调用了HandlerMapping的gethandler,由此就和日志中的对上了,gethandler方法中就是根据url在映射中找方法,先抛开这个细节不看。

拿到这个处理器(方法)后,将其丢入到了HandlerAdapter请求适配器中,这个在mvc架构中熟悉的身影。
之后并不是立即执行该方法而是先判断方法的种类,如果是get或者head方法,则执行逻辑。

这个逻辑会衡返回一个-1,也就是写死的,我不理解为什么是这样,在网上搜了Last-Modified,发现这是一种缓存机制,
这也就理解了为什么必须是get方法,而他实现需要实现LastModified接口,我并没有实现这个,所以spring这个判断逻辑会衡false。

之后是一个applyPreHandle的方法,点进去就会发现是使用当前spring的拦截器组件对请求进行拦截,

能发现就是一些请求方法拦截器、token拦截器、资源拦截器等等,也就是一些坏的请求会在这条被拦截

之后就是请求适配器执行自己的处理逻辑了,可以看到他返回的是一个modelAndView类型的实例,那么就意味着,此时方法已经被执行了。

但是实际上并没有使用modelAndView作为返回值,而是直接返回的string。所以mv是一个空值,但是可以在响应体中观察到,
已经有19字节的东西被写进了响应体中,页印证了方法已经被执行。

紧接着是判断请求是否是异步请求,如果是异步请求可能会做First响应之类的处理。

再往下是一个名叫应用默认的视图名的方法,将请求体中或者默认的视图名加入model中。

因为如果应用了modelandview,此时才只是一个model,必然要添加对应的view。可以简单测试一下。我们在处理方法中new一个modelandview对象,设置一个model值并返回。

可以看到,经过此方法之后,我们并没有设置view值,系统默认将hello当作了view加入model,这个默认值即是请求路径去掉前面的/。

这个方法之后,又是一个类似于拦截器,有点类似于在方法执行前后各执行一次拦截。拦截器执行完毕后,方法结束。
后面就是一些兜底处理,比如如果是文件相关之类的请求,要关闭对应的流。如果是异步请求执行怎样的逻辑。

2)handle方法

我们知道,适配器的handle方法里面执行了方法逻辑,具体是怎么执行的呢。实际上是调用了super的AbstractHandlerMethodAdapter的handle方法。
这个handle方法又会调用自己(RequestMappingHandlerAdapter)的handleInternal方法。该方法内对session做了一些处理,
而后调用invokeHandlerMethod方法,这个方法内做了很多的处理。比如WebDataBinderFactory binderFactory对象,他是对参数之中的数据格式做转换的,他里面初始化了128种对象转换的方式;

又或者初始化一些参数解析器和返回值处理器:里面对各种参数和返回值做对应的解析和处理,总之就是把这些丢入到一个mavContainer容器中。

然后调用invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0])方法,同时传入的还有webRequest,
这个就是将请求体和响应体包装到了一起。这个方法里面第一行就直接执行了方法,可以看到此时已经有返回值了。

这个方法就比较简单了:获取参数、执行方法

①getMethodArgumentValues

该方法最开始是获取映射方法的参数类型以及参数名

之后做的事大概可以猜到,就是从请求中找出这些参数名对应的参数并且转化成对应的类型。可以看看具体的

首先定义了一个接收数组,用来存储获取到的参数,而后尝试从providedArgs中拿对应的参数,但实际上这个参数传的是空值。

所以执行后面的逻辑,从请求中拿,并且同时传入了mav容器,这个容器中就有格式转换器和返回值处理器等。

该方法里面有两个重要的构成,getArgumentResolver获取参数解析器和resolveArgument解析参数。获取参数解析器方法中,
循环目前已经存进ioc容器的解析器组件,和参数进行一一比对,找到可以解析对应参数的解析器。

解析参数方法中,就是获取参数名容器→获取方法参数容器→获取参数名→解析参数。之后会做很多的后续处理,比如格式转换之类的。

②doInvoke

这个方法比较简单,利用之前已经存入InvocableHandlerMethod中的反射方法public java.lang.String
com.glodon.controller.HelloController.home(java.lang.String),实际上,该对象实例也是专门存储方法处理的相关组件的。
获取到方法后,利用spring反射机制执行该方法。

3)请求响应

前面将方法执行完之后,封装完modelAndView,在执行完处理后拦截器后,还要执行一个兜底方法对结果进行处理,从名字来看,他是用来处理结果转发的。

开始是判断方法执行是否有异常,如果有则走异常处理逻辑。而后如果modelandview不为空,则执行一个render方法render(mv, request, response)

render方法首先从请求中拿到语言标识,并加入到响应体中,而后拿出modelandview中的视图名,即重定向或者其他视图。
然后调用resolveViewName方法对视图名进行处理,对当前容器中的所有解析器进行遍历,哪个可以解析这个视图名,就直接返回解析结果。

解析器的解析过程,以ContentNegotiatingViewResolver举例。

这个解析器获取了请求的Attributes,然后传入getMediaTypes方法中并调用来获取返回数据类型。

getMediaTypes里面其实就是一个双重循环匹配,格式化网页请求→获取浏览器请求头中的可接受媒体类型→获取系统可生产的媒体类型→初始化匹配的媒体类型。
然后进行一个双重循环,如果匹配成功,则把对应的媒体类型加入到compatibleMediaTypes中。

排序完成后就是一个set→List,然后进行一个排序,排序的依据就是媒体类型的权重。

浏览器在发送请求的时候会给服务器一个accept,里面明确表示了浏览器可以接收的返回类型以及他的权重,而这里的排序就是按照这个权重进行排序的。

该方法返回后,在接着看解析方法,之后调用了getCandidateViews获取候选视图。

getCandidateViews方法内同样是个双层循环。外层是除了当前解析器外的其他三个视图解析器,内层是对匹配到的媒体类型。

通过调试,发现最终是被InternalResourceViewResolver成功处理,我们只看他的细节。

同样还是resolveViewname方法,该方法先尝试去缓存中拿,拿不到了才执行createView方法创建视图。而这个方法就很清晰明了了

判断是重定向还是转发来生成对应的视图。最终返回合适的视图,添加缓存巴拉巴拉。

我们测试的刚好是一个重定向视图,所以返回的结果就是bean为redirect,url为/helloWorld的视图。

拿到这个视图后,估计就是转发工作了,我们再退回到render方法中,发现了dispatcherServlet.render方法中在获取了视图之后,
进行了一堆判空+set操作之后又调用了一个view的render方法。这个方法中会先拿出model

而后prepare方法中设置了几个响应头,调用了一个renderMergedOutputModel方法

这个方法里就很明显了,获取转发url,进行转发。

4)错误处理

我们在请求方法中加入一个1/0异常,查看底层对异常的处理方法。

首先可以看到错误在反射底层的调用方法MethodProxy.invoke中被捕获,而后是CglibAopProxy的动态代理的proceed捕获。
这些都是底层执行方法调用的默认处理逻辑,目的是查看spring是否能处理这些错误,获取错误的信息并包装,逐层将错误抛出。
直到我们看到mvc熟悉的方法requestMappingHandlerAdapter中执行了webRequest.requestCompleted(),
该方法点进来发现是进行了一些善后工作,比如请求销毁,修改session。

而后就返回到doDispatcherServlet方法中,被catch(Exception var20捕获),但这里只进行了简单的错误引用赋值

赋值完成后就是结果转发处理。这次就会走到之前的错误流程了

我们不是一个model定义错误,所以走else逻辑,else里面拿到请求方法处理器,
传参调用processHandlerException(request, response, handler, exception)方法。
结果错误处理方法里对当前注册的错误处理组件进行了遍历,如果哪个可以处理返回ModelAndView则返回他处理的结果。

最终处理成功的是这个处理器HandlerExceptionResolverComposite下的ExceptionHandlerExceptionResolver。看看他是怎么处理的

首先执行了一个shouldApplyTo方法,里面判断了mappedHandlers,mappedHandlerClasses是否为空以及是否包含错误方法的处理器,
判断该错误方法处理器是否应该在这里进行处理。该方法在这种场景下是恒true的,也就是一个抽象方法适应。

而后执行了一个prepareResponse方法,给响应头中加入了"Cache-Control", "no-store"键值对,然后才是处理错误doResolveException()

该方法拿到错误方法的类,然后尝试从缓存中拿该类型的解析器,如果拿不到就新建一个加入缓存。新建的解析器中存储了映射的处理方法和错误类型

之后从存储的处理方法中尝试获取能够处理该错误的类,但实际上我们并没有定义,所以method是的size是0。之后再判断是不是代理类巴拉巴拉。

最后的操作很重要,是对一个叫exceptionHandlerAdviceCache的属性进行遍历,查找有没有能够处理该错误的方法。

这个exceptionHandlerAdviceCache里面存储的就是我们标注了全局异常处理的类。也就是@ControllerAdvice+@ExceptionHandler的类。

这里的逻辑就是拿出ControllerAdvice的组件,然后进行一个校验,

解析出全局异常处理组件的可识别错误类型,和待处理错误进行比对,得出是否匹配的结果。匹配成功则调用ServletInvocableHandlerMethod反射执行该处理方法。

前面我们好奇,全局异常错误处理类是怎么注册进HandlerExceptionResolverComposite的,我们很容易直到是bean注册,但是如何注册的,
这个类的构造方法是空的,只提供了一个set方法设置处理器,打断点!

发现是WebMvcConfigurationSupport的handlerExceptionResolver调用了他的set方法,而且这个方法是加了bean的,按spring套路来说,
这个方法就是专门初始化HandlerExceptionResolverComposite组件的,他是调用了一个空方法,一般看到这种空方法或者恒返回值都是要你自己整活的,
也就是让我们自己重写方法,我没整活,就会走这个空方法,之后肯定会走空逻辑,调用addDefaultHandlerExceptionResolvers(exceptionResolvers,
contentNegotiationManager)方法,它的参数是一个存放结果的空集合和一个内容协商管理器。前面走请求处理的时候我们知道这个管理器一般用来做媒体匹配等工作。

在这个添加方法里,首先创建了一个exceptionHandlerResolver对象,然后将协商管理器、类型转换器、参数解析器、返回值处理器、applicationcontext等丢进去。

丢完之后调用了afterPropertiesSet()方法,这个方法就有门道了。最重要的是这个


剩余的就是判断参数解析器和返回值处理器是否为空,为空则加入默认的。

那么这个方法干了些什么呢

一下就来感觉了,他从ApplicationContext()中拿到了所有加ControllerAdvice注解的类,这个类不就是我们的全局异常处理类么。
拿到了之后遍历初始化成ExceptionHandlerMethodResolver,判断其对应的处理类型,比如是根据错误类型来处理还是根据相应体信息处理。

拿到这个重要信息之后再返回add方法,之后是把获得的错误解析器丢进结果,并且初始化了一个相应状态解析器还有一个默认的错误解析器丢入,
也就是目前有三个错误解析器(自定义、默认错误处理、相应状态解析器)。

刚好composite中的错误解析器也是三个,破案了

现在我们知道,exceptionHandler错误解析器就是专门解析我们定义了全局异常处理的错误,而且处理的结果会按照我们自己定义的方式来,
我这边的处理是将错误写进响应体。那么如果这个错误没有被全局异常捕获呢,可以猜到应该是这个默认的错误解析器,我们浅试一下。我直接把全局异常处理类注释掉。

可以看到在processHandlerException方法中,没有找到可以处理该错误的解析器。spring的选择是再次将错误抛出去。

那么这次会被谁接收呢。

可以看到是被这块代码接收了并且执行了triggerAfterCompletion方法,这个AfterCompletion是拦截器的环绕方法,
所以这个方法可能是执行拦截器的AfterCompletion内容。点进去发现确实如此,那么证明这块代码并不是处理错误的最终方法,他只是必须走的一个流程。

对拦截器进行遍历,挨个执行AfterCompletion方法,这里需要注意这个循环次数,拦截器组件里会有一个下标值记录当前执行到了那个拦截器,
而执行AfterCompletion时,也会从下标开始--执行,也就是没有执行的拦截器在这里也不会执行,这是为了应对,某个拦截器将请求拦截了下来。
在AfterCompletion方法还会执行权重低的拦截器的方法的情况。

最后是在一个StandardHostValve类中才给相应体中传输了信息,中间历经大概20次左右的跳转吧,已经不是我能涉及的范畴了。

在百度上搜了下,这是交给tomcat去处理了。而且这个方法是将响应体中的信息写入outputbuffer,也就是说可能,错误信息已经写入响应体了,
只是没有写入响应流中。但是我在相应体中并没有找到相关的信息,而且这个错误信息是比较离谱的,他是一个token校验错误。所以我又猜测可能是哪里做了转发,使得走了token校验流程。

所以迫于无奈,我又来到了这个StandardHostValve中,发现是在调用了一个throwable方法后,响应体中有了输出,
苦哇,我这个版本竟然是在tomcat底层中处理默认错误的。于是乎,我点进了这个方法。

发现他确实是在进行错误处理,而且这个错误信息不是加到了相应体中,而是在请求体中,直接颠覆了我得三观,
这个方法中判断这个抛出的错误是不是ServletException的实例啦、在applicationContext中拿能处理这个异常的错误页(并没有)最后set了一个状态码调用了status函数

这个status中是按照状态拿错误页,他在这拿到了一个code为0,路径为/error的错误页,dna动了不是吗。

然后set了一大堆Attribute,然后调用了custom方法,然后点进来。

果然看到了转发,转发的结果就是token校验失败。破案!

posted @ 2023-01-11 16:52  姬如乀千泷  阅读(286)  评论(0编辑  收藏  举报