除了元注解之外,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的视图。