1、requestMapping注解的方法怎么转变成handlerMethod
- 入口
WebMvcAutoConfiguration里面有个内部配置类WebMvcAutoConfigurationAdapter它会Import一个EnableWebMvcConfiguration,它会@Bean一个RequestMappingHandlerMapping对象,同时这边设置了很多属性,包括拦截器,mapping顺序,CorsConfiguration等等;关键点在HandlerMapping对象的父类实现了InitializingBean,HandlerMapping对象又重写了afterPropertiesSet这个方法,但是最后还是调用了父类的afterPropertiesSet,这个方法里面就是处理HandlerMethods方法逻辑 - HandlerMethod如何初始化的
- 获取applicationContext里面所有的Bean对象,过滤出不是scopedTarget.开头的bean
- 然后判断这个bean的类型是否有注解Controller或者RequestMapping注解
- 获取这个bean的中所有标@RequestMapping注解的方法,可以扩展customCondition,然后为每个方法创建RequestMappingInfo对象,这个Info对象主要将requestMapping注解里面的属性进行解析出来封装,一个方法创建完成后,继续处理如果类上有@requestMapping对象,则要继续同样封装成为RequestMappingInfo对象,然后跟方法的组合成一个新的RequestMappingInfo对象,同时接着处理PathPrefix逻辑;最后封装为一个method映射mapingInfo的map类型
- 接着为每个方法创建HandlerMethod对象,这边会处理方法上面的ResponseStatus注解,将里面的属性赋值给HandlerMethod,然后将mapingInfo与HandlerMethod建立关系存入mappingLookup(Map<mapingInfo, HandlerMethod>),如果mappingLookup里面已经有了mapping对应的HandlerMethod就会报错;
- 接着如果请求路径没有包含* ? { } 这些,将会为url建立mapping集合放入urlLookup(MultiValueMap<String, mapingInfo>),即相同路径的mappingInfo对象放在一起
- 如果requestMappingInfo有name即@requestMapping注解有设置name的,如果么有,则将当前handlerMethod的类名大写字母都提取出来+#+方法名称,建立nameLookup( Map<String, List<HandlerMethod》),这里面放着是相同name的handlerMethod集合
- 接着处理@CrossOrigin注解,包括类上和方法上的,如果有,则会初始化CorsConfiguration对象,然后放入corsLookup(Map<HandlerMethod, CorsConfiguration>)
- 最后将mapping,handlerMethod,requestMapping的请求路径,requestMapping的name,这个四个封装成MappingRegistration对象,放入registry(Map<mapping, MappingRegistration>)
- 这就完成了所有方法的初始化了,这边留了一个扩展点,可以覆盖handlerMethodsInitialized进行额外业务逻辑处理mappingLookup
2、@ControllerAdvice@ExceptionHandler怎么被收集起来和生效的
- 入口
WebMvcAutoConfiguration里面有个内部配置类WebMvcAutoConfigurationAdapter它会Import一个EnableWebMvcConfiguration,在它的父类WebMvcConfigurationSupport里面会@Bean一个HandlerExceptionResolverComposite对象;
先检查如果没有通过configurers配置HandlerExceptionResolver,那么就会加入默认的三种异常解析器:
ExceptionHandlerExceptionResolver同时会直接调用它的afterPropertiesSet方法, ResponseStatusExceptionResolver,
DefaultHandlerExceptionResolver
接下去提供通过configurers去extendHandlerExceptionResolvers,最后将这些解析器设置到HandlerExceptionResolverComposite对象里面,它的顺序值是0; - ControllerAdvice注解类的收集
- 收集ControllerAdviceBeans
在类ExceptionHandlerExceptionResolver的afterPropertiesSet方法里面,获取容器内所有bean对象过滤出有标识了ControllerAdvice注解的类,然后封装成ControllerAdviceBean,里面会解析出注解里面所有属性封装为HandlerTypePredicate成员对象赋值给beanTypePredicate;
然后将这些adviceBean进行排序,根据注解order - ExceptionHandlerMethodResolver的封装
循环这些adviceBean获取类中标注@ExceptionHandler注解的方法集合,会解析出注解里面的异常类型,如果注解里面没写,则解析方法参数,必须要有一个异常类型参数,否则报错;
然后建立一个异常类型与method的对应关系mappedMethods(Map<Class<? extends Throwable>, Method>),如果存在相同异常类型则报错;
如果收集到异常则建立adviceBean和resolver关系,放入exceptionHandlerAdviceCache(Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>)
如果这个类有实现ResponseBodyAdvice接口,则adviceBean会添加到responseBodyAdvice
- 收集ControllerAdviceBeans
-
生效
-
入口
在DispatcherServlet执行到拦截器调用了postHandle方法,在这之前任何的异常抛出,且异常类型不是ModelAndViewDefiningException,那么会由handlerExceptionResolvers进行处理异常
循环handlerExceptionResolvers解析器,找到一个能处理异常的HandlerExceptionResolver;
这个解析集合就是IOC内实现了HandlerExceptionResolver接口的类集合。这边发现有两个DefaultErrorAttributes(这个主要将异常保存到request),HandlerExceptionResolverComposite -
HandlerExceptionResolverComposite
这个类在WebMvcConfigurationSupport里面进行@Bean了,然后取出它里面解析器HandlerExceptionResolver,就是上面提到的三个默认的;
-
异常处理
-
ExceptionHandlerExceptionResolver
从exceptionHandlerCache进行获取当前handlerMethod所在的类对应的ExceptionHandlerMethodResolver,如果没有则使用handlerMethod的handlerType直接构造出ExceptionHandlerMethodResolver对象(这个实例化的会将当前类标有@ExceptionHandler注解异常类型全部收集起来跟method对应)进行缓存到exceptionHandlerCache中,这是先检查本地Controller是否写了异常处理,比如BasicErrorController类中就写了一个@ExceptionHandler(HttpMediaTypeNotAcceptableException.class);
然后使用这个ExceptionHandlerMethodResolver对象找出异常类型对应的method,如果当前类封装的resolver对象里面没有找到,则往exceptionHandlerAdviceCache里面找,这个在上面1的步骤中缓存了;那advice匹配逻辑如下:
将ControllerAdviceBean与当前的方法所在的类进行匹配,怎么匹配呢?ControllerAdviceBean注解里面有很多值(basePackages,assignableTypes,annotations)已经封装成了beanTypePredicate成员变量,如果注解里面啥都没有写,则是符合;否则按照三个注解值进行相应匹配;匹配成功就获取到了ExceptionHandlerMethodResolver,此时也是将异常类型进行查找是否有对应的method,然后封装成ServletInvocableHandlerMethod对象,后面就是反射调用了;
从异常处理的源码中发现@ExceptionHandler注解方法可以设置request,response,handleMethod等参数
-
ResponseStatusExceptionResolver
这个解析器是在上一个解析器没找到对应时,它就出场了,这个处理异常类型上面有写注解ResponseStatus的情况 -
DefaultHandlerExceptionResolver
这个就是上面两个都么有处理到,这个类里面会处理一些默认的异常,比如NoHandlerFoundException,AsyncRequestTimeoutException,ConversionNotSupportedException等等
-
-
3、SpringMVC同步请求执行流程
-
springboot自动装配做了好多事情,如图
-
DispatcherServlet
在首次发起请求的时候,会执行initStrategies方法,这里面进行很多赋值处理,比如
multipartResolver:获取MultipartResolver接口子类赋值
handlerMappings :获取HandlerMapping接口子类进行排序后赋值
handlerAdapters:获取HandlerAdapter接口子类进行排序后赋值
handlerExceptionResolvers:获取HandlerExceptionResolver接口子类进行排序后赋值
viewResolvers:获取ViewResolver接口子类进行排序后赋值
-
请求执行流程 doDispatch
- 如果是文件上传请求,则使用multipartResolver进行解析处理request
- 从handlerMapping里面获取HandlerExecutionChain对象,其实就是获取handlerMethod
- 循环handlerMappings集合,第一个就是RequestMappingHandlerMapping;
- 解析出请求路径lookupPath,然后通过urlLookup找到匹配的mappingInfo集合,循环这个集合为当前的request匹配到合适的mappingInfo;怎么匹配------->就是mappingInfo里面有methodsCondition,paramsCondition,patternsCondition就是注解里面属性进行依次匹配request,匹配成功后构造新的RequestMappingInfo,最后封装成为Match(mappingInfo, HandlerMethod )对象,handlerMethod是通过mappingInfo去mappingLookup里面寻找获取到的;
针对于restful风格的路径并不存在urlLookup里面,所以当urlLookup里面找不到的时候,会将所有的requestMappingInfo进行全部匹配一遍,根据Ant风格进行匹配路径; - 如果匹配到多个,要进行比较mappingInfo,如果比较后还是多个直接报错;
- 选到一个合适的mappingInfo后,如果lookupPath是restful风格的,会将路径和值直接解析成map的形式然后存到request
- 直接new一个HandlerExecutionChain(Object HandlerMethod, HandlerInterceptor… interceptors),然后获取到adaptedInterceptors拦截器,循环这些拦截器集合里面有没有是MappedInterceptor类型的,如果有要进行lookupPath匹配,符合则加入到chain;如果没有MappedInterceptor类型的拦截器,则直接加入chain
- 判断corsLookup里面是否有当前HandlerMethod的跨域配置,如果有,则需要在chain里面 加入CorsInterceptor拦截器或者CorsUtils.isPreFlightRequest(request)为true,则封装新的HandlerExecutionChain对象返回
如果最终没有发现能够处理handler,如果配置spring.mvc.throw-exception-if-no-handler-found=true,那么就会抛出异常(比如让404抛出异常,然后项目自定义捕获处理),如果没有配置,那么会设置response.sendError(HttpServletResponse.SC_NOT_FOUND);
然后此时后续会再次发起/error请求- 总结:解析lookupPath,然后找到HandlerMethod,检查是否有跨域配置,最后和拦截器一起封装为chain对象;如果没找到HandlerMethod就会抛出异常或者sendError
- 根据handlerMethod获取HandlerAdapter
- 循环handlerAdapters集合,adapter.supports(handler),就能获取到adapter
- RequestMappingHandlerAdapter直接判断handler是否是handlerMethod,supportsInternal方法没人重写直接是true
(handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler))
- 执行chain里面的拦截器preHandle方法,如果返回false,接着执行afterCompletion;然后就结束流程了
- adapter处理实际业务方法获取mv
- 这边分synchronizeOnSession情况处理,正常是么有同步的
- 获取到handlerMethod所在类中标有注解InitBinder的方法,缓存起来,然后为这些方法封装为InvocableHandlerMethod,然后封装到ServletRequestDataBinderFactory对象里面
- 获取到handlerMethod所在类中标有注解ModelAttribute的方法,缓存起来,然后为这些方法封装为InvocableHandlerMethod,然后封装到ModelFactory(attrMethods, binderFactory, sessionAttrHandler)
- 为当前的handlerMethod创建ServletInvocableHandlerMethod对象,设置argumentResolvers,returnValueHandlers,binderFactory
- 创建ModelAndViewContainer对象,这边会调用ModelAttribute方法,处理下sessionAttributes的东西,略过了
- 创建WebAsyncManager处理异步请求,callableInterceptors,deferredResultInterceptors,略过
- 执行ServletInvocableHandlerMethod#invokeAndHandle方法,先解析请求参数,然后通过反射调用实际方法,参数使用resolvers去解析,这里面很深,跳过跳过…
- 处理方法上@ResponseStatus注解,将里面的code,reason设置到response里面,这个reason如果有值就response.sendError设置到这里面;不会去处理返回值了
- 接着用使用returnValueHandlers去解析返回值,这里面先判断这些returnHandler里面是否有AsyncHandlerMethodReturnValueHandler,然后检查isAsyncReturnValue,如果满足isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler),则不进行解析了;
如果不是,那依次判断returnHandler的supportsReturnType方法,比如返回JSON,就是判断方法或者类上是否有ResponseBody注解,这样就获取到了具体的返回值处理器了HandlerMethodReturnValueHandler。然后就到对应的returnHandler的handleReturnValue去解析返回值,这里面也很深,跳过跳过 - 接着处理sessionAttributes的事情,跳过跳过
- 此时如果mavContainer.isRequestHandled()是false,会接着处理ModelAndView,如果true,这个就返回null;当responseStatus有reason这个就会被设置成true,或者么有返回值但是方法上有responseStatus也是True
- 最后处理response的cacheControl
- 获取到mv后,如果没有视图名称,会进行设置默认的ViewName
- 处理拦截器的postHandle方法了,如果在这之前抛出异常,这个拦截器调用就无法触发了
- 处理异常,看上面controllerAdvice那边有写;
- 渲染试图,如果有viewName,先从需要从viewResolvers解析出View,解析不出来就报错,如果没有viewName,这直接获取view,如果没有,则报错;获取到veiw后进行view.render了;最后 处理拦截器的afterCompletion
- 发布ServletRequestHandledEvent事件
总结:从handlerMapping里面根据urlLookup获取到handlerMethod和拦截器,使用handleMethod找到对应的adapter,执行拦截器preHandler,开始使用adapter处理业务逻辑方法得到mav,然后执行拦截器postHandler,如果有抛出异常则处理异常,否则就开始渲视图,最后执行拦截器的afterCompletion;
4、@ResponseStatus中的reason值引出/error请求
-
ResponseStatus常规用法
-
写在了标有 @RequestMapping注解方法上时
这种注解ResponseStatus如果没写reason,只是写了code,那么会改变了http请求的状态码,如果同时写了reason,那么页面就报错了,同时显示reason和code。
原理: 在ServletInvocableHandlerMethod#invokeAndHandle执行完业务方法获取返回值后,会开始处理ResponseStatus,如果handlerMethod方法上标注了@ResponseStatus注解,写了reason触发response.sendError(status.value(), reason)同时后面就不会处理返回值和视图了,到了tomcat源码中会去找这个code对应的errorPage,如果没找到则获取code=0的errorPage,然后继续发起 errorPage.getLocation()地址的请求。这个0的errorpage是在IOC#onfresh过程中,TomcatServletWebServerFactory#configureContext中加入的
如果没写reason触发response.setStatus(status.value());
-
第二种写在标有注解@ExceptionHandler的方法上
这种本质跟第一种最后都是一样的逻辑处理,因为你会发现在异常处理过程中也是封装为了ServletInvocableHandlerMethod对象去调用的,如果下图所示,handlerAdapter最终也是封装为ServletInvocableHandlerMethod执行的
-
-
- 场景
如果在@ResponseStatus中写了reason值,那么在执行的时候,如果没有引入ErrorPage配置,使用默认的,那么会再次发起一个/error请求 - 流程
- springboot暗中搞了些动作
springboot里面有个后置处理器ErrorPageRegistrarBeanPostProcessor,它在postProcessBeforeInitialization方法执行的时候会将容器内ErrorPageRegistrar接口的所有的子类执行一遍这个接口registerErrorPages方法,将当前bean传入,当前bean必须是ErrorPageRegistry类型,即每个实现了ErrorPageRegistrar接口子类一个机会接触到ErrorPageRegistry类,它可以添加errorPages
重点:TomcatServletWebServerFactory实现了这个ErrorPageRegistry接口,所以添加的errorPages就全部积累到了tomcat里面了,在tomcat在发现错误的时候,会去寻找对应code的errorPage
Springboot框架在tomcat初始化的过程中会将这些errorPage统统加入到errorPageSupport,方便tomcat执行过程出现错误时执行对应的errorPage
- 解析responseStatus注解
在mvc执行完业务方法后invokeAndHandle,会解析handerMethod里面的responseStatus,处理里面的reason,这个一设置就在response里面标记了errorState为1即ErrorReportRequired
- RequestDispatcher.forward
执行完业务逻辑流程后,在StandardHostValve类的invoke方法,会进行判断response.isErrorReportRequired(),然后获取 RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation()); 执行/error/请求
- springboot暗中搞了些动作
- /error请求的处理
在spring-boot-autoconfigure jar包里面有个类BasicErrorController,这类里面处理了/error请求,一个modelView,一个是RsponseEntity的;
从源码分析可得在mvc请求过程中如果一个lookupPath找到两个requestMappingInfo的时候,要进行比较,结果一比较MediaType.TEXT_HTML_VALUE它的处理排在了前面,所以此时你页面会出现一个很经典的Whitelabel Error Page,就是它搞出来的;
这个排序我目前还不知道改变,所以舍弃这个类被扫描,它是在ErrorMvcAutoConfiguration进行@bean进入的,在上面有个注解ConditionalOnMissingBean,就是如果没有出现ErrorController子类,那么就用这个BasicErrorController,-
啥都不动,在classpath的建立一个文件夹,目录/static/error/,这个目录下可以放置对应code的html那么那个Whitelable就不会出现了
-
实现接口ErrorController,自定义处理/error/请求
-
添加对应code的errorPage,这样就没机会执行这个默认的为0的errorpage了
-
5、拦截器怎么加入到requestMapping里面
- 前置
springboot拦截器的定义就声明一个拦截器,然后加入到配置类里面,这个配置类实现了一个WebMvcConfigurer接口,凡是实现了这个接口的子类都会加入到configurers(DelegatingWebMvcConfiguration类)里面,最后放入delegates这里面了
- delegates的套路执行
delegates这是所有WebMvcConfigurer接口子类集合,WebMvcConfigurer接口里面定义很多方法,
然后都是传入一个对应的处理类型,比如InterceptorRegistry,CorsRegistry,HandlerMethodArgumentResolver等等,这样子要在这些传入进来的类型上做扩展功能,就是循环一遍WebMvcConfigurer子类,然后把传入的类型传给这些WebMvcConfigurer子类处理一遍它们自己的处理逻辑,比如添加拦截器,添加方法解析器
- requestMapping拦截器加入
在RequestMappingHandlerMapping对象创建的时候,会设置拦截器,前提创建了一个InterceptorRegistry,然后放入到了configurers里面,这样自己就会把程序定义的拦截器给加入进去了,如果拦截器有路径匹配,它就会封装成为MappedInterceptor
- requestMapping拦截器加入
- delegates的套路执行
6、Filter怎么加入
-
过滤器属于servlet框架层面的,所以这玩意是在tomcat启动过程中进行设置的
-
Springboot中有一个类FilterRegistrationBean,从类名定义以及内容来看,这里面可以放置一个filter
-
所以项目要自定义Filter,只要生成此对象实例,往里面设置filter,然后同时设置相关属性即可
-
原理
- springboot的tomcat在启动过程中会触发ServletContainerInitializer接口子集类的onStartup方法
- 所以先获取这些子类,从源码可以发现 servlet,filter,Listener三大组件齐全
- 所以FilterRegistrationBean实例都会被收集到,然后逐一将他们里面的filter添加到StandardContext中
- 所以dispatcherServlet 这个类在springboot中,从上图源码可以看到肯定由ServletRegistrationBean子类负责,所以你会很顺畅的看到一个类DispatcherServletRegistrationBean,他在@Bean过程中就放入了DispatcherServlet