5、SpringMVC自动配置概览

在这里插入图片描述

1、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 (covered later in this document)).

    • 静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.

    • 自动注册 Converter,GenericConverter,Formatter
  • Support for HttpMessageConverters (covered later in this document).

    • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).

    • 自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.

    • 静态index.html 页支持
  • Custom Favicon support (covered later in this document).

    • 自定义 Favicon
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

    • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到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.

声明 **WebMvcRegistrations** 改变默认底层组件(请求解析器,异常处理解析器)

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

2、简单功能分析

2.1、静态资源访问

1、静态资源目录

只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources

访问 : 当前项目根路径/ + 静态资源名

静态资源访问或者其他资源访问的原理:

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

2、静态资源访问前缀和静态资源目录

默认无前缀: **/****.

我们可如下配置static-path-pattern修改其前缀:

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

访问路径:当前项目 + static-path-pattern + 静态资源名 => 静态资源文件夹下找静态资源

我们可通过static-locations改变默认的静态资源路径(目录)

spring:
  resources:
    static-locations: [classpath:/haha/]

这样我们访问静态资源将从 resources/haha/ 下进行查找.

3、webjar

SpringBoot 中将一些静态资源达成了Jar包,通过导入依赖,我们可以使用相关的静态资源.

自动映射 /webjars/**

可通过https://www.webjars.org/选取需要的静态资源jar包.

举例:

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

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

2.2、欢迎页支持

  • 静态资源路径下 index.html

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

spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致welcome page功能失效

  resources:
    static-locations: [classpath:/haha/]
  • controller能处理/index

2.3、自定义 Favicon

favicon.ico 放在静态资源目录下即可。

spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致 Favicon 功能失效

Ctrl+shift+r/Ctrl+F5:强制刷新页面

2.4、静态资源配置原理

  • SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
  • SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@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
1、配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定
//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、资源处理的默认规则
#########################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/**")) {
                //如果是,则从/META-INF/resources/webjars/进行寻找静态资源.
		customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
						.addResourceLocations("classpath:/META-INF/resources/webjars/")
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));//按照设置的时间缓存
			} 
            
            //其他静态资源的规则
			String staticPathPattern = this.mvcProperties.getStaticPathPattern();//获取获取静态资源路径static-path-pattern的值.
			if (!registry.hasMappingForPattern(staticPathPattern)) {
				customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
						.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
			}//调用getResourceLocations(),从staticLocations下找静态资源
		}

#########################################################################################
spring:
  resources:
    add-mappings: false   false:禁用所有静态资源规则
    cache:1100			  静态资源可以存活多长时间   
        
@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/" };
	private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
    ...
}
3、欢迎页的处理规则
	//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()构造方法.#############################
	WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
			ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
			//如果欢迎页存在,并且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

如果并且staticPathPattern是默认的/**,则取资源路径下寻找favicon.jpg,如果有则使用.如果不是默认的配置,则该规则失效.

3、请求参数处理

0、请求映射

1、rest使用与原理

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

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET- 获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
    • 核心Filter:HiddenHttpMethodFilter
      • 用法: 表单method=post ,隐藏域 _method=put
      • SpringBoot中手动开启:spring.mvc.formcontent.filter.enable=true
 #####################################MethodController################################## @RestController
public class MethodController {
    @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-张三";
    }
}

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

Rest原理(表单提交要使用REST的时候)

  • 表单提交会带上**_method=PUT**

  • 请求过来被HiddenHttpMethodFilter拦截

    • 请求是否正常,并且是POST
      • 获取到**_method**的值。
      • 兼容以下请求;PUT.DELETE.PATCH
      • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
      • 过滤器链放行的时候用wrapper(被包装后的request)。以后的方法调用getMethod是调用 requestWrapper的。
###############################原理#####################################################
################SpringMvcAutoConfiguration#############################################	
    @Bean
	@ConditionalOnMissingBean(FormContentFilter.class)//OrderedFormContentFilter继承FormContentFilter    没有FormContentFilter 就创建一个.
	@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
	public OrderedFormContentFilter formContentFilter() {
		return new OrderedFormContentFilter();
	}
    

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		HttpServletRequest requestToUse = request;
//判断类型是否为post类型
		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
		//获取_method的值.
			String paramValue = request.getParameter(this.methodParam);
			if (StringUtils.hasLength(paramValue)) {
				String method = paramValue.toUpperCase(Locale.ENGLISH);
				if (ALLOWED_METHODS.contains(method)) {
					requestToUse = new HttpMethodRequestWrapper(request, method);
				}
			}
		}

		filterChain.doFilter(requestToUse, response);
	}

扩展:如何把_method 这个名字换成我们自己喜欢的。

@Configuration
public class WebConfig {
    //自定义Bean
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();//默认methodParam为_method
        methodFilter.setMethodParam("_m");//我们调用set方法,起名进行覆盖.
        return methodFilter;
    }
}

Rest使用客户端工具

  • 如PostMan/IDEA-Client直接发送Put、delete等方式请求,无需Filter。

注:我们还可以使用@PostMapping,@DeleteMapping,@PutMapping直接发送对应的注解.

2、请求映射原理

在这里插入图片描述

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################################               
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {//handlerMappings:处理器映射,一共五个.
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}                

img

RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。

img

总结:

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

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;

  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping,即自定义 HandlerMapping

1、普通参数与基本注解

1.1、注解:

@PathVariable
  • 如果有参数,获取请求路径中参数的值.

  • 如果无参数,且后面跟 Map<String,String>类型的.将获取路径中所有参数的值并存放到该Map中.

@GetMapping("/car/{id}/owner/{username}")
    public Map<String,Object> getCar(@PathVariable("id") Integer id,@PathVariable("username") String name,@PathVariable Map<String,String> pv){
        Map <String, Object> map = new HashMap <>();
        map.put("id", id);
        map.put("name", name);
        map.put("pv", pv);
        return  map;
    }
##########################运行结果############################################
 {"pv":{"id":"3","username":"zhangsan"},"name":"zhangsan","id":3}
@RequestHeader
  • 如果有参数,获取的是指定请求头的值.
  • 无参数,且后跟类型为Map<String,String>,获取所有请求头的键和值.
@GetMapping("/car")
    public Map<String,Object> getCar(@RequestHeader("User-Agent") String userAgent,@RequestHeader Map<String,String> headers){

        Map <String, Object> map = new HashMap <>();
        map.put("userAgent", userAgent);
        map.put("headers", headers);
        return  map;
    }
@RequestParam
@GetMapping("/car")
    public Map<String,Object> getCar(@RequestParam("age") Integer age, @RequestParam("inters") List<String> inters,
                                     @RequestParam Map<String,String> params){
        Map <String, String> map = new HashMap <>();
        map.put("age",age);
        map.put("inters", inters);
        map.put("params", params);
        return  map;
    }
##############################################################################
 {"inters":["football","game"],"params":{"age":"18","inters":"football"},"age":18}
@CookieValue
  • 如果有参数,获取的是指定Cookie的值.
  • 有参数,后面跟的是一个Cookie类型, 将Cookie值保存到另外一个Cookie中.
@RequestBody

获取Post请求的请求体.

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

#################################################################################
{"content":"userName=zhagnsan&email=1456"}
@RequestAttribute
@Controller
public class RequestController {
    @GetMapping("/goto")
    public String goToPage(HttpServletRequest request){
        request.setAttribute("msg","成功了...");
        request.setAttribute("code",200);
        return "forward:/success"; //转发到success的请求
    }

    @ResponseBody
    @GetMapping("/success")
    public Map success(@RequestAttribute("msg")String msg,@RequestAttribute("code") Integer code,HttpServletRequest request){
        Map <String, Object> map = new HashMap <>();
        Object msg1 = request.getAttribute("msg");
        map.put("reqMathod_msg", msg1);
        map.put("anno_msg", msg);
        map.put("code", code);
        return map;
    }


}
####################################################################################
{"reqMathod_msg":"成功了...","code":200,"anno_msg":"成功了..."}
@MatrixVariable

传统方式获取/cars/{path}?xxx=xxx&aaa=ccc queryString 中请求参数的数据使用@RequestParam

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

未被禁用时:

  • 正向:每次请求时携带cookie,cookie中存放着jsessionid ,通过它这样可以找到指定的session.

  • 逆向**:session.set(a,b)—> jsessionid —> cookie ----> 每次发请求携带**。

    所以一旦cookie被禁用,session便无法找到了.

矩阵变量 😗*/cars/sell;low=34;brand=byd,audi,yd ; ** /cars/sell;low=34;brand=byd;brand=audi;brand=yd;

解决办法:url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.



############################################第一种配置方式,实现WebMvcConfigurer接口#########
@Configuration
public class WebConfig implements WebMvcConfigurer {
   
    //其实就是重写该方法,然后该方法里面的方法就会生效代替默认的.
    //JDK8及其之后接口方法有了默认实现,所以我们只需要重写自己想要的即可.
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        //设置成false:不移除;后面的内容,矩阵变量功能开始生效.
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

############################################第二种方式,注册一个WebMvcConfigurer#############
@Configuration
public class WebConfig2 {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                UrlPathHelper urlPathHelper = new UrlPathHelper();
                urlPathHelper.setRemoveSemicolonContent(false);
                configurer.setUrlPathHelper(urlPathHelper);
            }
        };
    }
}
############################################Controller###################################
	//1、语法:请求路径:/cars/sell;low=34;brand=byd,audi,yd;		 
       //          或/cars/sell;low=34;brand=byd;brand=audi;brand=yd;
    //2、SpringBoot默认是禁用了矩阵变量的功能
    //      手动开启:原理:
    //      对于路径的处理,UrlPathHelper进行解析。
    //      removeSemicolonContent(true移除分号后面的内容):flase ,使用矩阵变量.
    //3.矩阵变量必须绑定在路径变量上才可以被解析,所以要写@GetMapping("cars/{path}"),而非@GetMapping("cars/sell")   
   

    @GetMapping("cars/{path}")
    public Map carsSell(@MatrixVariable("low") Integer low,@MatrixVariable("brand") List<String> brand,@PathVariable("path") String path){//path:真正的访问路径
        Map <Object, Object> map = new HashMap <>();
        map.put("low",low);
        map.put("brand", brand);
        map.put("path", path);
        return map;
    }


######################################运行结果1#######################################
{"path":"sell","low":34,"brand":["byd","audi","yd"]} 
可以看出真正的路径仍然是sell,不包括后面的矩阵变量.
    
 	//当矩阵变量重复时,如何获取.
    // /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 <Object, Object> map = new HashMap <>();
        map.put("bossAge", bossAge);
        map.put("empAge", empAge);
        return map;
    }    

####################################运行结果2############################################
  {"bossAge":20,"empAge":10}  

1.2、Servlet API:

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

ServletRequestMethodArgumentResolver 以上的部分参数

@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);
	}

1.3、复杂参数:

Map、**Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、**Errors/BindingResult、RedirectAttributes( 重定向携带数据)ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder


#################################################TestController#######################
 @GetMapping("/params")
    public String testParame(Map<String,Object> map, Model model,
                             HttpServletRequest request,
                             HttpServletResponse response){
        map.put("hello", "world666");
        model.addAttribute("wordl", "hello666");
        request.setAttribute("message","HelloWorld");
        Cookie cookie = new Cookie("c1", "v1");
        response.addCookie(cookie);
        return "forward:/success";
    }

    @ResponseBody
    @GetMapping("/success")
    public Map success(@RequestAttribute(value = "msg",required = false)String msg,@RequestAttribute(value = "code",required = false) Integer code,HttpServletRequest request){
        Map <String, Object> map = new HashMap <>();
        Object msg1 = request.getAttribute("msg");
        Object hello = request.getAttribute("hello");
        Object world = request.getAttribute("world");
        Object message = request.getAttribute("message");
        map.put("reqMathod_msg", msg1);
        map.put("anno_msg", msg);
        map.put("code", code);
        map.put("hello", hello);
        map.put("world", world);
        map.put("message", message);
        return map;
    }
###############################################运行结果################################
{"reqMathod_msg":null,"code":null,"world":null,"hello":"world666","message":"HelloWorld","anno_msg":null}    

结果分析:Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据,
**最终可通过request.getAttribute()**来获取

  • 当参数是Map时,HandlerMethodArgumentResolver为MapMethodProcessor;当参数是Model时,HMAResolver为ModelMethodProcessor;

  • 通过Debug5.2 可知,Map、Model类型的参数都会通过 mavContainer.getModel()==>>BindingAwareModelMap 既 是Model 也是Map

###############################ModelAndViewContainer#####################################
public class ModelAndViewContainer {

   private boolean ignoreDefaultModelOnRedirect = false;

   @Nullable
   private Object view;

   private final ModelMap defaultModel = new BindingAwareModelMap();
   public ModelMap getModel() {
		if (useDefaultModel()) {
			return this.defaultModel;
		}
		else {
			if (this.redirectModel == null) {
				this.redirectModel = new ModelMap();
			}
			return this.redirectModel;
		}
	}
###########################################BindingAwareModelMap体系#######################	
public class BindingAwareModelMap extends ExtendedModelMap {}
public class ExtendedModelMap extends ModelMap implements Model {}

第一个和第二个参数是一样的.本质上是一个对象.

在这里插入图片描述

Model 和Map被放在ModelAndViewContainer中.
在这里插入图片描述

2、自定义对象参数

可以自动类型转换与格式化,可以级联封装。

##################################index.html###########################################
/**
 *     姓名: <input name="userName"/> <br/>
 *     年龄: <input name="age"/> <br/>
 *     生日: <input name="birth"/> <br/>
 *     宠物姓名:<input name="pet.name"/><br/>
 *     宠物年龄:<input name="pet.age"/>
 */
###################################################PersonPet########################    
@Data
public class Person {
    
    private String userName;
    private Integer age;
    private Date birth;
    private Pet pet;
    
}

@Data
public class Pet {

    private String name;
    private String age;

}
###########################################ParameterTestController#######################
    /**
     * 数据绑定:页面提交的请求数据(GET/POST)都可以和对象属性进行绑定.
     * @param person
     * @return
     */
    @PostMapping("/saveuser")
    public Person saveuser(Person person){
        return person;
    }
######################################################运行结果#######################
    {"userName":"zhangsan","birth":"2019-12-09T16:00:00.000+00:00","age":18,"pet":{"name":"啊猫","age":"5"}

POJO封装过程

依赖于ServletModelAttributeMethodProcessor , 原理见 5.3

3、参数处理原理

  • HandlerMapping中找到能处理请求的Handler(里面保存了浏览器的请求路径与Controller中对应method的信息.)==============>getHandler()

  • 为当前Handler 找一个适配器 HandlerAdapter: RequestMappingHandlerAdapter ================>getHandlerAdapter()

    适配器作用:执行目标方法并确定方法参数的每一个值

img

0 - 支持方法上标注@RequestMapping (咱们使用的就是这个)

1 - 支持函数式编程的

1、HandlerAdapter

public interface HandlerAdapter {
	//支持哪些方法.
	boolean supports(Object handler);

	//如果支持进行处理的方法.
	@Nullable
	ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;


	@Deprecated
	long getLastModified(HttpServletRequest request, Object handler);

}

2、执行目标方法

// Actually invoke the handler.
//DispatcherServlet -- doDispatch()
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//==>进入RequestMappingHandler的handleInternal()方法. 里面也出现了 参数解析器和返回值解析器...
    
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法

invocableMethod.invokeAndHandle(webRequest, mavContainer);//执行..
//再次进入ServletInvocableHandlerMethod的invokeAndHandle
//  真正的执行目标方法.
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值  原理在下面...
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);

doInvoke(args);//开始执行..底层就是反射技术

3、参数解析器-HandlerMethodArgumentResolver

-----------------------------------------------------------------------位于RequestMappingHandler的handleInternal中

作用:确定将要执行的目标方法的每一个参数的值是什么;

SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
在这里插入图片描述

HandlerMethodArgumentResolver本质:— 一个接口

img

  • 当前解析器是否支持解析这种参数
  • 支持就调用 resolveArgument

4、返回值处理器

-----------------------------------------------------------------------位于RequestMappingHandler的handleInternal中

作用:确定方法可以写的返回值类型.比如:ModelAndView、Model、View、ResponseBody…

img

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++) {
            //获取第i个参数..
			MethodParameter parameter = parameters[i];
			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
			args[i] = findProvidedArgument(parameter, providedArgs);
			if (args[i] != null) {
				continue;
			}
            //判断当前解析器是否支持这个参数类型.  底层原理 5.1
			if (!this.resolvers.supportsParameter(parameter)) {
				throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
			}
			try {
                // 利用上面的参数解析器进行解析并存入 args数组中.  底层原理5.2 
				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;
	}
5.1、挨个判断所有参数解析器那个支持解析这个参数
	@Nullable
	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) 
    {
        //从参数解析缓存中找到该参数的 解析器.
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {//如果没找到
            //遍历所有的参数解析器25种
			for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
				if (resolver.supportsParameter(parameter)) {//如果该解析器支持该参数.
					result = resolver;
					this.argumentResolverCache.put(parameter, result);//将其存入到缓存中,这样下次可以直接从缓存中获取, 不用再进行循环获取了.  牛掰了..这操作. 所以通常第一次访问会比较慢,之后就很快了.
					break;
				}
			}
		}
		return result;
	}
5.2、利用参数解析器解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可

底层原理

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);==>进入resolveArgument()
resolveArgument(parameter, mavContainer, webRequest, binderFactory);==>在进入
mavContainer.getModel()
5.3、自定义类型参数 封装POJO

ServletModelAttributeMethodProcessor 这个参数处理器支持

是否为简单类型。

############################################当参数处理器是:ServletModelAttributeMethodProcessor,  resolvers.supportsParameter(parameter)的底层判断原理########
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) || //是不是Number类型
				Date.class.isAssignableFrom(type) ||
				Temporal.class.isAssignableFrom(type) ||
				URI.class == type ||
				URL.class == type ||
				Locale.class == type ||
				Class.class == type));
	}
##########################################resolveArgument###############################	
@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 {
			//创建一个空的person对象..
				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中 ,如String->Integer,String0->Date的转换器…

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

Converter的总接口: @FunctionalInterface public interface Converter<S, T>

WebDataBinder

在这里插入图片描述

数据类型转换器Converters

img

未来我们可以给WebDataBinder里面放自己的Converter;

private static final class StringToNumber<T extends Number> implements Converter<String, T>

自定义 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数据。

img

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);
		}
	}

暴露模型作为请求域属性

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);
			}
		});
	}

4. 自定义类型转换器

当提交的参数和要封装的属性名称一致时,SpringBoot回为我们自动封装,但当不一致时,我们应该自己定义类型转换器.Converter <String, T>

##################################index.html###########################################
/**
 *     姓名: <input name="userName"/> <br/>
 *     年龄: <input name="age"/> <br/>
 *     生日: <input name="birth"/> <br/>
 *     宠物: <input name="pet" value="啊猫,3"/>
 */
###################################################PersonPet########################    
@Data
public class Person {
    
    private String userName;
    private Integer age;
    private Date birth;
    private Pet pet;
    
}

@Data
public class Pet {

    private String name;
    private String age;

}
###########################################ParameterTestController#######################
    /**
     * 数据绑定:页面提交的请求数据(GET/POST)都可以和对象属性进行绑定.
     * @param person
     * @return
     */
    @PostMapping("/saveuser")
    public Person saveuser(Person person){
        return person;
    }
######################################WebConfig########################################
    @Configuration
public class WebConfig3 {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            //添加格式转换器
            @Override
            public void addFormatters(FormatterRegistry registry) {
                registry.addConverter(new Converter <String, Pet>() {

                    @SuppressWarnings("deprecation")
                    @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(split[1]);
                            return pet;
                        }
                        return null;
                    }
                });
            }
        };
    }
}
######################################################运行结果#######################
{"userName":"zhangsan","birth":"2019-12-09T16:00:00.000+00:00","age":18,"pet":{"name":"啊猫","age":"3"}}

这样在数据进行转换时会用125个转换器…在转换时会使用我们定义的那个…

4、数据响应与内容协商

img

1、响应JSON

1.1、jackson.jar+@ResponseBody

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--web场景自动引入了json场景-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-json</artifactId>
      <version>2.3.4.RELEASE</version>
      <scope>compile</scope>
    </dependency>

<!--底层主要依赖这些jar-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.4</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jdk8</artifactId>
      <version>2.12.4</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
      <version>2.12.4</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.module</groupId>
      <artifactId>jackson-module-parameter-names</artifactId>
      <version>2.12.4</version>
      <scope>compile</scope>
    </dependency>

测试代码:

@Controller
public class ResponseTestController {
    @ResponseBody //利用返回值处理器里面的消息转换器进行处理
    @GetMapping(value = "test/person")
    public Person getPerson(){
        Person person = new Person();
        person.setAge(28);
        person.setBirth(new Date());
        person.setUserName("zhangsan");
        return person;
    }
}

给前端自动返回json数据;

1、返回值解析器

在这里插入图片描述

######################################invokeAndHandle()方法############################
// 真正的执行方法  returnValue:返回的Person类型/Object类型
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

// 处理返回值.
this.returnValueHandlers.handleReturnValue(
			returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
##############################进入handleReturnValue()####################################
	@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);
	}
#######################selectHandler()############################################
@Nullable
	private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
        // 判断是不是异步返回值.. false
		boolean isAsyncValue = isAsyncReturnValue(value, returnType);
        // 最终找到RequestResponseBodyMethodProcessor可以进行处理
		for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
			if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
				continue;
			}
			if (handler.supportsReturnType(returnType)) {
				return handler;
			}
		}
		return null;
	}

##########################该返回值处理器对应的handleReturnValue()###########################
@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);
	}
2、返回值解析器原理

在这里插入图片描述

  • 1、返回值处理器判断是否支持这种类型返回值 supportsReturnType

  • 2、返回值处理器调用 handleReturnValue 进行处理

  • 3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。

      1. 利用 MessageConverters 进行处理 将数据写为json
      • 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      • 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,最终确定一种两者都可以处理的类型.
      • 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
        • 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
        • 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。

浏览器的请求头

img

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

ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
支持     ListenableFuture
    	 CompletionStage
   		 WebAsyncTask@ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor...

1.3、HTTPMessageConverter原理

1、MessageConverter规范

img

HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。

例子:当写的时候支不支持Person对象转为JSON。或者读的时候支不支持 JSON转为Person

2、默认的MessageConverter

img

0 - 只支持Byte类型的

1 - String

2 - String

3 - Resource

4 - ResourceRegion

5 - DOMSource.*class * SAXSource.class) \ StAXSource.**class **StreamSource.**class **Source.class

6 - MultiValueMap

7 - true utf-8

8 - true ISO-8859-1

9 - 支持注解方式xml处理的。

注:其中1和2,7和8都是字符编码不同,一个数utf-8一个数IOS8859-1

最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)

img

2、内容协商

根据客户端接收能力(环境不同)不同,返回不同媒体类型的数据。

1、引入xml依赖

 <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
######当SpringBoot2.3版本时,使用这个依赖前后结果不同.如果是2.5版本,结果不会发生变化..##########

2、postman分别测试返回json和xml

只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。

在这里插入图片描述

3、内容协商原理

  • 1、判断当前响应头中是否已经有确定的媒体类型-- MediaType

  • 2、获取客户端(PostMan、浏览器)支持接收的内容类型。(默认获取客户端Accept请求头字段)【application/xml】

    • contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
    • img
    • HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
    • img
  • 3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

  • 4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。

  • 5、客户端需要【application/xml】。服务端能够【10种、json、xml】

  • img

  • 6、进行内容协商的最佳匹配媒体类型

  • 7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。

    现在咱们客户端需要的是xml,只有后两种满足条件.

img

底层原理

#############################writeWithMessageConverters##############################

		MediaType selectedMediaType = null;
		//1.判断当前响应头中是否已经有确定的媒体类型。MediaType
		MediaType contentType = outputMessage.getHeaders().getContentType();
		boolean isContentTypePreset = contentType != null && contentType.isConcrete();
		if (isContentTypePreset) {
			if (logger.isDebugEnabled()) {
				logger.debug("Found 'Content-Type:" + contentType + "' in response");
			}
			 //如果有,就使用之前确定的MediaType 
			selectedMediaType = contentType;
		}
		else {
		//2.获取客户端(PostMan、浏览器)支持接收的内容类型。application/xml ==>  底层原理 在下面
			List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
			 	//5.获取服务器端能产生的媒体类型.(10种) ==>底层原理 在下面
			List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
			//最终要使用的媒体类型
			List<MediaType> mediaTypesToUse = new ArrayList<>();
			//6.进行内容协商的最佳匹配媒体类型,即浏览器能接收的类型有没有和服务器能产生的类型相匹配.
			for (MediaType requestedType : acceptableTypes) {//浏览器能接收的类型
				for (MediaType producibleType : producibleTypes) {//服务器能产生的类型
					if (requestedType.isCompatibleWith(producibleType)) {
						mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
					}
				}
			}
		}
if (selectedMediaType != null) {
			selectedMediaType = selectedMediaType.removeQualityValue();
			//7.用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
			for (HttpMessageConverter<?> converter : this.messageConverters) {
				GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
						(GenericHttpMessageConverter<?>) converter : null);
				if (genericConverter != null ?
						((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) ://判断是否能写
						converter.canWrite(valueType, selectedMediaType)) {//进行转换
			
################################################getProducibleMediaTypes()##############
	List<MediaType> result = new ArrayList<>();
//3、遍历循环所有当前系统的 **MessageConverter**,看谁支持操作这个对象(Person)
		for (HttpMessageConverter<?> converter : this.messageConverters) {
			if (converter instanceof GenericHttpMessageConverter && targetType != null) {
				if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
				//4.找到支持操作Person的converter,把converter支持的媒体类型统计出来。
					result.addAll(converter.getSupportedMediaTypes(valueClass));
				}
			}
			else if (converter.canWrite(valueClass, null)) {
				result.addAll(converter.getSupportedMediaTypes(valueClass));
			}
		}
 ######################################getAcceptableMediaTypes()#######################
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request){

		return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));//contentNegotiationManager 内容协商管理器
	}   
==================================================================================>>>>   @Override
	public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
    //		默认使用HeaderContentNegotiationStrategy  
		for (ContentNegotiationStrategy strategy : this.strategies) {
		//本质上获取请求头的底层:AcceptString[] headerValueArray=request.getHeaderValues(HttpHeaders.ACCEPT);
			List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
			if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {// 如果得到的是 */*,则继续进行遍历寻找
				continue;
			}
			return mediaTypes;
		}
		return MEDIA_TYPE_ALL_LIST;//如果最后还是 */*,则直接返回这个.
	} 
============================================resolveMediaTypes=========================                    
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
		if (headerValueArray == null) {
			return MEDIA_TYPE_ALL_LIST;
	}                    

导入了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()));//添加一个MappingJackson2XmlHttpMessageConverter()
		}

4、开启浏览器参数方式内容协商功能

浏览器除了可以通过AJAX可以指定返回值类型,同时进行为了方便内容协商,还可以开启基于请求参数的内容协商功能

spring:
    contentnegotiation:
      favor-parameter: true  #开启请求参数内容协商模式

发请求: http://localhost:8080/test/person?format=json

http://localhost:8080/test/person?format=xml,将根据format后面的参数不同,显示对应类型的返回值.

**内容协商管理器中有两个策略, 一个是基于请求参数的参数协商策略,另一个是默认的请求头协商策略. **

当使用第一种请求时,内容协商管理器情况如下:

在这里插入图片描述

确定客户端接收什么样的内容类型:

1、根据Parameter策略优先确定是要返回json数据(获取请求头中的format的值)

return request.getParameter(getParameterName())//getParameterName():format 根据请求参数得值返回需要的数据类型

2、最终进行内容协商返回给客户端json即可。

5、自定义 MessageConverter

如果需要以其他格式如PDF,Word,进行内容的返回,则需要自定义MessageConverter

实现多协议数据兼容:json、xml、x-guigu的原理分析:

0、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

1、Processor 处理方法返回值。通过 MessageConverter 处理

2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

3、内容协商找到最终的 messageConverter

如果需要给SpringMVC自定义什么功能。只需要在入口给容器中添加一个 WebMvcConfigurer

在这里插入图片描述

自定义messageConverter

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

            //扩展MessageConverter,默认的依旧保留,只是进行扩展.
            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) 				{

            }
            //配置MessageConverter 会覆盖掉默认的
            @Override
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                
			}
        }
    }
5.1 使用PostMan进行内容协商

案例需求以及实现步骤分析:

 /**
     * 1、浏览器发请求直接返回 xml    [application/xml]        jacksonXmlConverter
     * 2、如果是ajax请求 返回 json   [application/json]      jacksonJsonConverter
     * 3、如果硅谷app发请求,返回自定义协议数据  [appliaction/x-guigu]   xxxxConverter
     *       需要的返回结果格式:属性值1;属性值2;
     *
     * 步骤:
     * 1、添加自定义的MessageConverter进系统底层
     * 2、系统底层就会统计出所有MessageConverter能操作哪些类型
     * 3、客户端内容协商 [guigu--->guigu]
     *
     * 作业:如何以参数的方式进行内容协商(下一部分..)
     * @return
     */

实现代码:

#######################################GuiguMessageConverter############################
public class GuiguMessageConverter implements HttpMessageConverter<Person> {
    //咱们现在不关心读
    @Override
    public boolean canRead(Class <?> clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class <?> clazz, MediaType mediaType) {
        return clazz.isAssignableFrom(Person.class);//如果是Person类型则可以写操作..
    }

    /**
     * 服务器要统计所有的MessageConverter都能写哪些内容类型
     *
     * application/x-guigu
     * @return
     */
    @Override
    public List <MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/x-guigu");
    }

    @Override
    public Person read(Class <? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        //自定义数据的写出
       String data= person.getUserName()+";"+person.getAge()+";"+person.getBirth();
       //写出去
        OutputStream body = outputMessage.getBody();
        body.write(data.getBytes());
    }
}
#####################################WebConfig4#########################################
 @Configuration
public class WebConfig4 {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            //扩展MessageConverter
            @Override
            public void extendMessageConverters(List <HttpMessageConverter <?>> converters) {
                converters.add(new GuiguMessageConverter());
            }
        };
    }
}

在这里插入图片描述

原理分析:

源码中messageConverters:

在这里插入图片描述

5.2 如何以参数的形式进行内容协商

默认的基于参数的内容协商的MediaType只有xml和json两种类型.

img

基于参数的自定义MessageConverter

@Configuration
public class WebConfig4 {
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            //扩展MessageConverter
            @Override
            public void extendMessageConverters(List <HttpMessageConverter <?>> converters) {
                converters.add(new GuiguMessageConverter());
            }

            /**
             * 自定义内容协商策略
             * @param configurer
             */
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                Map <String, MediaType> mediaTypes = new HashMap <>();
                mediaTypes.put("json", MediaType.APPLICATION_JSON);
                mediaTypes.put("xml", MediaType.APPLICATION_XML);
                mediaTypes.put("gg", MediaType.parseMediaType("application/x-guigu"));
                
                ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
               // 设置请求参数的名称,默认是format
                // parameterStrategy.setParameterName("ff");
                //因为自定义参数的内容协商策略后,默认的请求头方式的会失效.所以可以手动曾加一个默认的headerContentNegotiationStrategy.
                HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();
                configurer.strategies(Arrays.asList(parameterStrategy,headerContentNegotiationStrategy));
            }
        };
    }
}

Debug内容协商管理器情况如下:

img注:如果使用了 基于参数的自定义MessageConverter,会使基于请求头的那种失效.无论传递哪种参数,最终返回的都是json. 处理:在进行实现时,增加一个HeaderContentNegotiationStrategy(实际上就是系统默认的一些内容协商策略…)

总结:**有时候我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。**这时候,我们可以通过Debug进行观察核心部分,然后进行处理.

大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】

5、视图解析与模板引擎

视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。

1、视图解析

img

1、视图解析原理流程

1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址

2、方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer

3、任何目标方法执行完成以后都会返回 ModelAndView数据和视图地址-------->原来是放在mavContainer中的,之后在getModelAndView()被重新抽取了出来.)。

4、processDispatchResult 处理派发结果(页面改如何响应

  • 1、render(mv, request, response); 进行页面渲染逻辑

    • 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】 ====>resolveViewName()
      • 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
      • 2、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。( 实际上内部使用的是 ThymeleafViewResolver)
      • 3、根据返回值 redirect:/main.html–> Thymeleaf解析器内部通过new RedirectView() 进行返回.
    • 2、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作 ===========>RedirectView 调用自己的render方法.

        • RedirectView 如何渲染【重定向到一个页面
        • 1、获取目标url地址
        • 2、response.sendRedirect(encodedURL); 底部是原生的重定向方法.

2.总结: 视图解析:

    • 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> render就是转发request.getRequestDispatcher(path).forward(request, response);
    • 返回值以 redirect: 开始: new RedirectView() --》 render就是重定向 response.sendRedirect(encodedURL);
    • 返回值是普通字符串: new ThymeleafView()—> 该模板底层按照自己的规则进行解析渲染.

扩展:自定义视图解析器+自定义视图;比如可以把数据渲染成Exel表格/Word文档…

img

遍历所有的视图解析器,根据返回值寻找可以处理的解析器

img

ContentNegotiationManager内部包含所有的视图解析器. 所以只需要遍历这一个视图解析器就OK了.

img

3.底层代码分析:

1.ViewNameMethodReturnValueHandler
###################返回值类型为String的返回值处理器ViewNameMethodReturnValueHandler##########
	//判断是否支持该返回值类型
    @Override
	public boolean supportsReturnType(MethodParameter returnType) {
		Class<?> paramType = returnType.getParameterType();
		return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));// 如果是空类型 或者是字符串类型,则支持
	}

// 如果支持,则进行处理
@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
		// mavContainer 本质上是一个ModelAndView
		if (returnValue instanceof CharSequence) {
			String viewName = returnValue.toString();//返回值转为String
			mavContainer.setViewName(viewName);//设置进去
			if (isRedirectViewName(viewName)) {//判断是否是重定向的视图 =>
                							//底层原理:viewName.startsWith("redirect:")
				mavContainer.setRedirectModelScenario(true); // 如果是,则设置一个重定向的传感器..
			}
		}
		else if (returnValue != null) {
			// should not happen
			throw new UnsupportedOperationException("Unexpected return type: " +
					returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
		}
	}
2.getModelAndView()方法
@Nullable
	private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
			ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

		modelFactory.updateModel(webRequest, mavContainer);
		if (mavContainer.isRequestHandled()) {
			return null;
		}
		ModelMap model = mavContainer.getModel();//这个model和Controller里面的model是同一个.
		ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());// 将viewName,model再次封装到ModelandView 中.
		if (!mavContainer.isViewReference()) {
			mav.setView((View) mavContainer.getView());
		}
		if (model instanceof RedirectAttributes) {//判断model是否是 重定向携带数据.. 即请求方法还可以使用 @RedirectAttributes 携带数据.
			Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
			HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
			if (request != null) {
				RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
			}//如果是则把数据放进去..
		}
		return mav;
	}


#########################################################################################
 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // 返回ModelandView.   ===>任何目标方法执行完成以后都会返回 ModelAndView
3.View对象的定义

在这里插入图片描述

2、模板引擎-Thymeleaf

1、thymeleaf简介

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.

现代化、服务端Java模板引擎

2、基本语法

1、表达式
表达式名字语法用途
变量取值${…}获取请求域、session域、对象等值
选择变量*{…}获取上下文对象值
消息#{…}获取国际化等值
链接@{…}生成链接
片段表达式~{…}jsp:include 作用,引入公共页面片段
2、字面量

文本值: ‘one text’ , ‘Another one!’ **,…**数字: 0 , 34 , 3.0 , 12.3 **,…**布尔值: true , false

空值: null

变量: one,two,… 变量不能有空格

3、文本操作

字符串拼接: +

变量替换: |The name is ${name}|

4、数学运算

运算符: + , - , * , / , %

5、布尔运算

运算符: and , or

一元运算: ! , not

6、比较运算

比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )

7、条件运算

If-then: (if) ? (then)

If-then-else: (if) ? (then) : (else)

Default: (value) ?: (defaultvalue)

8、特殊操作

无操作: _

3、设置属性值-th:attr

设置单个值

<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>

设置多个值

<img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

以上两个的代替写法 th:xxxx

<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">

所有h5兼容的标签写法

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes

4、迭代

###################创建User的集合users###################################################
public String dynamic_table(Model model){
        //表格内容的遍历
        List <User> users = Arrays.asList(new User("zhagnsan", "123"), new User("lisi", "123456"),
                new User("wangwu", "789"), new User("zhaoliu", "15651"));
        model.addAttribute("users", users);
        return "table/dynamic_table";
    }
###############################dynamic_table页面进行遍历#################################
<form>
		<thead>
            <tr>
                <th>编号</th>
                <th>用户名</th>
                <th>密码</th>
            </tr>
        </thead>
        <tbody>
        <tr class="gradeX" th:each="user, status:${users}">    //status:循环的状态.
            <td th:text="${status.count}">Trident</td>
            <td th:text="${user.userName}">Internet </td>

            <td > [[${user.passWord}]] </td>

        </tr>

        </tbody>
</form>

5、条件运算

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>

6、属性优先级

img

3、thymeleaf使用

1、引入Starter
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
2、自动配置好了thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }

自动配好的策略

  • 1、所有thymeleaf的配置值都在 ThymeleafProperties

  • 2、配置好了 SpringTemplateEngine

  • 3、配好了ThymeleafViewResolver

  • 4、配置好了页面的存放位置,以及后缀.

	public static final String DEFAULT_PREFIX = "classpath:/templates/";// 页面前缀

	public static final String DEFAULT_SUFFIX = ".html";  //xxx.html
3、页面开发
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1 th:text="${msg}">哈哈</h1>
<h2>
    <!-- 真正的取出了link值-->
    <a href="www.atguigu.com" th:href="${link}">去百度</a><br>
    <!--把link拼装成要去的地址.并且如果项目还有访问的前置路径则还会将其拼接上去.-->
    <a href="www.atguigu.com" th:href="@{link}">去百度</a>
</h2>
</body>
</html>

##################################ViewTestController#################################
@Controller
public class ViewTestController {

    @GetMapping("/atguigu")
    public String atguigu(Model model){

        //model中的数据会被自动放在请求域中
        model.addAttribute("msg", "你好,硅谷!");
        model.addAttribute("link", "http://www.baidu.com");
        return "success";
    }
}

4、构建后台管理系统

1、项目创建

thymeleaf、web-starter、devtools、lombok

2、静态资源处理

自动配置好,我们只需要把所有静态资源放到 static 文件夹下

3、路径构建

th:action="@{/login}"

4、模板抽取

th:insert/replace/include

5、页面跳转

    @PostMapping("/login")
    public String main(User user, HttpSession session, Model model){

        if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
            //把登陆成功的用户保存起来
            session.setAttribute("loginUser",user);
            //登录成功重定向到main.html;  重定向防止表单重复提交
            return "redirect:/main.html";
        }else {
            model.addAttribute("msg","账号密码错误");
            //回到登录页面
            return "login";
        }

    }

6、数据渲染

    @GetMapping("/dynamic_table")
    public String dynamic_table(Model model){
        //表格内容的遍历
        List<User> users = Arrays.asList(new User("zhangsan", "123456"),
                new User("lisi", "123444"),
                new User("haha", "aaaaa"),
                new User("hehe ", "aaddd"));
        model.addAttribute("users",users);

        return "table/dynamic_table";
    }
        <table class="display table table-bordered" id="hidden-table-info">
        <thead>
        <tr>
            <th>#</th>
            <th>用户名</th>
            <th>密码</th>
        </tr>
        </thead>
        <tbody>
        <tr class="gradeX" th:each="user,stats:${users}">
            <td th:text="${stats.count}">Trident</td>
            <td th:text="${user.userName}">Internet</td>
            <td >[[${user.password}]]</td>
        </tr>
        </tbody>
        </table>

7.页面公共代码的抽取

在这里插入图片描述

#######################################common.html文件
<head th:fragment="commonheader">
    <link href="css/style.css" rel="stylesheet">
    <link href="css/style-responsive.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
    <script src="js/html5shiv.js"></script>
    <script src="js/respond.min.js"></script>
    <![endif]-->
 </head>
#######################################在main.html########################################
================================================
<div th:insert="common :: commonheader"></div>   
================================================
<div><head>
    <link href="css/style.css" rel="stylesheet">
    <link href="css/style-responsive.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
    <script src="js/html5shiv.js"></script>
    <script src="js/respond.min.js"></script>
    <![endif]-->
 </head></div>
=======================================================
<div th:replace="common :: commonheader"></div>
=======================================================
<head>
    <link href="css/style.css" rel="stylesheet">
    <link href="css/style-responsive.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
    <script src="js/html5shiv.js"></script>
    <script src="js/respond.min.js"></script>
    <![endif]-->
</head>

=====================================================
<div th:include="common :: commonheader"></div>
====================================================
<div>
    <link href="css/style.css" rel="stylesheet">
    <link href="css/style-responsive.css" rel="stylesheet">

    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
    <script src="js/html5shiv.js"></script>
    <script src="js/respond.min.js"></script>
    <![endif]-->
</div>

6、拦截器

1、HandlerInterceptor 接口

//**
 * 登录检查
 * 1、配置好拦截器要拦截哪些请求
 * 2、把这些配置放在容器中
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        log.info("preHandle拦截的请求路径是{}",requestURI);
        //登录检查逻辑
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");
        if(loginUser!=null){
            //放行
            return true;
        }
        request.setAttribute("msg","请先登录!");
        request.getRequestDispatcher("/").forward(request,response);

        //进行拦截.
        return false;
    }

    /**
     * 目标方法完成之后
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行{}", modelAndView);
    }

    /**
     * 页面渲染之后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常{}", ex);
    }
}

2、配置拦截器

/**
 * 1、编写一个拦截器实现HandlerInterceptor接口
 * 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
 * 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
 *
 *
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")// 所有的请求都被拦截器拦截,包括静态资源  /**中 * 单级目录,/**代表多级目录.不能直接使用/*/*
                .excludePathPatterns("/", "/login","/error", "/css/**", "/fonts/**", "/images/**", "/js/**");//放行的请求.
    }
}

3、拦截器原理

1、根据当前请求,找到**HandlerExecutionChain【**可以处理请求的handler以及handler的所有 拦截器】

2、先来顺序执行 所有拦截器的 preHandle方法

  • 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
  • 2、如果当前拦截器返回为false。直接 触发 triggerAfterCompletion 倒序执行所有已经执行了的拦截器的 afterCompletion;

3、如果任何一个拦截器返回false。直接跳出不执行目标方法

4、如果所有拦截器都返回True。执行目标方法

5、目标方法执行完毕,倒序执行所有拦截器的postHandle方法。

6、前面的步骤(2,3,4,5)有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

img

拦截器链执行过程图解

img

总结:只有拦截器链的PreHandle执行完才会执行目标方法,只有目标方法执行完,才会倒叙执行postHandle方法.当出现异常时,直接倒叙执行AfterCompletion方法.当目标方法正常执行时,也会倒叙执行AfterCompletion方法.

7、文件上传

1、页面表单

<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
                            <div class="form-group">
                                <label for="exampleInputEmail1">邮箱</label>
                                <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
                            </div>
                            <div class="form-group">
                                <label for="exampleInputPassword1">名字</label>
                                <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
                            </div>
                            <div class="form-group">
                                <label for="exampleInputFile">头像</label>
                                <input type="file" id="exampleInputFile" name="headerImg">

                            </div>
                            <div class="form-group">
                                <label for="exampleInputFile">生活照</label>
                                <input type="file"  name="photos" multiple>

                            </div>
                            <div class="checkbox">
                                <label>
                                    <input type="checkbox"> Check me out
                                </label>
                            </div>
                            <button type="submit" class="btn btn-primary">提交</button>
</form>

2、文件上传代码

    /**
 * 文件上传测试
 */
@Controller
@Slf4j
public class FormTestController {
    @GetMapping("/form_layouts")
    public String form_layouts(){
        return "form/form_layouts";
    }

    /**
     *
     * MultipartFile 自动封装上传过来的文件
     * @param email
     * @param username
     * @param headerImg
     * @param photos
     * @return
     * @throws IOException
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email, @RequestParam("username") String username,@RequestPart("headerImg") MultipartFile headerImg,@RequestPart("photos") MultipartFile[] photos) throws IOException {
        log.info("上传的信息:email={},username={},headerImg={},photos={}",email,username,headerImg,photos);
        if(!headerImg.isEmpty()){
            //保存到文件服务器,OSS服务器
            String originalFilename = headerImg.getOriginalFilename();
            headerImg.transferTo(new File("D:\\upload2\\"+originalFilename));
        }
        if(photos.length>0){
            for (MultipartFile photo : photos) {
                if (!photo.isEmpty()) {
                    String originalFilename = photo.getOriginalFilename();
                    photo.transferTo(new File("D:\\upload2\\"+originalFilename));
                }
            }
        }
        return "main";

    }
}

可以通过配置文件来修改 单个文件的最大限制,以及多个文件上传每次请求的最大限制.

spring.servlet.multipart.max-file-size=10MB  # 默认为1MB
spring.servlet.multipart.max-request-size=100MB  # 默认为10MB

3、自动配置原理

文件上传自动配置类-MultipartAutoConfiguration----- MultipartProperties

  • 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】 默认只支持标准的Servlet方式,如果是流的方式,则应该自定义个文件上传解析器.

  • 原理步骤

    • 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
    • 2、参数解析器来解析请求中的文件内容封装成MultipartFile ===>本质是一个List集合.
    • **3、将request中文件信息封装为一个Map;**MultiValueMap<String, MultipartFile>

      参数名:MultipartFile集合

FileCopyUtils实现文件流的拷贝

    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
                         @RequestPart("headerImg") MultipartFile headerImg,
                         @RequestPart("photos") MultipartFile[] photos)

RequestPartMethodArgumentResolver 进行MultipartFile 类型参数的解析.

在这里插入图片描述

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);//解析每个参数
#############################################resolveArgument###########################
// name:photos  
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);//获取该参数对应的List<MultipartFile>
##################################resolveMultipartArgument#####################
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) {
    List<MultipartFile> multipartFiles = multipartRequest.getFiles(name);
	return multipartFiles.toArray(new MultipartFile[0]);
}

8、异常处理

1、错误处理

1、默认规则
  • 默认情况下,Spring Boot提供/error处理所有错误的映射

  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据

  • imgimg

  • 要对其进行自定义,添加 View 解析为 error

  • 要完全替换默认行为,可以实现 ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。

  • error/下的4xx,5xx页面会被自动解析;

    • img
2、异常处理自动配置原理
  • ErrorMvcAutoConfiguration 自动配置异常处理规则

    1. 容器中的组件:类型: DefaultErrorAttributes -> id:errorAttributes
      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
      • DefaultErrorAttributes定义错误页面中可以包含哪些数据。
      • img
      • img
    1. 容器中的组件:类型:BasicErrorController --> id:basicErrorControllerv(json+白页 适配响应)
      • 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
      • 容器中有组件 View->id是error;(响应默认错误页)
      • 容器中放组件 BeanNameViewResolver(视图解析器)按照返回的视图名(error)作为组件的id去容器中找View对象。(如果没找到就创建一个默认的error组件.)

        如果是浏览器,显示的是错误页.

在这里插入图片描述

  如果是一些机器客户端,显示的将是Json数据.

在这里插入图片描述

如果想要返回默认error页面;就会找error视图【StaticView】。(默认是一个白页)

在这里插入图片描述

    1. 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
      • 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面,然后进行跳转.
      • error/404、5xx.html

在这里插入图片描述

3、异常处理步骤流程

1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException 接收

2、进入视图解析流程(页面渲染?)

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); (此时mv为null,下列一些列操作最终目的都是为了找到能够解决错误的mv----ModelAndView)

3、mv = processHandlerException()处理handler发生的异常,处理完成返回ModelAndView;

  • 1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器

  • img

  • 2、系统默认的 异常解析器;

  • img

    • 1、DefaultErrorAttributes先来处理异常。把异常信息保存到request域(其他啥都不干),并且返回null;
    • 2、默认没有任何人能处理异常,所以异常会被继续抛出
      • 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
      • 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。 第二步中自动注入的那个组件
      • img

      • 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,拼接成error/500.html
      • 4、模板引擎最终响应这个页面 error/500.html

        在这里插入图片描述

4、定制错误处理逻辑
  1. 自定义错误页

    ​ error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页

  2. @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的

    #########################################GlobalExceptionHandler#####################
    /**
     * 处理整个web controller的异常.
     */
    @Slf4j
    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler({ArithmeticException.class,NullPointerException.class})
        public String headleArithException(Exception e){
            log.error("异常是:{}",e);
            return "login";//返回到login页面
        }
    }
    
    #############################Controller##############################################
    /**
         *
         * @param a  不带请求参数或者参数类型不对  400;Bad Request  一般都是浏览器的参数没有传递正确
         * @return
         */
        @GetMapping("/basic_table")
        public String basic_table(){
    
            int i = 10/0;
            return "table/basic_table";
        }
    
    #####################运行结果#######################################################
     跳转到登录页..
    
  3. @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 **response.sendError(statusCode, resolvedReason);tomcat发送的/error,**然后就按照上面所说被BasicErrorController进行处理…

    sendError作用:本次请求结束,Tomcat底层发送/error…

    ######################################自定义异常#######################################
    /**
     * 自定义异常 用户数量太多
     */
    @ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量太多") //HttpStatus.FORBIDDEN:403
    public class UserToManyException extends RuntimeException{
        public UserToManyException(){}
        public UserToManyException(String message){
           super(message);
        }
    }
    ##################################controller#######################################
    @GetMapping("/dynamic_table")
        public String dynamic_table(Model model){
            //表格内容的遍历
            List <User> users = Arrays.asList(new User("zhagnsan", "123"), new User("lisi", "123456"),
                    new User("wangwu", "789"), new User("zhaoliu", "15651"));
            model.addAttribute("users", users);
            if(users.size()>3){
                throw new UserToManyException();
            }
            return "table/dynamic_table";
        }
    
    #################################运行结果#######################################
     页面显示:用户数量太多的信息...
    

    Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。

在这里插入图片描述

  • response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

    下面是Tomcat原生的错误页.so urgly… 但是Spring boot底层有默认的error页帮我们进行处理.

  • img

  1. 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则

    @Order(value = Ordered.HIGHEST_PRECEDENCE) // 指定组件的优先级,数字越小优先级越高.
    @Component
    public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
    
        @Override
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            try {
                response.sendError(511,"俺喜欢的错误");//
            }catch (Exception e){
                e.printStackTrace();
            }
            return new ModelAndView();
        }
    }
    
    ##############################################运行结果#################################
    页面显示:511,俺喜欢的错误....    
    

    从下图可以看出,CustomerHandlerExceptionResolver被放在了第一位,而它可以拦截任何的错误.

    • img
  2. ErrorViewResolver 实现自定义处理异常 最底层的异常解析器.

    使用该解析器的两种情况:

    • response.sendError 。error请求就会转给basicErrorController

    • 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给basicErrorController

    注:basicErrorController 要去的页面地址是 ErrorViewResolver

9、Web原生组件注入(Servlet、Filter、Listener)

1、使用Servlet API

@ServletComponentScan(basePackages = “com.atguigu.admin”) :指定原生Servlet组件都放在那里

@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器?

@WebFilter(urlPatterns={"/css/*","/images/*"})

@WebListener

测试代码:

@WebServlet(urlPatterns = {"/my"})
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("6666666666");
    }
}

@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("MyFilter初始化完成...");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("MyFilter工作...");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
        log.info("MyFilter销毁...");

    }
}

@WebListener
@Slf4j
public class MyListener implements ServletContextListener  {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("MyListener监听到项目的初始化");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("MyListener监听到项目的销毁");
    }
}

@ServletComponentScan(basePackages = "com.rg.admin")
@SpringBootApplication
public class Boot05AdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(Boot05AdminApplication.class, args);
    }

}
    

2、使用RegistrationBean

ServletRegistrationBean,FilterRegistrationBean, and ServletListenerRegistrationBean


//proxyBeanMethods = true:保证依赖的组件始终是单实例.
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {
    @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet, "/my", "/my02");
    }

    @Bean
    public FilterRegistrationBean myFilter(){
        MyFilter myFilter = new MyFilter();
        //return new FilterRegistrationBean(myFilter,myServlet());//对Servlet的路径进行过滤
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
        return filterRegistrationBean;
    }

    @Bean
    public ServletListenerRegistrationBean myListener(){
        MyListener myListener = new MyListener();
        return new ServletListenerRegistrationBean(myListener);
    }
}

3.扩展:DispatchServlet 如何注册进来

  • 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
  • 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
  • 默认映射的是 / 路径。

img

为啥拦截器不拦截用户向容器中注册的Servlet?

多个Servlet都能处理到同一层路径时,采取精确优选原则

比如: A: /my/ B: /my/1 ==>当发送/my/1请求时A处理,当发送/my/2请求时B处理.

我们测试的是/my 是个具体的路径,最终会被Tomcat处理.只有经过Spring流程的Servlet才会被拦截器拦截.

DispatcherServlet是如何注册进来的之底层原理**

#######################DispatcherServletAutoConfiguration#############################
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)//将容器放置一个dispatcherServlet
		public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
			DispatcherServlet dispatcherServlet = new DispatcherServlet();
			...一系列初始化
			return dispatcherServlet;
		}
//DispatcherServletRegistrationBean继承ServletRegistrationBean,所以这个采取的也是第二种方式.
		@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
		@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
				WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
			DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
					webMvcProperties.getServlet().getPath());
			registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
			registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
			multipartConfig.ifAvailable(registration::setMultipartConfig);
			return registration;
		}

10、嵌入式Servlet容器

1、切换嵌入式Servlet容器

  • 默认支持的webServer

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
  • 切换服务器

img

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>//排除tomcat的依赖.
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  • 原理

    • SpringBoot应用启动发现当前是Web应用。web场景包—将导入tomcat
    • web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂—> Servlet 的web服务器)`
    • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类---->ServletWebServerFactoryAutoConfiguration
    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
    • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
    • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize()—this.tomcat.start();
    • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)

2、定制Servlet容器

  • 实现 **WebServerFactoryCustomizer **

    • 把配置文件的值和** ServletWebServerFactory **进行绑定
  • 修改配置文件 server.xxx

  • 直接自定义 ConfigurableServletWebServerFactory

xxxxxCustomizer:定制化器,可以改变xxxx的默认规则

import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);// 修改端口号
    }

}

11、定制化原理

1、定制化的常见方式

  • 修改配置文件;
  • 编写自定义的配置类 xxxConfiguration+ @Bean替换、增加容器中默认组件;如视图解析器…
  • xxxxxCustomizer; 定制化器.
  • Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer

@EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置(非大佬,请勿使用!);

实现定制和扩展功能

  • 原理

  • 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类:静态资源、欢迎页…

  • 2、一旦使用 @EnableWebMvc 、。会 @Import(DelegatingWebMvcConfiguration.class)

  • 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用

    • 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
    • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
    • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
  • 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

  • 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。

… …

SpringBoot 底层的自动配置(WebMvcAutoConfiguration)…
在这里插入图片描述

2、原理分析套路

场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项

以上笔记参考于 尚硅谷雷神的SpringBoot2开发教程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱编程的大李子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值