SpringBoot 的入门学习(2):SpringMVC自动配置之静态资源、Rest映射,请求处理,参数解析,响应处理

1、 SpringMVC 自动配置的场景概述

在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。

只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析;途径二:官方文档!

官网阅读

官方文档内容概述如下:

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
    内容协商视图解析器和BeanName视图解析器

  • Support for serving static resources, including support for WebJars 。
    静态资源(包括webjars)

  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    自动注册了转换器(这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型),格式化器(比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象)】

  • Support for HttpMessageConverters (covered later in this document).
    支持HttpMessageConverters( SpringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;)

  • Automatic registration of MessageCodesResolver (covered later in this document).
    定义错误代码生成规则的

  • Static index.html support.
    首页定制

  • Custom Favicon support (covered later in this document).
    图标定制

  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
    初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!

  • If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
    不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则

  • If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
    如果希望提供RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandler、ExceptionResolver的自定义实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。

  • If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.
    使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

2、Web开发静态资源【使用方法】

静态资源目录

只要静态资源放在下面几种路径下就可以正常访问:

  • /static
  • /public
  • /resources
  • /META-INF/resources

访问路径:当前项目根路径/ + 静态资源名
原理: 静态映射/**。

  • 请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。

可以改变默认的静态资源路径,如下:

resources:
  static-locations: [classpath:/haha/]
  • 此时我们在location上加了一个大括号,表示该localtions为数组,也就是说还可以在后面加其他路径。
  • 注意:改变后 /static,/public,/resources, /META-INF/resources 等静态资源路径会失效

welcome与favicon功能

官方文档

欢迎页支持

  • 静态资源路径下 index.html 为默认欢迎页。

  • 可以通过配置静态资源路径改变目录,但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问

    spring:
    #  mvc:
    #    static-path-pattern: /res/**   这个会导致welcome page功能失效
      resources:
        static-locations: [classpath:/haha/]
    
  • controller也能处理/index。

自定义Favicon

  • 指网页标签上的小图标。
  • favicon.ico 放在静态资源目录下即可。

3、Web开发静态资源【源码分析】

使用SpringBoot的步骤:

  • SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
  • SpringMVC功能的自动配置类WebMvcAutoConfiguration

写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?

方法1:导入已有资源

SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 这个配置类里面;
我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
有一个方法:addResourceHandlers 添加资源处理,如下:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        // 已禁用默认资源处理
        logger.debug("Default resource handling disabled");
        return;
    }
    // 缓存控制
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    // webjars 配置
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    // 静态资源配置
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

读一下源代码可以得到下面两个信息

  • 如果将 spring:resources:add-mappings: 置为 false 那么就能够进制掉后面的静态资源规则:

    spring:
      resources:
        add-mappings: false   #禁用所有静态资源规则
    
  • 所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;

什么是webjars 呢?
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。现在使用 springboot时,我们需要使用Webjars 来导入资源。可以点击这里 访问 webjar的官方网站,如下:a在这里插入图片描述
可以看到webjar中保存了大部分常用的 静态资源的jar 包。我们只需要使用 maven 直接引入即可:

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>

导入完毕,查看 webjars 目录结构,并访问 Jquery.js 文件!
在这里插入图片描述
访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js
在这里插入图片描述

方法2:导入自定义资源

还是从 addResourceHandlers 中查看,可以发现还有一种 getstaticPathPattern 的方法,进行静态资源的导入。
在这里插入图片描述
点开这个方法,代码如下:

// 进入方法
public String[] getStaticLocations() {
    return this.staticLocations;
}
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
    "classpath:/META-INF/resources/",
  "classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/" 
};

所以得出结论,以下四个目录存放的静态资源可以被我们识别:

"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件。这些文件夹的存在的优先级如下:

说明优先级: resources>static>public

自定义静态资源路径: 当然处理上面四种路径,还可以通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;

spring.resources.static-locations=classpath:/coding/,classpath:/kuang/

静态资源访问前缀: 如果需要给所有静态访问添加前缀:

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

默认首页路径的处理规则

静态资源文件夹说完后,继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                                                           FormattingConversionService mvcConversionService,
                                                           ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
        new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), // getWelcomePage 获得欢迎页
        this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    return welcomePageHandlerMapping;
}

点进去继续看

private Optional<Resource> getWelcomePage() {
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    // ::是java8 中新引入的运算符
    // Class::function的时候function是属于Class的,应该是静态方法。
    // this::function的funtion是属于这个对象的。
    // 简而言之,就是一种语法糖而已,是一种简写
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
// 欢迎页就是一个location下的的 index.html 而已
private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

欢迎页,静态资源文件夹下的所有 index.html 页面;被 /** 映射。

比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html

4、Request 映射-【使用方法】

使用

  • 注解如下 的格式为 @xxxMapping;
    • @GetMapping
    • @PostMapping
    • @PutMapping
    • @DeleteMapping
  • Rest的 HTTP 请求方式
    • 以前:

      • /getUser 获取用户
      • /deleteUser 删除用户
      • /editUser 修改用户
      • /saveUser保存用户
    • 现在: /user

      • GET-获取用户
      • DELETE-删除用户
      • PUT-修改用户
      • POST-保存用户
  • 核心Filter;HiddenHttpMethodFilter

具体实例:

  • 开启页面表单的Rest功能

    spring:
      mvc:
        hiddenmethod:
          filter:
            enabled: true   #开启页面表单的Rest功能
    
  • 页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)

    <form action="/user" method="get">
        <input value="REST-GET提交" type="submit" />
    </form>
    
    <form action="/user" method="post">
        <input value="REST-POST提交" type="submit" />
    </form>
    
    <form action="/user" method="post">
        <input name="_method" type="hidden" value="DELETE"/>
        <input value="REST-DELETE 提交" type="submit"/>
    </form>
    
    <form action="/user" method="post">
        <input name="_method" type="hidden" value="PUT" />
        <input value="REST-PUT提交"type="submit" />
    <form>
    
    @GetMapping("/user")
    //@RequestMapping(value = "/user",method = RequestMethod.GET)
    public String getUser(){
        return "GET-张三";
    }
    
    @PostMapping("/user")
    //@RequestMapping(value = "/user",method = RequestMethod.POST)
    public String saveUser(){
        return "POST-张三";
    }
    
    @PutMapping("/user")
    //@RequestMapping(value = "/user",method = RequestMethod.PUT)
    public String putUser(){
        return "PUT-张三";
    }
    
    @DeleteMapping("/user")
    //@RequestMapping(value = "/user",method = RequestMethod.DELETE)
    public String deleteUser(){
        return "DELETE-张三";
    }
    
    

原理

Rest原理: 表单提交要使用REST的时候,请求过来被 HiddenHttpMethodFilter拦截,拦截器将把提交方式赋值给 _method参数。

public class HiddenHttpMethodFilter extends OncePerRequestFilter {

	private static final List<String> ALLOWED_METHODS =
			Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
					HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));

	/** Default method parameter: {@code _method}. */
	public static final String DEFAULT_METHOD_PARAM = "_method";

	private String methodParam = DEFAULT_METHOD_PARAM;


	/**
	 * Set the parameter name to look for HTTP methods.
	 * @see #DEFAULT_METHOD_PARAM
	 */
	public void setMethodParam(String methodParam) {
		Assert.hasText(methodParam, "'methodParam' must not be empty");
		this.methodParam = methodParam;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		//1、拿到请求
		HttpServletRequest requestToUse = request;
		//2、判断表单是否为 post,说明只有post才能用REST风格
		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
			//3、获取 _method 的值
			String paramValue = request.getParameter(this.methodParam);
			if (StringUtils.hasLength(paramValue)) {
			//4、所有的请求字符变为大写(所以小写也可以)
				String method = paramValue.toUpperCase(Locale.ENGLISH);
			//判断允许的请求方式中是否包含method 请求
			//点进去可以看到,兼容:PUT.DELETE.PATCH
				if (ALLOWED_METHODS.contains(method)) {
					requestToUse = new HttpMethodRequestWrapper(request, method);
				}
			}
		}

		filterChain.doFilter(requestToUse, response);
	}


	/**
	 * Simple {@link HttpServletRequest} wrapper that returns the supplied method for
	 * {@link HttpServletRequest#getMethod()}.
	 */
	 //原生 request 使用的为 post,这里使用包装模式,重写了getMethod方法,返回传入的值。
	 //过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
	private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {

		private final String method;

		public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
			super(request);
			this.method = method;
		}

		@Override
		public String getMethod() {
			return this.method;
		}
	}

}

因此,在前端提交REST格式的请求还可以用post方法,并且传递参数 _method。如下:
在这里插入图片描述

原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。

改变默认的_method

  • 首先查看 WebMvcAutoConfiguration 的源代码

    @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 })
    public class WebMvcAutoConfiguration {
    
        ...
        
        @Bean
        @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
        @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
        public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
            return new OrderedHiddenHttpMethodFilter();
        }
        
        ...
    }
        
    
  • 其中有一句 @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) 表示没有HiddenHttpMethodFilter时,才执行hiddenHttpMethodFilter()。因此,我们可以自定义filter,改变默认的_method

  • 举例,此时我们将默认的 “_method” 属性改为"_m",如下:

    @Configuration(proxyBeanMethods = false)
    public class WebConfig{
        //自定义filter
        @Bean
        public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
            HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
            methodFilter.setMethodParam("_m");
            return methodFilter;
        }    
    }
    
    <form action="/user" method="post">
        <input name="_m" type="hidden" value="DELETE"/>
        <input value="REST-DELETE 提交" type="submit"/>
    </form>
    
    

总结

在面对Rest格式的请求时的具体步骤:

  • 首先 Rest 请求的基本原理肯定是存在一个过滤器,对发送过来的请求进程处理。而这个过滤器默认关闭,只有通过spring:mvc: hiddenmethod: filter:enabled: true 来开启页面表单的Rest功能
  • 浏览器发送一个请求,到达过滤器后,过滤器会判断该请求是否为 POST,浏览器只会对POST请求的request进行包装。
  • 如果是POST请求,就需要查看,其中的 “_method” 参数,该参数决定了 是PUT,DELETE 还是 PATCH。
  • 在得到“_method” 后会进行统一的大写转换。
  • 然后利用包装模式,加入对应的请求格式。

5、Request 映射【源码分析】

接下来我们探索一下,发送一个请求,如何映射到指定的controller位置的

  • Servlet 中的 doGet 的调用顺序如下:
    在这里插入图片描述

  • 换句话说,doGet 最终调用的是 doDispatch() 。SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet -> 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 {
            ModelAndView mv = null;
            Exception dispatchException = null;
    
            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);
    
                // 找到当前请求使用哪个Handler(Controller的方法)处理
                mappedHandler = getHandler(processedRequest);
    
                //HandlerMapping:处理器映射。/xxx->>xxxx
        ...
    }
    
    
  • getHandler()方法如下:

    @Nullable
    protected HandlerExecutionChain getHandler(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;
    }
    
  • this.handlerMappings 在Debug模式下展现的内容:
    在这里插入图片描述

  • 其中的RequestMappingHandlerMapping保存了所有@RequestMapping 和handler的映射规则。
    在这里插入图片描述

总结:

  • 在发送一个请求时,请求会通过Servlet进行接收。Servlet会调用其子类 spring.web.servlet.DispatcherdoDispatch() 方法。
  • 在这个方法中,会根据请求寻找相应的handler(即controller)
  • 而这个controller存储在一个叫做 RequestMappingHandlerMapping 的map中。该map以请求为key,以controller为value。只要直接 get 就能够获得。

IDEA快捷键:查看继承类,查看层次结构

  • Ctrl + Alt + U : 以UML的类图展现类有哪些继承类,派生类以及实现哪些接口。
  • Crtl + Alt + Shift + U : 同上,区别在于上条快捷键结果在新页展现,而本条快捷键结果在弹窗展现。
  • Ctrl + H : 以树形方式展现类层次结构图。

6、常用参数注解使用

  • @PathVariable 路径变量
  • @RequestHeader 获取请求头
  • @RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2)
  • @CookieValue 获取Cookie值
  • @RequestAttribute 获取request域属性
  • @RequestBody 获取请求体[POST]
  • @MatrixVariable 矩阵变量

例子1:获取变量,请求参数,请求头,cookie

@RestController
public class ParameterTestController {


    //  car/2/owner/zhangsan
    @GetMapping("/car/{id}/owner/{username}")
    public Map<String,Object> getCar(@PathVariable("id") Integer id,
                                     @PathVariable("username") String name,
                                     @PathVariable Map<String,String> pv,
                                     @RequestHeader("User-Agent") String userAgent,
                                     @RequestHeader Map<String,String> header,
                                     @RequestParam("age") Integer age,
                                     @RequestParam("inters") List<String> inters,
                                     @RequestParam Map<String,String> params,
                                     @CookieValue("_ga") String _ga,
                                     @CookieValue("_ga") Cookie cookie){

        Map<String,Object> map = new HashMap<>();

//        map.put("id",id);
//        map.put("name",name);
//        map.put("pv",pv);
//        map.put("userAgent",userAgent);
//        map.put("headers",header);
        map.put("age",age);
        map.put("inters",inters);
        map.put("params",params);
        map.put("_ga",_ga);
        System.out.println(cookie.getName()+"===>"+cookie.getValue());
        return map;
    }


    @PostMapping("/save")
    public Map postMethod(@RequestBody String content){
        Map<String,Object> map = new HashMap<>();
        map.put("content",content);
        return map;
    }
}

例子2:获取 request 域,请求体

@Controller
public class RequestController {

    @GetMapping("/goto")
    public String goToPage(HttpServletRequest request){

        request.setAttribute("msg","成功了...");
        request.setAttribute("code",200);
        return "forward:/success";  //转发到  /success请求
    }

    @GetMapping("/params")
    public String testParam(Map<String,Object> map,
                            Model model,
                            HttpServletRequest request,
                            HttpServletResponse response){
        map.put("hello","world666");
        model.addAttribute("world","hello666");
        request.setAttribute("message","HelloWorld");

        Cookie cookie = new Cookie("c1","v1");
        response.addCookie(cookie);
        return "forward:/success";
    }

    ///<-----------------主角@RequestAttribute在这个方法
    @ResponseBody
    @GetMapping("/success")
    public Map success(@RequestAttribute(value = "msg",required = false) String msg,
                       @RequestAttribute(value = "code",required = false)Integer code,
                       HttpServletRequest request){
        Object msg1 = request.getAttribute("msg");

        Map<String,Object> map = new HashMap<>();
        Object hello = request.getAttribute("hello");
        Object world = request.getAttribute("world");
        Object message = request.getAttribute("message");

        map.put("reqMethod_msg",msg1);
        map.put("annotation_msg",msg);
        map.put("hello",hello);
        map.put("world",world);
        map.put("message",message);

        return map;
    }
}

例子3:获取矩阵变量

  • 矩阵变量: 在 url中用 ; 分隔的这些变量的集合。如:/cars/sell;low=34;brand=byd,audi,yd当cookie 被禁用时,由于session的sessionid存储在cookie中,因此无法进行获取。这是就可以将sessionID用矩阵变量的形式,利用url进行传递。

  • 具体语法: /cars/sell;low=34;brand=byd,audi,yd
    其中 sell 表示该矩阵变量。后面的每个等于好表示一个key和一个value。一个key 可以对应一个value数组。

  • SpringBoot默认是禁用了矩阵变量的功能。基本原理是所有url的处理其实都是交给UrlPathHelper 进行处理的。UrlPathHelper 其中一个方法为 removeSemicolonContent,该方法翻译过来是是否删除分号后面的所有字符,默认为 True。也就是不支持 矩阵变量。

  • 由于默认的MVC配置使用的默认的 UrlPathHelper,因此为了能够让其支持矩阵变量,我能就必须自定义一个MVC配置。这里就需要用到 定制SpringMVC配置文件了。自定义规则的方式,官方文档给了三种:
    在这里插入图片描述

  • 手动开启矩阵变量方法1: 实现WebMvcConfigurer接口,利用proxyBeanMethods = false 关闭默认的MVC自动配置器。然后重写里面的configurePathMatch方法,来自定义url解析方式。

    @Configuration(proxyBeanMethods = false)
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
    
            UrlPathHelper urlPathHelper = new UrlPathHelper();
            // 不移除;后面的内容。矩阵变量功能就可以生效
            urlPathHelper.setRemoveSemicolonContent(false);
            configurer.setUrlPathHelper(urlPathHelper);
        }
    }
    
    
  • 手动开启矩阵变量方法2: 直接创建一个 bean:WebMvcConfigurer。

    @Configuration(proxyBeanMethods = false)
    public class WebConfig{
        @Bean
        public WebMvcConfigurer webMvcConfigurer(){
            return new WebMvcConfigurer() {
                            @Override
                public void configurePathMatch(PathMatchConfigurer configurer) {
                    UrlPathHelper urlPathHelper = new UrlPathHelper();
                    // 不移除;后面的内容。矩阵变量功能就可以生效
                    urlPathHelper.setRemoveSemicolonContent(false);
                    configurer.setUrlPathHelper(urlPathHelper);
                }
            }
        }
    }
    
    
  • 此时测试 @MatrixVariable

    @RestController
    public class ParameterTestController {
    
        ///cars/sell;low=34;brand=byd,audi,yd
        @GetMapping("/cars/{path}")
        public Map carsSell(@MatrixVariable("low") Integer low,
                            @MatrixVariable("brand") List<String> brand,
                            @PathVariable("path") String path){
            Map<String,Object> map = new HashMap<>();
    
            map.put("low",low);
            map.put("brand",brand);
            map.put("path",path);
            return map;
        }
    
        // /boss/1;age=20/2;age=10
    
        @GetMapping("/boss/{bossId}/{empId}")
        public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
                        @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
            Map<String,Object> map = new HashMap<>();
    
            map.put("bossAge",bossAge);
            map.put("empAge",empAge);
            return map;
    
        }
    
    }
    
    

7、参数解析的过程【源码解析】

①、参数解析的流程

所有的请求从 DispatcherServlet 开始:

public class DispatcherServlet extends FrameworkServlet {
    
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

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

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

                // Determine handler for the current request.                
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // Determine handler adapter for the current request.
               // 为当前 Handler 找一个适配器 `HandlerAdapter`,用的最多的是RequestMappingHandlerAdapter。

                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                ...
        // Actually invoke the handler.
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

方法处理总结:

  • 1、HandlerMapping 中找到能处理请求的 Handler(Controller.method())

  • 2、为当前 Handler 找一个适配器 HandlerAdapter,用的最多的是RequestMappingHandlerAdapter。

  • 3、拿 RequestMappingHandlerAdapter 适配器举例,如果此时为 RequestMappingHandlerAdapter 后,会调用该适配器的 handle 方法。

  • 4、handle 方法会调用 invokeHandlerMethod

  • 5、 invokeHandlerMethod 首先会将 26 个参数解析器,14个返回值处理器都会被封装到 ServletInvocableHandlerMethod 中。
    在这里插入图片描述

  • 6、封装好后。 invokeHandlerMethod 方法会调用invokeForRequest 方法。该方法才是真正执行对应 controller 代码的目標方法。
    在这里插入图片描述

  • 7、由于需要真正执行目标方法,那么肯定需要开始解析参数。我们点入 invokeForRequest 方法。
    在这里插入图片描述invokeForRequest 中主要有两个方法。

    • getMethodArgument 用于获取方法的参数值。该方法后,arg 结果如下:
      在这里插入图片描述
    • doInvoke 是真正使用反射来运行目标方法。
  • 8、getMethodArgumentValues 方法确定了具体的每个参数的值。getMethodArgumentValues 的具体代码如下:

    public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
        ...
    
    	@Nullable//InvocableHandlerMethod类的,ServletInvocableHandlerMethod类继承InvocableHandlerMethod类
    	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
    			Object... providedArgs) throws Exception {
    
            获取方法的参数值
    		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    
            ...
           
    		return doInvoke(args);
    	}
     
        //本节重点,获取方法的参数值
    	protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
    			Object... providedArgs) throws Exception {
    
    		MethodParameter[] parameters = getMethodParameters();
    		if (ObjectUtils.isEmpty(parameters)) {
    			return EMPTY_ARGS;
    		}
    
    		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) {
    				continue;
    			}
                //查看resolvers是否有支持
    			if (!this.resolvers.supportsParameter(parameter)) {
    				throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
    			}
    			try {
                    //支持的话就开始解析吧
    				args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
    			}
    			catch (Exception ex) {
    				....
    			}
    		}
    		return args;
    	}
        
    }
    

    可以看到,getMethodArgumentValues 遍历每一参数,判断当前参数是否存在对应的解析器(support 方法),如果存在则进行解析(resolveArgument方法)。

  • this.resolvers. 就是参数解析器(该解析类型为 HandlerMethodArgumentResolverComposite),后面的参数解析器章节会进行详解。

②、参数解析器

invokeHandlerMethod 方法中存在下面两个判断,分别为设置参数解析器设置返回值处理器
在这里插入图片描述

参数解析器概述

参数解析器 一共有26个,用来确定目标方法的参数的具体值是什么。
在这里插入图片描述
参数解析器的接口为 HandleMethodArgumentResolver ,该接口主要有下面两个方法:
在这里插入图片描述

  • support:首先该参数解析器会判断该解析器是否支持该参数的解析。
  • resolve:如果支持,就会利用该函数进行解析
源码
  • this.resolvers 的具体类别为实例 resolver 接口的HandlerMethodArgumentResolverComposite类。通过 invokeHandlerMethod 中的 invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); 进行设置。

  • 可以看到 solver 就是一个list,所有的参数解析器都会被加入到这个solvers里面

    public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
    		implements BeanFactoryAware, InitializingBean {
    	
        @Nullable
        private HandlerMethodArgumentResolverComposite argumentResolvers;
        
        @Override
        public void afterPropertiesSet() {
            ...
        	if (this.argumentResolvers == null) {//初始化argumentResolvers
            	List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
                this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
            }
            ...
        }
    
        //初始化了一堆的实现HandlerMethodArgumentResolver接口的
    	private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
    
    		// Annotation-based argument resolution
    		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    		resolvers.add(new RequestParamMapMethodArgumentResolver());
    		resolvers.add(new PathVariableMethodArgumentResolver());
    		resolvers.add(new PathVariableMapMethodArgumentResolver());
    		resolvers.add(new MatrixVariableMethodArgumentResolver());
    		resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    		resolvers.add(new ServletModelAttributeMethodProcessor(false));
    		resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    		resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    		resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
    		resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    		resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
    		resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    		resolvers.add(new SessionAttributeMethodArgumentResolver());
    		resolvers.add(new RequestAttributeMethodArgumentResolver());
    
    		// Type-based argument resolution
    		resolvers.add(new ServletRequestMethodArgumentResolver());
    		resolvers.add(new ServletResponseMethodArgumentResolver());
    		resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    		resolvers.add(new RedirectAttributesMethodArgumentResolver());
    		resolvers.add(new ModelMethodProcessor());
    		resolvers.add(new MapMethodProcessor());
    		resolvers.add(new ErrorsMethodArgumentResolver());
    		resolvers.add(new SessionStatusMethodArgumentResolver());
    		resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
    		if (KotlinDetector.isKotlinPresent()) {
    			resolvers.add(new ContinuationHandlerMethodArgumentResolver());
    		}
    
    		// Custom arguments
    		if (getCustomArgumentResolvers() != null) {
    			resolvers.addAll(getCustomArgumentResolvers());
    		}
    
    		// Catch-all
    		resolvers.add(new PrincipalMethodArgumentResolver());
    		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    		resolvers.add(new ServletModelAttributeMethodProcessor(true));
    
    		return resolvers;
    	}
        
    }
    
  • HandlerMethodArgumentResolverComposite类如下:(众多参数解析器argumentResolvers的包装类)。

    public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
    
    	private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
        
        ...
        
    	public HandlerMethodArgumentResolverComposite addResolvers(
    			@Nullable HandlerMethodArgumentResolver... resolvers) {
    
    		if (resolvers != null) {
    			Collections.addAll(this.argumentResolvers, resolvers);
    		}
    		return this;
    	}
        
        ...
    }
    
    
  • 我们看看HandlerMethodArgumentResolver的源码:

    public interface HandlerMethodArgumentResolver {
    
        //当前解析器是否支持解析这种参数
    	boolean supportsParameter(MethodParameter parameter);
    
    	@Nullable//如果支持,就调用 resolveArgument
    	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
    
    }
    
    

③、返回值处理器

返回值处理器概述

返回值处理器:一共14中返回值处理器。
在这里插入图片描述

  • this.argumentResolvers 在 afterPropertiesSet()方法内初始化
源码

代码:和解析器的添加方法类似

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
	
	@Nullable
	private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
    
	@Override
	public void afterPropertiesSet() {

        ...
        
		if (this.returnValueHandlers == null) {
			List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
			this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
		}
	}
    
    //初始化了一堆的实现HandlerMethodReturnValueHandler接口的
    private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
		List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);

		// Single-purpose return value types
		handlers.add(new ModelAndViewMethodReturnValueHandler());
		handlers.add(new ModelMethodProcessor());
		handlers.add(new ViewMethodReturnValueHandler());
		handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
				this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
		handlers.add(new StreamingResponseBodyReturnValueHandler());
		handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
				this.contentNegotiationManager, this.requestResponseBodyAdvice));
		handlers.add(new HttpHeadersReturnValueHandler());
		handlers.add(new CallableMethodReturnValueHandler());
		handlers.add(new DeferredResultMethodReturnValueHandler());
		handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));

		// Annotation-based return value types
		handlers.add(new ServletModelAttributeMethodProcessor(false));
		handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
				this.contentNegotiationManager, this.requestResponseBodyAdvice));

		// Multi-purpose return value types
		handlers.add(new ViewNameMethodReturnValueHandler());
		handlers.add(new MapMethodProcessor());

		// Custom return value types
		if (getCustomReturnValueHandlers() != null) {
			handlers.addAll(getCustomReturnValueHandlers());
		}

		// Catch-all
		if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
			handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
		}
		else {
			handlers.add(new ServletModelAttributeMethodProcessor(true));
		}

		return handlers;
	}
}

8、请求处理- Servlet参数解析器【案例1】

servlet的解析器为:ServletRequestMethodArgumentResolver
该解析器可以下面的类:WebRequest

  • ServletRequest
  • MultipartRequest
  • HttpSession
  • javax.servlet.http.PushBuilder
  • Principal
  • InputStream
  • Reader
  • HttpMethod
  • Locale
  • TimeZone
  • ZoneId

解析器的具体函数如下:

public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Nullable
	private static Class<?> pushBuilder;

	static {
		try {
			pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
					ServletRequestMethodArgumentResolver.class.getClassLoader());
		}
		catch (ClassNotFoundException ex) {
			// Servlet 4.0 PushBuilder not found - not supported for injection
			pushBuilder = null;
		}
	}


	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return (WebRequest.class.isAssignableFrom(paramType) ||
				ServletRequest.class.isAssignableFrom(paramType) ||
				MultipartRequest.class.isAssignableFrom(paramType) ||
				HttpSession.class.isAssignableFrom(paramType) ||
				(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
				(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
				InputStream.class.isAssignableFrom(paramType) ||
				Reader.class.isAssignableFrom(paramType) ||
				HttpMethod.class == paramType ||
				Locale.class == paramType ||
				TimeZone.class == paramType ||
				ZoneId.class == paramType);
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Class<?> paramType = parameter.getParameterType();

		// WebRequest / NativeWebRequest / ServletWebRequest
		if (WebRequest.class.isAssignableFrom(paramType)) {
			if (!paramType.isInstance(webRequest)) {
				throw new IllegalStateException(
						"Current request is not of type [" + paramType.getName() + "]: " + webRequest);
			}
			return webRequest;
		}

		// ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
		if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
			return resolveNativeRequest(webRequest, paramType);
		}

		// HttpServletRequest required for all further argument types
		return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
	}

	private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
		T nativeRequest = webRequest.getNativeRequest(requiredType);
		if (nativeRequest == null) {
			throw new IllegalStateException(
					"Current request is not of type [" + requiredType.getName() + "]: " + webRequest);
		}
		return nativeRequest;
	}

	@Nullable
	private Object resolveArgument(Class<?> paramType, HttpServletRequest request) throws IOException {
		if (HttpSession.class.isAssignableFrom(paramType)) {
			HttpSession session = request.getSession();
			if (session != null && !paramType.isInstance(session)) {
				throw new IllegalStateException(
						"Current session is not of type [" + paramType.getName() + "]: " + session);
			}
			return session;
		}
		else if (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) {
			return PushBuilderDelegate.resolvePushBuilder(request, paramType);
		}
		else if (InputStream.class.isAssignableFrom(paramType)) {
			InputStream inputStream = request.getInputStream();
			if (inputStream != null && !paramType.isInstance(inputStream)) {
				throw new IllegalStateException(
						"Request input stream is not of type [" + paramType.getName() + "]: " + inputStream);
			}
			return inputStream;
		}
		else if (Reader.class.isAssignableFrom(paramType)) {
			Reader reader = request.getReader();
			if (reader != null && !paramType.isInstance(reader)) {
				throw new IllegalStateException(
						"Request body reader is not of type [" + paramType.getName() + "]: " + reader);
			}
			return reader;
		}
		else if (Principal.class.isAssignableFrom(paramType)) {
			Principal userPrincipal = request.getUserPrincipal();
			if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
				throw new IllegalStateException(
						"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
			}
			return userPrincipal;
		}
		else if (HttpMethod.class == paramType) {
			return HttpMethod.resolve(request.getMethod());
		}
		else if (Locale.class == paramType) {
			return RequestContextUtils.getLocale(request);
		}
		else if (TimeZone.class == paramType) {
			TimeZone timeZone = RequestContextUtils.getTimeZone(request);
			return (timeZone != null ? timeZone : TimeZone.getDefault());
		}
		else if (ZoneId.class == paramType) {
			TimeZone timeZone = RequestContextUtils.getTimeZone(request);
			return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
		}

		// Should never happen...
		throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName());
	}


	/**
	 * Inner class to avoid a hard dependency on Servlet API 4.0 at runtime.
	 */
	private static class PushBuilderDelegate {

		@Nullable
		public static Object resolvePushBuilder(HttpServletRequest request, Class<?> paramType) {
			PushBuilder pushBuilder = request.newPushBuilder();
			if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
				throw new IllegalStateException(
						"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
			}
			return pushBuilder;

		}
	}
}

9、请求处理- Model、Map原理【案例2】

对于一些复杂的参数:

  • Map

  • Model(map、model里面的数据会被放在request的请求域 request.setAttribute)

  • Errors/BindingResult

  • RedirectAttributes( 重定向携带数据)

  • ServletResponse(response)

  • SessionStatus

  • UriComponentsBuilder

  • ServletUriComponentsBuilder

  • 无论是 model 还是 map 都使用的是 名为 modelMethodProcessor 解析器作为参数解析器。换句话说,只有这个解析器支持解析他们。

  • 这个解析器底层使用的也就是 setAttribute方法。如下:
    在这里插入图片描述

10、响应处理-ReturnValueHandler-【案例1】

对应的返回处理器的寻找方式和参数解析器类似

  • invokeAndHandle 函数中,在运行完所有controller后,数据会被返回到一个 Object 对象中 ,如下:

    public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
        
    	public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
    			Object... providedArgs) throws Exception {
    
    		Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    		
            ...
            
    		try {
                //看下块代码
                //这段代码就是用于将返回的 Object 包装到对应的返回类型解析器中
    			this.returnValueHandlers.handleReturnValue(
    					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    		}
    		catch (Exception ex) {
    			...
    		}
    	}
    
    

以 ResponseBody 注解为例

  • 该注解使用的返回值解析器为RequestResponseBodyMethodProcessor,它实现HandlerMethodReturnValueHandler接口
  • 代码如下:
    public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    
        ...
        
    	@Override
    	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
    			ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
    			throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    
    		mavContainer.setRequestHandled(true);
    		ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    		ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
    
            // 使用消息转换器进行写出操作,本方法下一章节介绍:
    		// Try even with null return value. ResponseBodyAdvice could get involved.
    		writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    	}
    
    }
    
    
    

11、响应处理-视图解析器-【案例2】

ContentNegotiatingViewResolver

视图解析器为 ContentNegotiatingViewResolver 该解析器可以根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。

  • 我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法:

    @Bean
    @ConditionalOnBean(ViewResolver.class)
    @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
    public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
        ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
        resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
        // ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
        resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return resolver;
    }
    
    
  • 进入 ContentNegotiatingViewResolver 类,我们可以看到一个视图解析的方法(该方法的目的是希望通过viewName 和location 找到对应的view):

    @Nullable // 注解说明:@Nullable 即参数可为null
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
        List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
        if (requestedMediaTypes != null) {
            // 获取候选的视图对象
            List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
            // 选择一个最适合的视图对象,然后把这个对象返回
            View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
            if (bestView != null) {
                return bestView;
            }
        }
        // .....
    }
    
  • 我们点击 getCandidateViews,观察他是怎么获得候选的视图的呢?
    getCandidateViews中看到他得到了存储所有视图解析器的集合的迭代器,然后进行while循环,挨个解析!
    在这里插入图片描述

    所以得出结论:ContentNegotiatingViewResolver 这个视图解析器(viewResolvers)就是用来组合所有的视图解析器的

  • 我们再去研究下他的组合逻辑,看到有个属性viewResolvers,看看它是在哪里进行赋值的!

    protected void initServletContext(ServletContext servletContext) {
    	//    这里它是从beanFactory工具中获取容器中的所有视图解析器
        // ViewRescolver.class 把所有的视图解析器来组合的
        Collection<ViewResolver> matchingBeans = 
        							BeanFactoryUtils.beansOfTypeIncludingAncestors(
        							this.obtainApplicationContext(),ViewResolver.class).
        							values();
        ViewResolver viewResolver;
        if (this.viewResolvers == null) {
            this.viewResolvers = new ArrayList(matchingBeans.size());
            Iterator var3 = matchingBeans.iterator();
    
            while(var3.hasNext()) {
                viewResolver = (ViewResolver)var3.next();
                if (this != viewResolver) {
                    this.viewResolvers.add(viewResolver);
                }
            }
        } else {
            for(int i = 0; i < this.viewResolvers.size(); ++i) {
                viewResolver = (ViewResolver)this.viewResolvers.get(i);
                if (!matchingBeans.contains(viewResolver)) {
                    String name = viewResolver.getClass().getName() + i;
                    this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
                }
            }
        }
    
        AnnotationAwareOrderComparator.sort(this.viewResolvers);
        this.cnmFactoryBean.setServletContext(servletContext);
    }
    

    可以看到,viewResolvers 初始化的逻辑为:

    • 从 BeanFactoryUtils.beansOfTypeIncludingAncestors 方法中获取ViewResolver 列表,该方法是遍历了整个spring容器,来获得了所有的 ViewResolver.class 类文件
    • 来获取ViewResolver列表后,赋值给viewResolvers;

通过上面的溯源,我们可以发现 ContentNegotiatingViewResolver会遍历整个spring容器,获得所有解析器。然后从这些解析器中返回需要的。因此我们可以自定义解析器,只要继承了 viewResolve 就会被自动配置。

自定义 视图解析器

  • 1、我们在我们的主程序中去写一个视图解析器来试试;

    // 如果 想DIY一些定制化的功能,只要写这个组件,然后将它交给springboot,springboot就会帮我们自动装配
    //扩展springmvc
    @Configuration
    public class MyMvcConfig implements WebMvcConfigurer {
    
        //ViewResolver 实现类视图解析器接口的类,我们就可以把它看做视图解析器
        @Bean
        public ViewResolver myViewResolver(){
            return new MyViewResolver();
        }
        //我们写一个静态内部类,视图解析器就需要实现ViewResolver接口
        public static class MyViewResolver implements ViewResolver {
            @Override
            public View resolveViewName(String s, Locale locale) throws Exception {
                return null;
            }
        }
    }
    
    
  • 2、怎么看我们自己写的视图解析器有没有起作用呢?
    我们给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中
    在这里插入图片描述

  • 3、我们启动我们的项目,然后随便访问一个页面,看一下Debug信息;找到this
    在这里插入图片描述

  • 4、找到视图解析器,我们看到我们自己定义的就在这里了;
    在这里插入图片描述

12、更改转换器和格式化器

在springboot项目里面,我们导入web的依赖,就有了springmvc,里面有人家已经封装的springmvc的配置,现在我们想要自定义自己的配置,比如格式化转换器。

日期的格式,比如2020-2-2 和 2020/2/2
我们使用哪个?源码里面默认的是哪个?

  • 既然要看源码,那么找spring.factoryies 文件,找到关于springmvc的类:
    在这里插入图片描述
  • 可以看到 其中有一个 mvcProperties 的变量。找到定义位置:
    在这里插入图片描述
  • 点击进去
    在这里插入图片描述
  • 这样就看到关于springmvc里面的配置了。我们在这个里面找和格式化相关的配置
    在这里插入图片描述
    可以看到默认就是 dd/MM/yyyy
  • 我们可以直接通过yml 重新配置date格式如下:
    在这里插入图片描述
  • 同样,我们也可以使用 properties 来完成这件事:
    在这里插入图片描述

添加额外的MVC配置

这么多的自动配置,原理都是一样的,通过这个 WebMVC 的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。

SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;

  • SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;
  • 如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!

官方文档如下:

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

我们要做的就是编写一个@Configuration注解类,并且 类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解; 我们去自己写一个;我们新建一个包叫config,写一个类MyMvcConfig;

//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器发送/test , 就会跳转到test页面;
        registry.addViewController("/test").setViewName("test");
    }
}

我们去浏览器访问一下:
在这里插入图片描述

我们可以去分析一下原理:

  • 1、WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter

  • 2、这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)

  • 3、我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration
    这个父类中有这样一段代码:

    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
        private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
        
      // 从容器中获取所有的webmvcConfigurer
        @Autowired(required = false)
        public void setConfigurers(List<WebMvcConfigurer> configurers) {
            if (!CollectionUtils.isEmpty(configurers)) {
                this.configurers.addWebMvcConfigurers(configurers);
            }
        }
    }
    
  • 4、我们可以在这个类中去寻找一个我们刚才设置的viewController 当做参考,发现它调用了一个

    protected void addViewControllers(ViewControllerRegistry registry) {
        this.configurers.addViewControllers(registry);
    }
    
    
  • 5、我们点进去看一下

    public void addViewControllers(ViewControllerRegistry registry) {
        Iterator var2 = this.delegates.iterator();
    
        while(var2.hasNext()) {
            // 将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
            WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
            delegate.addViewControllers(registry);
        }
    }
    
    

所以得出结论: 所有的 WebMvcConfiguration 都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用;

13、 全面接管SpringMVC

官方文档:

If you want to take complete control of Spring MVCyou can add your own @Configuration annotated with @EnableWebMvc.

全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!

只需在我们的配置类中要加一个@EnableWebMvc

  • 我们看下如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效,我们可以去测试一下;

不加注解之前,访问首页:
在这里插入图片描述
给配置类加上注解:@EnableWebMvc
在这里插入图片描述
我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;

源码分析:
1、这里发现它是导入了一个类,我们可以继续进去看

@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

2、它继承了一个父类 WebMvcConfigurationSupport

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
  // ......
}

3、我们来回顾一下Webmvc自动配置类

@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 })
public class WebMvcAutoConfiguration {
    
}

总结: 由于 自动配置类中,有个条件注解,只有没有bean 时才会注入 WebMvcConfigurationSupport。但是如果此时,添加了注解 @EnableWebMvc 相当于增加了 bean。那么自动配置里面就不会去加载那些扩展类。

关于网站图标说明:
在这里插入图片描述

与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。

  • 1、关闭SpringBoot默认图标

    #关闭默认图标
    spring.mvc.favicon.enabled=false
    
  • 2、自己放一个图标在静态资源目录下,我放在 public 目录下

  • 3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值