SpringBoot -Web 使用及源码分析

2022-09-03 08:08:36

1 yaml

YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重命名。

说明YAML就是为了配置数据而专门设计的。在SpringBoot中用于替代xml配置文件。

语法注意是k:v形式,层级以空格分隔。

SpringBoot中默认命名为application.yaml,演示xml配置文件与yaml文件对比,明显看出yaml文件易于阅读:

server.port=8888
spring.banner.charset=UTF-8
spring.banner.location=xxx
spring.cache.cache-names=xxx
server:port:8811spring:banner:charset: utf-8cache:cache-names: xxx

2 静态资源

2.1 访问静态资源

从官网文档可知,只需要是放在resources路径下的这些目录都是可访问的静态资源:/static (or/public or/resources or/META-INF/resources)

比如test.jpg的访问路径为:localhost:8080/test.jpg

简单原理:

SpringBoot默认情况下,会匹配/**访问路径。如果存在动态的访问路径,如配置了controller的访问路径也是/test.jpg,就会访问动态的路径,否则会匹配静态资源的路径。

可以修改配置来更改静态资源的访问路径:之后访问就是localhost:8080/resources/test.jpg

spring:mvc:static-path-pattern: /resources/**

比如:在/static目录下放一个heart.png,用浏览器打开:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cv0b4rHl-1614753646372)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210227214702794.png)]

2.2 静态资源访问源码解析

分析源码,入口都是在相应的autoConfiguration自动配置类中个,SpringMVC的内容,在WebMVCAutoConfiguration类中,进入查看:

package org.springframework.boot.autoconfigure.web.servlet;@Configuration(proxyBeanMethods=false)@ConditionalOnWebApplication(type= Type.SERVLET)@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE+10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class})publicclassWebMvcAutoConfiguration{
    。。。}

以上分析可以得知:

  • 这是一个配置类,type类型必须为Type.SERVLET
  • 容器中必须存在Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class 三个类型的组件。
  • 容器中不存在WebMvcConfigurationSupport.class类型组件,说明可以自定义WebMvcConfiguration
  • @AutoConfigureOrder和@AutoConfigureAfter指示了创建顺序

分析WebMvcAutoConfiguration里面,有一个内部类EnableWebMvcConfiguration,也是一个配置类,它的构造方法如下:

publicEnableWebMvcConfiguration(
    org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
    WebMvcProperties mvcProperties, WebProperties webProperties,
    ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider,
    ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
    ListableBeanFactory beanFactory){this.resourceProperties= resourceProperties.hasBeenCustomized()? resourceProperties: webProperties.getResources();this.mvcProperties= mvcProperties;this.webProperties= webProperties;this.mvcRegistrations= mvcRegistrationsProvider.getIfUnique();this.resourceHandlerRegistrationCustomizer= resourceHandlerRegistrationCustomizerProvider.getIfAvailable();this.beanFactory= beanFactory;}

在SpringBoot中,如果配置类只有一个有参构造器,构造器的参数值会从容器中自动确定。

这个构造器方法,是配置类组件获得了各种配置资源。

在EnableWebMvcConfiguration配置类中有个addResourceHandlers,用于确定静态资源访问路径的。

@OverrideprotectedvoidaddResourceHandlers(ResourceHandlerRegistry registry){super.addResourceHandlers(registry);if(!this.resourceProperties.isAddMappings()){
        logger.debug("Default resource handling disabled");return;}
    ServletContext servletContext=getServletContext();addResourceHandler(registry,"/webjars/**","classpath:/META-INF/resources/webjars/");addResourceHandler(registry,this.mvcProperties.getStaticPathPattern(),(registration)->{
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());if(servletContext!= null){
            registration.addResourceLocations(newServletContextResource(servletContext, SERVLET_LOCATION));}});}

从中可以看出:

  • "/webjars/**"请求会匹配到 "classpath:/META-INF/resources/webjars/"路径下的资源。
  • 静态资源的请求默认是匹配"/**",处理的路径是{ “classpath:/META-INF/resources/”,
    “classpath:/resources/”, “classpath:/static/”, “classpath:/public/” }
    • this.mvcProperties.getStaticPathPattern()对应是"/**"
    • this.resourceProperties.getStaticLocations()对应的是{ “classpath:/META-INF/resources/”,
      “classpath:/resources/”, “classpath:/static/”, “classpath:/public/” }

3 web请求

3.1 请求映射的原理

首先我们知道SpringMVC处理请求都是通过DispatcherServlet来处理。通过DispatchServlet的继承关系可以得知,它是一个HttpServlet,那么处理请求的方法就是doGet和doPost。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4xUB7faY-1614753646376)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228123415457.png)]

分析源码可得到如下的调用关系:

doGet/doPost —> processRequest(request, response) --> doService(request, response) --> doDispatch(request, response)

重点分析doDispatch方法:(代码只保留了关键的步骤)

protectedvoiddoDispatch(HttpServletRequest request, HttpServletResponse response)throws Exception{
    HttpServletRequest processedRequest= request;
    HandlerExecutionChain mappedHandler= null;// 根据request获取执行的handler,也就是Controller的具体执行方法.
    mappedHandler=getHandler(processedRequest);// 根据handler获取handler适配器.
    HandlerAdapter ha=getHandlerAdapter(mappedHandler.getHandler());// 真正执行handler的地方.
    mv= ha.handle(processedRequest, response, mappedHandler.getHandler());}

DispatchServlet如何根据请求找到执行方法,就是通过请求映射。

解析getHandler方法:

@Nullableprotected HandlerExecutionChaingetHandler(HttpServletRequest request)throws Exception{if(this.handlerMappings!= null){for(HandlerMapping mapping:this.handlerMappings){
            HandlerExecutionChain handler= mapping.getHandler(request);if(handler!= null){return handler;}}}return null;}

有一个handlerMappings属性,它是List,说明有多个HandlerMapping。对于请求,会循环所有的HandlerMapping,只需要有一个HandlerMapping里能匹配到响应的方法即可。

也可自己添加自定义的HandlerMapping到容器中,自定义HandlerMapping

通过打断点,可以看到,SpringBoot中有个5个HandlerMapping,第一个RequestMappingHandlerMapping中有/hello对应的getHello方法。所以访问localhost:8080/hello就执行这个方法,即handler。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKpmBHuG-1614753646378)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228125519102.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbtBVJ6p-1614753646383)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228125250889.png)]

总结:HandlerMapping的作用其实就是请求路径与执行方法的映射。

3.2 常用请求参数注解

  • @PathVariable:路径变量

    • 使用RESTFUL风格时使用,可以获取请求路径中的变量。如localhost:8080/hello/zhangsan/10
    • 两种方式:一种是使用value,一种使用Map获取所有路径参数。
  • @RequestHeader: 请求头

  • @RequestParam: 请求参数

    • 传统的参数,如localhost:8080/hello?name=zhangsan&age=10
  • @CookieValue:请求的Cookie

  • @RequestBody:请求体

    • 针对于post请求,获取post请求的请求体。
@RequestMapping("/param/{name}/{age}")public StringgetTest(@PathVariable("name") String name,@PathVariable("age")int age1,@PathVariable Map<String,String> pathVariable,@RequestHeader("User-Agent") String agent,@RequestHeader Map<String, String> header,@RequestParam("name") String name2,@RequestParam("age")int age2,@RequestParam Map<String,String> requestParam,@CookieValue("_ga") String ga,@CookieValue Cookie cookie,@RequestBody String content){return"hello";}

通过实例可知,很多注解除了使用value值获取单个值外,还可以一次性全部获取相关的值。如@PathVariable Map<String ,String> pathVariable,一次性获取所有路径请求的值。

  • @RequestAttribute: 请求属性
    • 在转发、过滤器、拦截器中想HttpServletRequest对象中使用setAttribute添加值时,可以通过@RequestAttribute注解获取值。
    • 和使用getAttribute方法获取值的效果是一样的。
@ControllerpublicclassParamController{@RequestMapping("/goto")public StringtestA(HttpServletRequest request){
        request.setAttribute("msg","hello test B");return"forward:/success";//转发}@ResponseBody@RequestMapping("/success")public StringtestB(@RequestAttribute("msg") String msg){

        request.getAttribute("msg");return msg;}}
  • @MatrixVariable: 矩阵变量
    • 可以在请求路径中放置更多的值。如localhost:8080/hello/boss;name=zhangsan;age=50/10
    • 不太常用,更多语法及使用上网查查。
  • @ModelAttribute

3.3 参数解析 - 源码分析

在编写controller类里的方法的时候,即handler方法时,参数是比较随意和多样的,SpringBoot在反射执行该方法时,是需要给参数赋值的,如果确定各个参数的值是这次需要分析的点。

之前分析请求映射的原理时,分析了如何通过handlerMapping获取执行的handler。获取到mappedHandler之后,会根据handler获取handler适配器,再通过适配器执行handler。如下所示:

protectedvoiddoDispatch(HttpServletRequest request, HttpServletResponse response)throws Exception{
    HttpServletRequest processedRequest= request;
    HandlerExecutionChain mappedHandler= null;// 根据request获取执行的handler,也就是Controller的具体执行方法.
    mappedHandler=getHandler(processedRequest);// 根据handler获取handler适配器.
    HandlerAdapter ha=getHandlerAdapter(mappedHandler.getHandler());// 真正执行handler的地方.
    mv= ha.handle(processedRequest, response, mappedHandler.getHandler());}

分析getHandlerAdapter(mappedHandler.getHandler())

protected HandlerAdaptergetHandlerAdapter(Object handler)throws ServletException{if(this.handlerAdapters!= null){for(HandlerAdapter adapter:this.handlerAdapters){if(adapter.supports(handler)){return adapter;}}}}

同handlerMapping,也有多个handlerAdapter,打断点可知,有四个适配器。其中,RequestMappingHandlerAdapter就是适配RequestMapping这种注解形式的访问的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHueoMgO-1614753646385)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228184607906.png)]

获取handlerAdapter之后,会执行ha.handle方法,该方法的执行路径是:

ha.handle --> handleInternal --> invokeHandlerMethod

invokeHandlerMethod方法的简化版如下:

protected ModelAndViewinvokeHandlerMethod(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod)throws Exception{
    ServletInvocableHandlerMethod invocableMethod=createInvocableHandlerMethod(handlerMethod);if(this.argumentResolvers!= null){
        invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}if(this.returnValueHandlers!= null){
 invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}

    invocableMethod.invokeAndHandle(webRequest, mavContainer);}

可以看到:

  • this.argumentResolvers: 参数解析器
    • 通过断点可知,有27种参数解析器。
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zCP8huhL-1614753646386)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228185513535.png)]
  • this.returnValueHandlers: 返回值处理器
    • 这里有15中返回值处理器。
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-epvgHDys-1614753646388)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210228185705116.png)]

继续探究invocableMethod.invokeAndHandle(webRequest, mavContainer):

执行路径:invokeAndHandle --> invokeForRequest --> getMethodArgumentValues

在getMethodArgumentValues方法中,就是确定每个变量的值了。

protected Object[]getMethodArgumentValues(NativeWebRequest request,@Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs)throws Exception{

   Object[] args=newObject[parameters.length];for(int i=0; i< parameters.length; i++){if(!this.resolvers.supportsParameter(parameter)){throw xxx;}
      args[i]=this.resolvers.resolveArgument(parameter, mavContainer, request,this.dataBinderFactory);}return args;}

先来看一下参数处理器的接口结构:

publicinterfaceHandlerMethodArgumentResolver{booleansupportsParameter(MethodParameter parameter);@Nullable
	ObjectresolveArgument(MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest,@Nullable WebDataBinderFactory binderFactory)throws Exception;}

可以知道,handler方法参数处理器的执行过程就是先调用supportsParameter查看是否支持,再调用resolveArgument确定参数。

在getMethodArgumentValues方法中调用this.resolvers.resolveArgument获取参数:

public ObjectresolveArgument(MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest,@Nullable WebDataBinderFactory binderFactory)throws Exception{//根据参数,获取相应的参数处理器
    HandlerMethodArgumentResolver resolver=getArgumentResolver(parameter);//调用resolveArgument确定参数return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);}

分析getArgumentResolver方法:

private HandlerMethodArgumentResolvergetArgumentResolver(MethodParameter parameter){
   HandlerMethodArgumentResolver result=this.argumentResolverCache.get(parameter);if(result== null){for(HandlerMethodArgumentResolver resolver:this.argumentResolvers){if(resolver.supportsParameter(parameter)){
            result= resolver;this.argumentResolverCache.put(parameter, result);break;}}}return result;}

可以看出:

  • 从之前提到的27种参数解析器中选择一个支持该参数的,并返回。
  • 这里有一个缓存优化,this.argumentResolverCache.put(parameter, result),这样下次访问就不要再遍历一次所有的参数处理器了。

至此,参数就是解析完成,会得到一个Object[] args,并且附上了正确的值。在invokeForRequest方法调用doInvoke(args)执行。

3.4 常用参数的解析器

Servlet API

  • WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
  • 解析器是:ServletRequestMethodArgumentResolver

Map

  • MapMethodArgumentResolver

Model

  • ModelMethodArgumentResolver

应该能发现规律了。

map和model在作为参数时,在视图返回时,都会放到request.setAttribute中。

例如:在/goto转发到/success之后,可以通过request获取值。

@RequestMapping("/goto")public StringtestA(Map<String,Object> map, Model model, HttpServletRequest request){

    map.put("k1","val1");
    model.addAttribute("k2","val2");
    request.setAttribute("k3","val3");return"forward:/success";}@ResponseBody@RequestMapping("/success")public StringtestB(HttpServletRequest request){

    request.getAttribute("k1");//val1
    request.getAttribute("k2");//val2
    request.getAttribute("k3");//val3return"msg";}

原理在processDispatchResult解析视图中的exposeModelAsRequestAttributes方法:

protectedvoidexposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request)throws Exception{

    model.forEach((name, value)->{if(value!= null){
            request.setAttribute(name, value);}else{
            request.removeAttribute(name);}});}

3.5 自定义参数解析 - 源码

@ControllerpublicclassParamController{@ResponseBody@RequestMapping("/user")public UsergetUser(User user){//user就是自定义参数return user;}}

自定义参数使用了参数解析器是:ServletModelAttributeMethodProcessor

在该处理器的resolveArgument方法中(删除不必要的部分):

publicfinal ObjectresolveArgument(MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest,@Nullable WebDataBinderFactory binderFactory)throws Exception{

    Object attribute= null;
    BindingResult bindingResult= null;

    attribute=createAttribute(name, parameter, binderFactory, webRequest);if(bindingResult== null){// binder -- 代码01
        WebDataBinder binder= binderFactory.createBinder(webRequest, attribute, name);if(binder.getTarget()!= null){if(!mavContainer.isBindingDisabled(name)){bindRequestParameters(binder, webRequest);//-- 代码02}validateIfApplicable(binder, parameter);}

        bindingResult= binder.getBindingResult();}// Add resolved attribute and BindingResult at the end of the model
    Map<String, Object> bindingResultModel= bindingResult.getModel();
    mavContainer.removeAttributes(bindingResultModel);
    mavContainer.addAllAttributes(bindingResultModel);return attribute;}

在代码01处,创建了一个WebDataBinder对象,查看内部结构,可知:http传回参数是字符串文本,要把它解析为具体的对象,都是使用相应的转换器的。

WebDataBinder对象的内部数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AIJQx5zH-1614753646390)(C:\Users\zl\AppData\Roaming\Typora\typora-user-images\image-20210301110625217.png)]

看一下StringtoNumber最终执行的地方,也是工厂模式,最终执行是convert方法。

finalclassStringToNumberConverterFactoryimplementsConverterFactory<String, Number>{@Overridepublic<TextendsNumber> Converter<String, T>getConverter(Class<T> targetType){returnnewStringToNumber<>(targetType);}privatestaticfinalclassStringToNumber<TextendsNumber>implementsConverter<String, T>{privatefinal Class<T> targetType;publicStringToNumber(Class<T> targetType){this.targetType= targetType;}@Override@Nullablepublic Tconvert(String source){if(source.isEmpty()){return null;}return NumberUtils.parseNumber(source,this.targetType);}}}

3.6 自定义返回值

  • 作者:幻行
  • 原文链接:https://blog.csdn.net/zl498380233/article/details/114313809
    更新时间:2022-09-03 08:08:36