【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发

一、配置文件

1、什么是 yaml ?

  • YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。

  • 适用于以数据为中心的配置文件

2、基本语法

  • 普通的 key: value, kv之间有一个空格
  • 区分大小写
  • 使用缩进表示层级关系
    • 缩进不用tab
    • 只允许用空格
    • 空格数不重要,只要相同层级的元素左对齐即可
  • 用 # 表示注释
  • 字符串无需加引号,如果要加分为两种情况
    • 单引号不会转义,会将 \n 作为字符串输出
    • 双引号会转义,会将 \n作为换行输出

3、针对每类数据类型的语法

(1)字面量:单个的、不可再分的值。 date、boolean、string、number、null

k: v

(2)对象:键值对的集合。map、hash、set、object

# 第一种写法
k: {k1:v1,k2:v2,k3:v3}
# 第二种写法
k:
    k1: v1
    k2: v2
    k3: v3

(3)数组:一组按照次序排列的值。 array、list、queue

# 第一种写法
k: [v1,v2,v3]
# 第二中写法
k:
   - v1
   - v2
   - v3

我们通过一个案例来演示,具体在yaml文件中如何对属性值进行配置

  • 实体类
package com.atguigu.boot.bean;

import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Bonbons
 * @version 1.0
 * 演示各种数据类型对应的yaml语法编写
 */
@Data
@Component
@ToString
@ConfigurationProperties(prefix = "person")
public class Person {
    private String userName;
    private Boolean boss;
    private Date birth;
    private Integer age;
    private Pet pet;
    private String [] interest;
    private List<String> animal;
    private Map<String, Object> score;
    private Set<Double> salarys;
    private Map<String, List<Pet>> allPets;
}

@Data
@Component
@ToString
class Pet{
    private String name;
    private Double weight;
}
  • 编写 application.yml
person:
  userName: "JiMei"
  boss: true
  birth: 2002/10/10
  age: 21
  interest: ['唱', '跳', 'Rap', '篮球']
  pet:
    name: "JiGe"
    weight: 20
  animal:
    - '小猫'
    - '小狗'
  score: {english:99,history:82}
  salarys:
    - 12000.0
    - 6000.0
  allPets:
    sick:
      - {name:阿芙,weight:10}
      - name: 赖皮蛇
        weight: 88
    health: [{name:诺手,weight:99}, {name:双面龟,weight:999}]
  • 可以看到我在上面用到了 @ConfigurationProperties注解,因为也对 application.properties 进行了配置

    server.port=8888
    person.userName=Xxx
    
  • 在附上我们的主程序

package com.atguigu.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Bonbons
 * @version 1.0
 */
@SpringBootApplication
public class HelloWorldApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloWorldApplication.class, args);

        System.out.println("项目启动成功!");
    }
}

运行结果如下

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第1张图片

  • 可以发现在 .properties、.yml 中的配置全都生效了,优先选择的properties中设定的配置
  • 这两个文件都是作为全局配置文件使用的

4、如何开启配置提示呢?

  • 就是我们在编写 yml 文件的时候,会根据我们打出的部分提示可能输入的内容

  • 我们可以导入一个依赖来完成这个功能

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-configuration-processor</artifactId>
    	<optional>true</optional>
    </dependency>
    
  • 还有一个打包插件:

	<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

二、Web开发

2.1 简单功能分析

1、如何访问到静态资源?

  • SpringBoot 为我们提供了两种方式来访问静态资源:
    • 把所有的静态资源放在指定的文件夹下,直接通过根路径就可以访问的到
    • 我们可以自定义静态资源文件夹

resources 的六个子目录,出了vue是我自定义的静态资源文件夹,其他几个都是默认的

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第2张图片
查找静态资源的优先级: 自定义 > /META-INF/resources/ > resources > static > public

  • 自定义静态资源的路径,需要在 application.yml 文件中进行配置【多个路径用逗号分隔】
spring:
	resources:
      static-locations: [classpath:/vue/]
  • 请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面

    • 也就是说: 如果控制器配置了与静态资源同名的请求处理,那么请求就会被控制器拦截
  • 使用了自定义的静态资源路径后,原来默认的静态资源路径仍会生效

  • 下面为默认路径在源码中的相关体现:

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {

	private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
			"classpath:/resources/", "classpath:/static/", "classpath:/public/" };

	/**
	 * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
	 * /resources/, /static/, /public/].
	 */
	private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

2、如果一个项目中有很多静态资源,我们如何区分调用它们呢?

  • 我们可以为静态资源设置访问前缀

    spring:
    	mvc:
    		static-path-pattern: /res/**
    
  • 之后我们想要在浏览器发送请求访问我们的静态资源,就要采取 当前项目 + static-path-pattern + 静态资源名 的方式
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第3张图片

  • 如果觉得加上前缀很麻烦,我们可以使用 webjar 来解决:

    • 我们需要导入对应的依赖
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.1</version>
        </dependency>
    
    • 对于webjar依赖中的静态资源,会被映射处理成两种请求路径:/webjars/** /static-path-pattern属性值/webjars/**
      • 所以访问webjars中的静态资源时,静态请求前缀可加可不加

3、SpringBoot 支持欢迎页特性

  • 需要我们将欢迎页 index 放到静态资源路径下

  • 可以配置额外的静态资源路径,但是不能设置访问前缀,否则我们只能通过 Controller 来处理 /index 请求 【底层源码是这样解释的】

    WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
            if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
                logger.info("Adding welcome page: " + welcomePage.get());
                this.setRootViewName("forward:index.html");
            } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
                logger.info("Adding welcome page template: index");
                this.setRootViewName("index");
            }
        }
    
  • 我们可以采取下面的方案,来实现添加前缀也能找到Welcome页 【在配置文件中进行配置】

在这里插入图片描述

  • 欢迎页的处理规则
	HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。	

	@Bean
		public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
				FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
			WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
					new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
					this.mvcProperties.getStaticPathPattern());
			welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
			welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
			return welcomePageHandlerMapping;
		}

	WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
			ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
		if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
            //要用欢迎页功能,必须是/**
			logger.info("Adding welcome page: " + welcomePage.get());
			setRootViewName("forward:index.html");
		}
		else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
            // 调用Controller  /index
			logger.info("Adding welcome page template: index");
			setRootViewName("index");
		}
	}

4、我们还可以自定义网站图标

  • 只需要把图标文件 favicon.ico 放在静态资源目录下即可
  • 需要注意:浏览器的session没关,就是同一次会话,浏览器的访问图标修改后也不会变
    • 我们可以更换一个浏览器进行访问
    • 打开浏览器开发者工具,禁用缓存后反复刷新 【不要关闭开发者工具这个页面】
      【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第4张图片

5、静态资源的配置原理

  • SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
  • SpringMVC功能的自动配置类 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 {}
  • 那么给容器中配置了什么呢?
	@Configuration(proxyBeanMethods = false)
	@Import(EnableWebMvcConfiguration.class)
	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
  • 配置文件的相关属性和xxx进行了绑定。WebMvcProperties==spring.mvc、ResourceProperties==spring.resources

  • 在配置类中只有一个有参构造器

	//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath  
//ServletRegistrationBean   给应用注册Servlet、Filter....
	public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
				ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
				ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
				ObjectProvider<DispatcherServletPath> dispatcherServletPath,
				ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
			this.resourceProperties = resourceProperties;
			this.mvcProperties = mvcProperties;
			this.beanFactory = beanFactory;
			this.messageConvertersProvider = messageConvertersProvider;
			this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
			this.dispatcherServletPath = dispatcherServletPath;
			this.servletRegistrations = servletRegistrations;
		}

2.2 请求参数处理

2.2.1 请求映射

1、rest(一种编程风格)的使用和原理

  • REST 风格支持 使用HTTP请求方式动词来表示对资源的操作

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
  • 正常不支持直接将method设置为 put 和 delete 的表单请求,会默认改为get请求

    • 不过我们可以先将表单设置为post请求,再使用一个隐藏参数_method更换为这两个请求
    • 手动开启 restful 风格
      hidden-method: filter: enabled: true

(1) 编写Mapping映射

@RequestMapping(value = "/user",method = RequestMethod.GET)
    public String getUser(){
        return "GET-张三";
    }

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


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

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

(2) 在欢迎页里编写表单


<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="put">
    <input value="REST-PUT 提交" type="submit" />
form>
<form action="/user" method="post">
    <input name="_zbc" type="hidden" value="delete">
    <input name="_method" type="hidden" value="delete">
      <input value="REST-DELETE 提交" type="submit" />
form>

(3)在 application.yml 中开启restful风格的设定

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第5张图片

  • 经过测试发送的几个请求都达到了预期效果
  • 我们可以在控制器的映射注解中去掉 method,将注解进行替换
    • @GetMapping、@PostMapping、@PutMapping、@DeleteMapping
  • 可以看到我的post表单还有一个隐藏的属性: _zbc
    • 通过创建一个 HiddenHttpMethodFilter 组件,实现将_method 修改为我们指定的内容
package com.atguigu.boot.config;

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

/**
 * @author Bonbons
 * @version 1.0
 */
@Configuration
public class WebConfig {

    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        //修改_method为_zbc
        hiddenHttpMethodFilter.setMethodParam("_zbc");
        return hiddenHttpMethodFilter;
    }
}

2、在使用Rest完成表单提交的原理 【只用于表单提交】

  • 表单提交会带上 _method =PUT/DELETE
  • 请求过来被HiddenHttpMethodFilter拦截
    • 先检查 请求是否正常,并且是POST
    • 满足上面的条件再获取到_method的值 【_method 的值可以是 PUT.DELETE.PATCH】
    • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值
    • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的

需要注意:上面的这个应用原理是针对表单请求而言的,而且在SpringBoot中是通过配置选择开启的

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true   #开启页面表单的Rest功能

3、我们需要了解请求映射的原理

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第6张图片

  • 这是一个向下继承,层级调用的体现
  • 每个请求都被 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

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第7张图片

  • RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第8张图片

  • 所有的请求映射都在HandlerMapping中。

    • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
    • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
    • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
      • 如果有就找到这个请求对应的handler
      • 如果没有就是下一个 HandlerMapping
    • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
	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;
	}

2.2.2 普通参数与基本注解

  • 在此部分我将介绍一些底层常用注解,会演示如何使用以及简单的对原理试图理解

1、先介绍@PathVariable、@RequestHeader、@RequestAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

  • @PathVariable 代表路径变量,使用的形式 @PathVariable("name") String name,用{}代表路径
  • @RequestHeader 代表请求头信息,我们可以获取指定的请求头,也可以获取全部的请求头
  • @RequestAttribute 代表请求域中的数据,我们可以向请求域添加数据,也可以取数据
  • @RequestParam 代表请求参数,我们可以获取到指定的请求参数和全部的请求参数
  • @MatrixVariable 代表矩阵变量,在SpringBoot中默认是禁用的,需要我们进行配置,是一种用分号相隔的请求
  • @CookieValue 代表Cookie,我们可以获取指定的cookie
  • @RequestBody 代表请求体,我们可以获取POST请求的请求体

接下来我们通过具体的代码进行演示

  • (1)直接给出了完整的用于发起请求的测试页面: annotation.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试基本注解</title>
</head>
<body>
<!--用户发起GET请求-->
<a href="/car/3/owner/zhangsan?age=18&inters=basket&inters=game">点击发起测试</a>
<!--&age=18&inters=basket&inters=game-->
<!--获取请求的请求体,因为只有post请求才有请求体,所以我们设计一个表单-->
<form action="/save" method="post">
    测试RequestBody获取数据 <br/>
    用户名 <input name="username" /><br>
    邮箱 <input name="email" />
    <input type="submit" value="提交">
</form>

<!--测试矩阵变量-->
<a href="/cars/sell;low=32;brand=biyadi,benchi">一个属性对应多个值,采用逗号隔开</a> <br>
<a href="/cars/sell;low=21;brand=aodi;brand=laosilaisi">一个属性对应多个值,采用k=v分号隔开</a> <br>
<a href="/boss/1;age=38/2;age=22">绑定多个路径,存在相同属性</a>
</body>
</html>
  • (2)接下来是我们用来处理请求的控制器方法:ParameterTestController.java
package com.atguigu.boot.controller;

import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Bonbons
 * @version 1.0
 *
 */
@RestController
public class ParameterTestController {
	//结合表单发起POST请求,测试@RequestBody获取请求体
    @PostMapping("/save")
    public Map postMethod(@RequestBody String context){
        Map<String, Object> map = new HashMap<>();
        map.put("context", context);
        return map;
    }
	//测试了路径变量、请求体、请求参数、Cookie值
   @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){
        //这里直接运行会产生保存,原因:我没有 _ga 这个@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;
    }

	//测试的是矩阵变量
    // 测试/cars/sell;low=31;brand=biyadi,benchi
    @GetMapping("/cars/{path}")
    public Map carsSell(@MatrixVariable("low") Integer low,
                        @MatrixVariable("brand") List<Object> 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;
    }

    //针对不同路径有相同属性名的演示
    @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;
    }

}
  • 因为矩阵变量默认是禁用的,所以我们需要进行一些配置才能使用矩阵变量的特性
    • 方案一:在我们的配置类中利用@Bean注解,将WebMvcConfigurer注册到容器中
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                UrlPathHelper urlPathHelper = new UrlPathHelper();
                //关闭自动移除分号后内容 -- 开启矩阵变量
                urlPathHelper.setRemoveSemicolonContent(false);
                configurer.setUrlPathHelper(urlPathHelper);
            }
        };
    }
    
    • 方案二:我们让配置类实现WebMvcConfigurer接口,然后重写它的 configurerPathMatch方法
    @Configuration
    public class WebConfig implements WebMvcConfigurer{
    	@Override
    	public void configurePathMatch(PathMatchConfigurer configurer) {
    		UrlPathHelper urlPathHelper = new UrlPathHelper();
    		urlPathHelper.setRemoveSemicolonContent(false);
    		configurer.setUrlPathHelper(urlPathHelper);
    	}
    }
    

2、补充几个知识点

  • @RestController注解声明控制器时,代表将控制器方法的返回值当做数据响应浏览器

    • 也就是我们想让控制器方法的返回值作为请求转发,那么只能用普通的 @Controller 声明控制器
  • 当浏览器的Cookie被禁用,我们如何获取请求数据呢?

    • 通过url重写,可以解决cookie禁用的问题,把cookie的值通过矩阵变量的方式进行传递

2.2.3 ServletAPI

  • WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
@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) ||
				InputStream.class.isAssignableFrom(paramType) ||
				Reader.class.isAssignableFrom(paramType) ||
				HttpMethod.class == paramType ||
				Locale.class == paramType ||
				TimeZone.class == paramType ||
				ZoneId.class == paramType);
	}

2.2.4 复杂参数

  • 这部分说的主要是在控制器方法的参数
  • Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

编写一个两个控制器方法进行测试:

//我们要测试Map、Model、HttpServletRequest、HttpServletResponse 作为参数的效果
    @GetMapping("/params")
    public String testParams(Map<String, Object> map,
                             Model model,
                             HttpServletRequest request,
                             HttpServletResponse response){

        //对于Map、Model、HttpServletRequest 都可以向请求域中添加数据
        map.put("hello", "world");
        model.addAttribute("world", "hello");
        request.setAttribute("message", "HelloWorld");
        //创建一个Cookie
        Cookie cookie = new Cookie("c1", "v1");
        response.addCookie(cookie);
        return "forward:/success";
    }
    @ResponseBody
    @GetMapping("/success")
    public Map success(@RequestAttribute(name = "msg", required = false) String msg,
                       @RequestAttribute(name = "code", required = false) Integer code,
                       HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        //尝试获取我们添加到请求域中的数据
        Object hello = request.getAttribute("hello");
        Object world = request.getAttribute("world");
        Object message = request.getAttribute("message");
        map.put("hello", hello);
        map.put("world", world);
        map.put("message", message);
        return map;
    }
  • 将Map、Model作为参数,相当于调用了RequestAttribute,将参数返回值添加到请求域中

  • 请求域参数不是必须的 required = false;

  • 为什么 Model 和 Map都是对同一个请求域进行操作呢?

    • Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map
    • mavContainer.getModel(); 获取到值的
      【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第9张图片
    • 通过下面这张图片更可以看出操作的事同一个对象
      【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第10张图片
      【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第11张图片

2.2.5 自定义参数

  • SpringBoot通过自定义对象参数,可以实现自动类型转换与格式化,并可以级联封装
    • 网页携带的数据如果满足某个pojo类,那么就会自动封装成这个pojo的对象

(1) 我们设计的POJO类

package com.atguigu.boot.bean;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Date;
import javax.xml.crypto.Data;

/**
 * @author Bonbons
 * @version 1.0
 */
@lombok.Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private String name;
    private Integer age;
    private Date birth;
    private Pet pet;
}
package com.atguigu.boot.bean;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * @author Bonbons
 * @version 1.0
 */
@lombok.Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Pet {
    private String name;
    private Integer age;
}

(2) 发起请求的前端表单

<!--自定义对象函数测试-->
<form action="/savePerson" method="post">
    姓名:<input name="userName" value="zhangsan"> <br>
    年龄:<input name="age" value="18" />
    生日:<input name="birth" value= "2002-06-01" />
    宠物姓名:<input name="pet.name" value="小花">
    宠物年龄:<input name="pet.age" value="2">
    <input type="submit" value="保存" />
</form>

(3) Controller

@PostMapping("/savePerson")
public Person savePerson(Person person){
	return person;
}

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第12张图片

  • SpringBoot 之所以可以自动获取表单值封装为指定类型对象,是因为SpringBoot 具有严密的参数解析机制, 但是若我们的输入值SpringBoot 不能解析时,那么应该如何处理呢?

    <form action="/savePerson" method="post">
        姓名:<input name="userName" value="zhangsan"> <br>
        年龄:<input name="age" value="18" />
        生日:<input name="birth" value= "2002-06-01" />
        宠物:<input name="pet.name" value="小花,2">
        <input type="submit" value="保存" />
    </form>
    
  • 我们可以使用自定义参数封装POJO

    • 编写WebConfig类实现WebMvcConfigurer类,重写 addFormatters 方法
      package com.example.demo2.config;
      import com.example.demo2.bean.Pet;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.core.convert.converter.Converter;
      import org.springframework.format.FormatterRegistry;
      import org.springframework.util.StringUtils;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      @Configuration
      public class WebConfig implements WebMvcConfigurer{
          //1、WebMvcConfigurer定制化SpringMVC的功能
          @Bean
          public WebMvcConfigurer webMvcConfigurer(){
              return new WebMvcConfigurer() {
                  @Override
                  public void addFormatters(FormatterRegistry registry) {
                      registry.addConverter(new Converter<String, Pet>() {
                          @Override
                          public Pet convert(String source) {
                              if(!StringUtils.isEmpty(source)){
                                  Pet pet = new Pet();
                                  String[] split = source.split(",");
                                  pet.setName(split[0]);
                                  pet.setAge(split[1]);
                                  return pet;
                              }
                              return null;
                          }
                      });
                  }
              };
          }
      }
      

2.3 参数处理原理

  • HandlerMapping中找到能处理请求的Handler(Controller.method())
  • 为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
  • 适配器执行目标方法并确定方法参数的每一个值

1、HandlerAdapter

  • 为能处理请求的控制器方法找到一个适配器
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第13张图片
  • 0 - 支持方法上标注@RequestMapping
  • 1 - 支持函数式编程的

2、执行目标方法

  • 在源码中执行目标方法的核心语句是下面这条:

    // Actually invoke the handler.
    //DispatcherServlet -- doDispatch
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
  • mav 是ModelAndView的对象

    mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法
    //ServletInvocableHandlerMethod
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    //获取方法的参数值
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    

3、参数解析器 HandlerMethodArgumentResolver

  • 要执行目标方法之前,我们需要知道这个方法有哪些参数,它们的值是什么
  • SpringMVC目标方法能写多少种参数类型,就取决于参数解析器
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第14张图片
  • 底层通过调用resolveArgument判断,这个方法是否支持当前的解析器解析其中的参数
    • 支持就说明有对应的参数,反之则无
      【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第15张图片

4、返回值处理器

  • 通过下面这张图我们可以大概了解一下都会返回什么

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第16张图片

5、如何确定目标方法每一个参数的值

  • 在源码中是这样描述的
============InvocableHandlerMethod==========================
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;
			}
			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) {
				// Leave stack trace for later, exception may actually be resolved and handled...
				if (logger.isDebugEnabled()) {
					String exMsg = ex.getMessage();
					if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
						logger.debug(formatArgumentError(parameter, exMsg));
					}
				}
				throw ex;
			}
		}
		return args;
	}
  • 挨个判断所有解析器是否支持解析这个参数
	@Nullable
	private HandlerMethodArgumentResolver getArgumentResolver(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;
	}
  • 然后调用各自 HandlerMethodArgumentResolverresolveArgument 方法即可
  • 对于自定义类型参数,会封装成 POJO
public static boolean isSimpleValueType(Class<?> type) {
		return (Void.class != type && void.class != type &&
				(ClassUtils.isPrimitiveOrWrapper(type) ||
				Enum.class.isAssignableFrom(type) ||
				CharSequence.class.isAssignableFrom(type) ||
				Number.class.isAssignableFrom(type) ||
				Date.class.isAssignableFrom(type) ||
				Temporal.class.isAssignableFrom(type) ||
				URI.class == type ||
				URL.class == type ||
				Locale.class == type ||
				Class.class == type));
	}
@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
		Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");

		String name = ModelFactory.getNameForParameter(parameter);
		ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
		if (ann != null) {
			mavContainer.setBinding(name, ann.binding());
		}

		Object attribute = null;
		BindingResult bindingResult = null;

		if (mavContainer.containsAttribute(name)) {
			attribute = mavContainer.getModel().get(name);
		}
		else {
			// Create attribute instance
			try {
				attribute = createAttribute(name, parameter, binderFactory, webRequest);
			}
			catch (BindException ex) {
				if (isBindExceptionRequired(parameter)) {
					// No BindingResult parameter -> fail with BindException
					throw ex;
				}
				// Otherwise, expose null/empty value and associated BindingResult
				if (parameter.getParameterType() == Optional.class) {
					attribute = Optional.empty();
				}
				bindingResult = ex.getBindingResult();
			}
		}

		if (bindingResult == null) {
			// Bean property binding and validation;
			// skipped in case of binding failure on construction.
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
					bindRequestParameters(binder, webRequest);
				}
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
			// Value type adaptation, also covering java.util.Optional
			if (!parameter.getParameterType().isInstance(attribute)) {
				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), 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;
	}
  • WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

  • WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面

  • WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中

  • GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
    byte – > file

  • @FunctionalInterfacepublic interface Converter

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第17张图片
【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第18张图片

  • 未来我们可以给WebDataBinder里面放自己的Converter;
    • private static final class StringToNumber implements Converter
  • 自定义 Converter
    //1、WebMvcConfigurer定制化SpringMVC的功能
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                UrlPathHelper urlPathHelper = new UrlPathHelper();
                // 不移除;后面的内容。矩阵变量功能就可以生效
                urlPathHelper.setRemoveSemicolonContent(false);
                configurer.setUrlPathHelper(urlPathHelper);
            }

            @Override
            public void addFormatters(FormatterRegistry registry) {
                registry.addConverter(new Converter<String, Pet>() {

                    @Override
                    public Pet convert(String source) {
                        // 啊猫,3
                        if(!StringUtils.isEmpty(source)){
                            Pet pet = new Pet();
                            String[] split = source.split(",");
                            pet.setName(split[0]);
                            pet.setAge(Integer.parseInt(split[1]));
                            return pet;
                        }
                        return null;
                    }
                });
            }
        };
    }

6、在目标方法执行完成后

  • 将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第19张图片
    7、最后派发处理结果

  • processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

  • renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

InternalResourceView@Override
	protected void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		// Expose the model object as request attributes.
		exposeModelAsRequestAttributes(model, request);

		// Expose helpers as request attributes, if any.
		exposeHelpers(request);

		// Determine the path for the request dispatcher.
		String dispatcherPath = prepareForRendering(request, response);

		// Obtain a RequestDispatcher for the target resource (typically a JSP).
		RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
		if (rd == null) {
			throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
					"]: Check that the corresponding file exists within your web application archive!");
		}

		// If already included or response already committed, perform include, else forward.
		if (useInclude(request, response)) {
			response.setContentType(getContentType());
			if (logger.isDebugEnabled()) {
				logger.debug("Including [" + getUrl() + "]");
			}
			rd.include(request, response);
		}

		else {
			// Note: The forwarded resource is supposed to determine the content type itself.
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to [" + getUrl() + "]");
			}
			rd.forward(request, response);
		}
	}
暴露模型作为请求域属性
// Expose the model object as request attributes.
		exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

    //model中的所有数据遍历挨个放在请求域中
		model.forEach((name, value) -> {
			if (value != null) {
				request.setAttribute(name, value);
			}
			else {
				request.removeAttribute(name);
			}
		});
	}

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第20张图片

2.3、数据响应与内容协商

  • 响应数据用于开发一些前后端分离的项目

1、我们如何实现让浏览器响应JSON数据呢?

  • 我们需要引入 web-start 场景,因为里面包含了json的开发场景
    • json开发场景引入相关依赖 jackson
    • 下面是web场景的依赖:
      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      
    • 下面是Json场景的依赖:
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
        <version>2.3.4.RELEASE</version>
        <scope>compile</scope>
      </dependency>
      
    • 下面是JSON场景里面关联到的JSON依赖(部分)
      【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第21张图片
  • 完成环境依赖导入后,我们需要在控制器方法上添加 @ResponseBody 注解
    • 将方法的返回值,以特定的格式写入到responsebody区域,进而将数据返回给客户端

2、引出返回值解析器的概念:

  • SpringMVC的底层,对于返回值的解析也是采用返回值解析器
  • 等待返回值返回后进行处理
  • 返回值解析器被放到 invocableMethod 可执行方法里 【返回值解析器总共有15个】
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第22张图片
    • 调用下面这个方法进行请求处理
      请添加图片描述
    • 利用doInvoke反射执行目标方法,得到了返回值 【我们指定返回的person对象】
      请添加图片描述
    • 有返回值,并且返回的不是一个字符串,调用handleReturnValue下面的这个处理返回值
      try {
      			this.returnValueHandlers.handleReturnValue(
      					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
      		}
      
      	@Override
      	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
      
      		HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
      		if (handler == null) {
      			throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
      		}
      		handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
      	}
      

3、那么具体是怎么处理的呢?

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第23张图片

(1)先获取返回值和返回值类型,找那个返回值处理器能处理,找到了(handler不为空)就用它处理返回值

	@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

		HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
		if (handler == null) {
			throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
		}
		handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
	}
  • 判断是不是异步的返回值
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第24张图片

    • 用supportReturnType判断是否支持,就是通过类型进行判断
      请添加图片描述

    总共有15种返回值解析器

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第25张图片

  • 支持,就调用handleReturnValue处理

4、SpringMVC到底支持哪些返回值

  • callable是判断异步的
  • 有标了@ModelAttribute&&不是字符串(是个对象才行)
  • @ResponseBody 注解 —> RequestResponseBodyMethodProcessor;用这个处理器
ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask@ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor

5、分析RequestResponseBodyMethodProcessor是如何处理的?

  • 内容协商就是遍历所有的MessageConverter最终找到一个能处理这个媒体类型数据的
  • 内容协商的目的是 在不同的情况下,能够返回不同的内容【我们以xml和jackson为例】
  • 支持json我们用的是jackson的工具包,想支持xml我们需要引入对应的依赖 【下面是xml对应的依赖】
     <dependency>
    	<groupId>com.fasterxml.jackson.dataformat</groupId>
    	<artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
    
  • 使用postman发送请求时, */* 代表能接受所有请求 【postman用于模拟浏览器发送请求】
    • accept请求头用于告诉浏览器我们接收什么样的数据:
      • application/json 接收的是json格式的数据
      • application/xml 接收的事xml格式的数据
      • 这就是在SpringMVC底层提供的非常方便的协商功能

演示一下我们使用PostMan模拟浏览器发送请求

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第26张图片
【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第27张图片

6、说一下内容协商的原理是什么?

  • 先找到@ResponseBody调用的Handler处理器
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第28张图片

  • 拿到返回值先判断是不是资源类型,接下来是内容协商的核心环节【判断当前响应头中是否已经有确定的媒体类型。MediaType】

    • 没有前置的拦截过程,就不会有媒体类型,有媒体类型说明已经被处理了
      • 没有就新创建,有的话就用之前的媒体类型
  • 然后获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段

    • 使用请求头的策略contentNegotiationManager来解析我们的媒体类型
      请添加图片描述
      HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
      在这里插入图片描述
  • 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

  • 找到支持操作Person的converter,把converter支持的媒体类型统计出来。【获得一个集合】

  • 客户端需要【application/xml】。服务端能力【10种、json、xml】
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第29张图片

  • 进行内容协商的最佳匹配媒体类型 【双层for循环进行最佳匹配】
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第30张图片

  • 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化【前俩支持Person -> JSON、后俩支持Person -> XML】
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第31张图片

  • 导入了jackson处理xml的包,xml的converter就会自动进来

WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

if (jackson2XmlPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
		}
  • canRead 如果能读就将传递过来的消息读成我们指定的类型,如果canWrite就把我们返回的数据写到我们的响应里面

7、自定义 MessageConverter

  • 正常的处理流程:

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第32张图片

  • 有些场景需要我们自定义MessageConverter
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第33张图片

  • 方案一:设计三个方法,让这三种不同的请求的请求路径不一样

  • 方案二:就要说到如何自定义converter

    • 我们要定义一个类实现 HttpMessageConverter 接口,在接口中指定规则

      package com.atguigu.boot.converter;
      
      import com.atguigu.boot.bean.Person;
      import org.springframework.http.HttpInputMessage;
      import org.springframework.http.HttpOutputMessage;
      import org.springframework.http.MediaType;
      import org.springframework.http.converter.HttpMessageConverter;
      import org.springframework.http.converter.HttpMessageNotReadableException;
      import org.springframework.http.converter.HttpMessageNotWritableException;
      
      import java.io.IOException;
      import java.io.OutputStream;
      import java.util.List;
      
      /**
       * @author Bonbons
       * @version 1.0
       */
      public class MyMessageConverter implements HttpMessageConverter<Person> {
          @Override
          public boolean canRead(Class<?> aClass, MediaType mediaType) {
              //读我们指的是通过@RequestBody注解作为参数携带过来的数据,此处我们不关心读
              return false;
          }
      
          @Override
          public boolean canWrite(Class<?> aClass, MediaType mediaType) {
              //返回值类型是Person类型就能写
              return aClass.isAssignableFrom(Person.class);
          }
      
          /**
           * 服务器要统计MessageConverter能写出哪些类型[兼容的媒体类型]
           * 我们通过parseMediaTypes将我们自定义类型传递进去
           * @return
           */
          @Override
          public List<MediaType> getSupportedMediaTypes() {
              return MediaType.parseMediaTypes("application/x-zwh");
          }
      
          @Override
          public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
              return null;
          }
      
          @Override
          public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
              //将我们Person中的数据拼装成一个用分号分隔的字符串
              String data = person.getName() + ";" + person.getAge() + ";" + person.getBirth();
              //通过输出流将我们的数据写出去
              OutputStream body = httpOutputMessage.getBody();
              body.write(data.getBytes());
          }
      }
      
    • 接下来要到我们的配置类中,重写 extendsMessageConverters 方法

      @Override
       public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
       	converters.add(new MyMessageConverter());
       }
      
  • 此时已经可以处理我们自定义请求,此处我通过 PostMan 模拟 【测试成功】

【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第34张图片

我们需要注意一点:重写那个方法要写在配置类的 WebMvcConfigurer

 @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {

            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

            }
        }
    }

8、如何以参数的方式进行内容协商

  • 我们知道,协商策略分为两种:
    【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第35张图片

    • 一种是基于参数的:在地址栏后添加 ?format=指定的类型【默认可以为json、xml】
      请添加图片描述
    • 另一种是基于请求头的:设置请求头的accept属性值,上面介绍了通过自定义MessageConverter实现请求头处理自定义的协商策略
  • 那么我们是否可以采用基于参数的形式制定自定义协商策略呢?

    • 在配置类中自定义WebMvcConfigurer
      • 重写 configureContentNegotiation 方法配置协商功能

        @Bean
        public WebMvcConfigurer webMvcConfigurer(){
        	return new WebMvcConfigurer() {
        	@Override
        	public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        		Map<String, MediaType> mediaTypes = new HashMap<>();
        		//向集合中添加策略
        		mediaTypes.put("xml", MediaType.APPLICATION_ATOM_XML);
        		mediaTypes.put("json", MediaType.APPLICATION_JSON);
        		mediaTypes.put("gg", MediaType.parseMediaType("application/x-guigu"));
        		//指定支持哪些参数对应的媒体类型
        		ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
        		configurer.strategies(Arrays.asList(parameterStrategy));
        	}
        };
        

        【零基础入门SpringBoot2】—— 核心功能_配置文件与Web开发_第36张图片

  • 如果我们现在不进行处理,那么基于请求头的协商策略就会失效(accept无论为什么都会以json的形式返回)

    • 我们需要在自定义的协商策略中,添加基于请求头的协商策略

    • 也就是将上面这段重写的方法替换为

      @Override
      public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
      	Map<String, MediaType> mediaTypes = new HashMap<>();
      	//向集合中添加策略
      	mediaTypes.put("xml", MediaType.APPLICATION_ATOM_XML);
      	mediaTypes.put("json", MediaType.APPLICATION_JSON);
      	mediaTypes.put("gg", MediaType.parseMediaType("application/x-guigu"));
      	//指定支持哪些参数对应的媒体类型
      	ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
      
      	//添加基于请求头的协商策略
      	HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
      	configurer.strategies(Arrays.asList(parameterStrategy, headerStrategy));
      }
      
  • 几个注意事项:

    • PostMan中模拟发起请求测试我们的自定义策略时,accept 字段属性值赋值的不是gg而是完成的策略名
      请添加图片描述
    • 对于基于参数的协商策略,我们可以将 format 修改为我们指定的字段【只需在上面我们重写的方法中,利用参数协商对象的setParamrterName方法】
      请添加图片描述

你可能感兴趣的:(SpringBoot2,前端,java,spring,java框架,springboot)