Spring Boot学习笔记总结(四)

1. Web请求参数处理 Rest映射源码解析

1.1 OrderedHiddenHttpMethodFilter对象原理


Rest风格:

  • 注意这里是Rest风格,并不是Restful风格不要混淆!
  • 可以直接认为Rest风格就是@RestController和@ResponseBody。
package com.itholmes.boot.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

//RestController就是rest风格,还有@Responsebody
@RestController
public class HelloController {
	
	//相同的路径/user,来处理不同的请求类型。
    @RequestMapping(value = "/user" ,method = RequestMethod.GET)
    //等同于@GetMapping("/user")
    public String getUser(){
        return "GET-张三";
    }

    @RequestMapping(value = "/user" ,method = RequestMethod.POST)
     //等同于@PostMapping("/user")
    public String saveUser(){
        return "POST-张三";
    }

    @RequestMapping(value = "/user" ,method = RequestMethod.PUT)
     //等同于@PutMapping("/user")
    public String putUser(){
        return "PUT-张三";
    }

    @RequestMapping(value = "/user" ,method = RequestMethod.DELETE)
     //等同于@DeleteMapping("/user")
    public String deleteUser(){
        return "DELETE-张三";
    }
}

对于rest风格的参数类型处理,必须要给容器中注入一个OrderedHiddenHttpMethodFilter对象,这个对象也是在WebMvcAutoConfiguration类自动配置注入。

在WebMvcAutoConfiguration类下,有一个hiddenHttpMethodFilter(排序隐藏Http方法过滤器)方法,这个方法的作用是过滤请求参数的方法类型,例如:get,post,delete,put等等。注意这个hiddenHttpMethodFilter装配到容器中,该对象本身是一个filter过滤器!

@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
    prefix = "spring.mvc.hiddenmethod.filter",
    name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
}

但是在@ConditionalOnProperty注解中有一个matchIfMissing注解属性,默认是false的!这个属性的功能如下:

/**
 *该属性为true时,配置文件中缺少对应的value或name的对应的属性值,也会注入成功
 */
boolean matchIfMissing() default false;

因为是matchIfMissing默认是false,所以开始的HiddenHttpMethodFilter过滤器是没有装配到容器中的,因此我们要设置spring.mvc.hiddenmethod.filter.enabled=true来启动该过滤器(装配到容器中)。

可以理解的是spring.mvc.hiddenmethod.filter.enabled=true是专门用来处理表单的Rest功能。

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true # 启用HiddenHttpMethodFilter过滤器,专门用来处理表单传送过来的_method的参数数据,进而来出来delete,put等类型参数。

1.2 表单提交要使用Rest的使用


对应上面内容。

这样我们在前端发送不同类型,后台就可以处理了:

  • 表单提交要带上_method=PUT , 这样的方式来能让SpringBoot自动配置的hiddenHttpMethodFilter方法识别到!
  • 再次提醒,不要忘记启动hiddenHttpMethodFilter组件对象,也就是必须要装配到容器中(被启动)。因为上面的这种方式必须基于该组件对象。
测试REST风格:
<form action="/user" method="get">
    <input type="submit" value="REST-GET">
</form>
<form action="/user" method="POST">
    <input type="submit" value="REST-POST">
</form>
<!--
    form表单是不能提交delete,put等类型的,直接定义这些类型,form表单还是走默认的get方法。
    根据hiddenHttpMethodFilter方法源码:通过传递一个name为"_method"值的参数来指定,并且方法的类型必须是post方法,因为它的源码规定这里必须是post方式!。
-->
<form action="/user" method="post">
    <input name="_method" type="hidden" value="DELETE">
    <input type="submit" value="REST-delete">
</form>
<form action="/user" method="post">
    <input name="_method" type="hidden" value="PUT">
    <input type="submit" value="REST-put">
</form>

hiddenHttpMethodFilter在容器中本质是一个OrderedHiddenHttpMethodFilter对象,该对象又继承了HiddenHttpMethodFilter对象,在HiddenHttpMethodFilter对象中,有一个doFilterInternal方法:

private static final List<String> ALLOWED_METHODS;

static {
	//这里定义支持哪几种请求,有delete,put,patch这三种。也就是允许这三种请求。
    ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
}


protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
	//拿到当前的请求request
    HttpServletRequest requestToUse = request;
    //先判断是不是post方法,也说明了前面form表单传递_method=PUT形式为什么要用post,这里规定的就是post。并且判断请求是否正常,有没有error。
    if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
    	//在request获取参数,这里的this.methodParam是整个类写死的一个值就是"_method";
    	//也就是在request中获取到了_method的参数了,也就是form表单传的delete,put等等。
        String paramValue = request.getParameter(this.methodParam);
        //这里判断有没有长度,也就是判断是不是空
        if (StringUtils.hasLength(paramValue)) {
        	//将值进行一个大写转换。也就迎合了前端发送大写小的方法名都可以。
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            //对应上面ALLOWED_METHODS,判断是否属于ALLOWED_METHODS,仅能支持delete,put,patch。
            if (ALLOWED_METHODS.contains(method)) {
            	//将原生request(post请求) , 包装模式requestWrapper重写了getMethod方法,返回的是重写后的request(就是这里的method请求[delete,put,patch请求])
                requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
            }
        }
    }
	//之后就是将处理后的requestToUse放行了。过滤器放行。
    filterChain.doFilter((ServletRequest)requestToUse, response);
}

==========================================================================================
//对应上面的HttpMethodRequestWrapper
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
    private final String method;

    public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
        super(request);
        this.method = method;
    }
	//这里将request的值重写了,也就是原来是request的method是post,现在重写为了根据_method传入的类型(delete,put,patch)
    public String getMethod() {
        return this.method;
    }
}

这样也就将原来的post请求类型,通过OrderedHiddenHttpMethodFilter过滤器转成了不同的请求类型。

1.3 Rest使用客户端工具发送不同类型的请求


例如:使用postman发送的put参数。
在这里插入图片描述
也就是不影响原生的功能。

1.4 如何改变默认的_method?


自己创建一个HiddenHttpMethodFilter对象,放入容器中。
在这里插入图片描述


自己写一个配置类,注入到容器中:

package com.itholmes.boot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;

@Configuration(proxyBeanMethods = false)
public class WebConfig {

    @Bean
    //注意这里有两个HiddenHttpMethodFilter对象,一个是普通servlet使用的,一个是响应式使用的。
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        //设置为自己想要的名字。
        hiddenHttpMethodFilter.setMethodParam("_m");
        return hiddenHttpMethodFilter;
    }

}

在这里插入图片描述

2. Web请求参数处理 请求映射原理


DispatcherServlet类实现的过程,例如,实现get方法的过程,也是从上往下:
在这里插入图片描述
SpringMvc功能分析都从org.springframework.web.servlet.DispatcherServlet =》doDispatch()方法实现。

分析doDispatch方法:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;

            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;

				//找到当前请求使用哪个Handler(Controller的方法)处理。
                mappedHandler = this.getHandler(processedRequest);

=================================================================
                
//查看getHandler方法,里面有一个HandlerMapping处理器映射。
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        Iterator var2 = this.handlerMappings.iterator();

        while(var2.hasNext()) {
            HandlerMapping mapping = (HandlerMapping)var2.next();
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }

    return null;
}

通过发送一个路径请求,查看handlerMappings中的内容:

  • RequestMappingHandlerMapping作用就是处理所有@RequestMapping注解 和 handler的映射规则。
  • WelcomePageHandlerMapping是处理欢迎页功能的映射。
    在这里插入图片描述
    在这里插入图片描述

SpringBoot自动配置欢迎页的WelcomePageHandlerMapping的源码分析(访问 " / " 能访问到index.html):

因为这里while循环的遍历,依次尝试所有的HandlerMapping看是否有对应路径的请求信息

  • 如果找到了对应的请求handler就会return handler。
  • 如果没有找到就继续遍历下一个handler。

在这里插入图片描述

总结起来就是知道RequestMappingHandlerMappin和WelcomePageHandlerMapping在SpringMvc中什么作用!剩余的三个了解

此外,还可以自定义映射处理,我们也可以自己给容器中放HandlerMapping。自定义HandlerMapping。

3. 请求处理常用的参数注解

3.1 SpringMvc常用的一些注解


@PathVariable的用法:

  • @PathVariable(“id”) Integer id://@PathVariable(“id”)拿到对应的值。
  • @PathVariable Map<String,String> path_value://拿到所有的@PathVariable注解对应的key-value(也就是指定@PathVariable注解的value拿指定的,没有指定就拿所有和@PathVariable绑定的key-value。)

@RequestHeader的用法:

  • @RequestHeader(“User-Agent”) String userAgent://@RequestHeader注解拿到请求头的信息。
  • @RequestHeader Map<String,String> headers://和上面一样,拿到所有的请求头信息。指定@RequestHeader注解的value值就拿到对应指定的,没有指定就拿全部的。

@RequestParam的用法:

  • @RequestParam(“age”) Integer age://接受form表单格式单个。
  • @RequestParam(“inters”) List inters://当form表单对一个key指定了多个value可以使用list来接受,例如: ?inters=1&inters=2&inters=4。
  • @RequestParam Map<String,String> params://和上面一样,获取全部的form表单形式的参数。

@CookieValue的用法:

  • @CookieValue(“token”) String value://在cookie中拿到token的value值。
  • @CookieValue(“token”) Cookie cookie://拿到token的整个cookie。

@RequestBody的用法:

  • @RequestBody String content://如果是form表单提交,例如:name=zhangsan&pwd=123123 , 这里的content就会变成{“name”:“zhangsan”,“pwd”:“123123”}

@RequestAttribute的用法:
在这里插入图片描述

3.2 SpringBoot的矩阵变量 和 UrlPathHelper类


什么是矩阵变量,矩阵变量以及对比其他的方式?

  • /cars/path?xxx=xxx&aaa=ccc 这可以是queryString查询字符串,后台用@RequestParam接受。
  • /cars/sell;low=34;brand=byd,audi,yd 使用" ; "的这种形式叫做矩阵变量。

页面开发,cookie禁用了,session里面的内容怎么使用?

  • 可以使用url重写:/abc;jsessionid=xxxx 把cookie的值使用矩阵变量的方式进行传递。

/boss/1/2 对比 /boss/1;age=20/2;age=18 分号;前面是路径,后面是矩阵变量。


SpringBoot默认是禁用了矩阵变量的功能。

原理:在WebMvcAutoConfiguration中,有一个configurePathMatch方法,在该方法中有一个UrlPathHelper(Url路径帮助器) 类,在这个类中,有一个removeSemicolonContent(Semicolon分号)成员变量属性,这个属性就是规定SpringBoot支不支持矩阵变量的(也就是是否移除分号后面的内容。)。
在这里插入图片描述

UrlPathHelper类有很多的功能:
在这里插入图片描述


如何开启SpringBoot支持矩阵变量的功能?

  • 可以在配置类中自定义配置WebMvcConfigurer容器对象,来修改UrlPathHelper对象中的removeSemicolonContent变量。
package com.itholmes.boot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;

@Configuration(proxyBeanMethods = false)
/*
    第一种方式:直接implement的WebMvcConfigurer。
        并且重写configure
*/
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        //设置为false,也就是不移除分号后面的内容,这样矩阵变量可以生效。
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }

    /*
        第二种方式:
            使用@Bean,直接向容器中装配一个WebMvcConfigurer。

        @Bean
        public WebMvcConfigurer webMvcConfigurer(){
            return new WebMvcConfigurer() {
                @Override
                public void configurePathMatch(PathMatchConfigurer configurer) {
                    UrlPathHelper urlPathHelper = new UrlPathHelper();
                    //设置为false,也就是不移除分号后面的内容,这样矩阵变量可以生效。
                    urlPathHelper.setRemoveSemicolonContent(false);
                    configurer.setUrlPathHelper(urlPathHelper);
                }
            };
        }
    */

}

获取矩形变量也有格式上的一些要求:

  • 例如: “/cars/{path}” 。
  • @PathVariable(“path”) String path拿到的就是path里面分号前面的路径。
  • @MatrixVariable(“xxx”) 拿到的就是矩形变量xxx对应的内容。
 /*
    矩形变量注意点:
        1.SpringBoot默认是禁用了矩形变量的功能。
        2.矩形变量必须有url路径变量才能被解析,也就是/cars/sell;low=34 => {path} sell就是路径,low就是矩形变量。
*/

//第一种情况: 一个key对应一个值,一个key对应多个值。
// 例如矩阵变量的语法为: /cars/sell;low=34;brand=byd,audi,yd
@GetMapping("/cars/{path}")
public Map carsSell(
        //获取path的路径。
        @PathVariable("path") String path,
        //拿到path中的矩阵变量。
        @MatrixVariable("low") Integer low,
        @MatrixVariable("brand") List<String> brand
){
    HashMap<String, Object> map = new HashMap<>();
    map.put("low",low);
    map.put("brand",brand);
    map.put("path",path);
    return map;
}

//第二种情况: 两个{}下的相同key。
// /boss/1;age=20/2;age=18
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(
    //通过pathVar来指定获取哪一个的age。
    @MatrixVariable(value = "age" , pathVar = "bossId") Integer bossAge,
    @MatrixVariable(value = "age", pathVar = "empId") Integer empAge
){
    HashMap<String, Object> map = new HashMap<>();
    map.put("bossAge",bossAge);
    map.put("empAge",empAge);
    return map;
}

在这里插入图片描述

4. 请求参数处理 类型参数的解析原理

4.1 找到合适的HandlerAdapter处理器的源码分析


同样和HandlerMapping一样,HandlerAdapter也会是在DispatcherServlet的doDispatch方法中运行:
在这里插入图片描述

接下来说的是Handler适配器,HandlerAdapter如何处理一些参数。

对于各种类型参数的解析原理,使用的是适配器HandlerAdapter来解决。

  • handlerMapping中找到能处理请求的handler(controller)。为当前Handler(controller)找一个适配器HandlerAdapter。

在这里插入图片描述
上图是SpringMvc默认的4个HandlerAdapter,handler适配器:

  • 0 - RequestMappingHandlerAdapter是支持方法上标注@RequestMapping。
  • 1 - HandlerFunctionAdapter是支持函数式变成的。

执行的核心又在这里:

//拿到能处理的handlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
//在这里执行handlerAdapter
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

4.2 HandlerAdapter的执行目标方法


//在这里执行handlerAdapter
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

mappedHandler.getHandler()的源码解析:
在这里插入图片描述

handle方法源码:
在这里插入图片描述

继续step into深入,就会看到继承了abstractHandlerMethodAdapter的RequestMappingHandlerAdapter类,在该类有一个handleInternal方法,该方法如下代码:

//这段代码就是来执行目标方法的。就是controller那些类里面的方法。
mav = this.invokeHandlerMethod(request, response, handlerMethod);

在invokeHandlerMethod方法中有一个参数解析器:

  • 每一个argumentResolvers参数解析器分别是对应了处理不同注解接受参数的形式。像@PathVariable,@RequestParam等等注解。
  • SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
    在这里插入图片描述

argumentResolvers参数解析器的实现接口。

public interface HandlerMethodArgumentResolver {
	//当前解析器会判断是否支持解析这种参数
    boolean supportsParameter(MethodParameter parameter);

	//支持就调用resolveArgument方法。
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

还有一个returnValueHandlers返回值处理器,决定能返回哪些类型:
在这里插入图片描述

最终参数解析器和返回值处理器都会集中到invocableMethod变量中。
在这里插入图片描述

//invocableMethod会调用invokeAndHandle方法。
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);

====================================
//在里面的源码中,有这段代码,这段代码会执行请求指向的handler(Controller层)的方法内容。
//同样在这个invokeForRequest中就会获取请求handler(controller)对应方法的参数值。
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);

====================================
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
	//args就是拿到对应请求handler方法中的所有参数。
    Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }

    return this.doInvoke(args);
}


//getMethodArgumentValues方法对应上面执行的方法。
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
	  //paramters存放了每个参数的详细信息。
      MethodParameter[] parameters = this.getMethodParameters();
      if (ObjectUtils.isEmpty(parameters)) {
          return EMPTY_ARGS;
      } else {
          Object[] args = new Object[parameters.length];

          for(int i = 0; i < parameters.length; ++i) {
              MethodParameter parameter = parameters[i];
              parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
              args[i] = findProvidedArgument(parameter, providedArgs);
              if (args[i] == null) {
              	  //判断这20多个解析器中有能支持该参数的解析器么。
                  if (!this.resolvers.supportsParameter(parameter)) {
                      throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
                  }

                  try {
                  	  //使用参数解析器来解析参数paramter
                      args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                  } catch (Exception var10) {
                      if (logger.isDebugEnabled()) {
                          String exMsg = var10.getMessage();
                          if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                              logger.debug(formatArgumentError(parameter, exMsg));
                          }
                      }

                      throw var10;
                  }
              }
          }

          return args;
      }
  }

在这里插入图片描述

paramters存放了每个参数的详细信息:
在这里插入图片描述

通过判断是否有RequestParam注解,从而判断该参数解析器是否支持:
在这里插入图片描述

随后一步步解析,拿到请求发送过来的参数。

5. 请求参数处理 Servlet API参数解析原理


在这里插入图片描述
ServletRequestMethodArgumentResolver解析器是专门处理将原生的request发给handler方法。
在这里插入图片描述
此外,还支持一下几种方式:
在这里插入图片描述

6. 请求参数处理 Model和Map原理


在这里插入图片描述


handler方法参数为map,使用MapMethodProcessor解析器来处理。
在这里插入图片描述
Map类型的参数,底层会调用mavContainer.getModel(); ==> BindingAwareModelMap是Model也是Map。
在这里插入图片描述


ModelMethodProcessor处理器是处理handler方法参数为Model类型的参数。
在这里插入图片描述

Model类型的参数,底层也会调用mavContainer.getModel()方法 最终获取到值的。
在这里插入图片描述


目标方法执行完成:将所有的数据放在ModelAndViewContainer,也包含要去的页面地址view,还包含Model数据。

跳过35-42。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xupengboo

你的鼓励将是我创作最大的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值