Springboot的Web开发(SpringMVC)

大纲

前提:我们引入了Springboot的Web开发场景之后

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

简单功能分析

1.静态资源访问

静态资源目录

  • 只要静态资源放在类路径下: called :/static (or /public or /resources or /META-INF/resources ,就能当作静态资源目录。

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

  • 原理: 静态映射:/**:拦截所有请求

    • 所有请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
  • 通过配置改变默认的静态资源路径如:

    spring:
      mvc:
    	#更改请求前置路径
        static-path-pattern: /res/**
      resources:
      	#更改资源目录2 1···```
        static-locations: [classpath:/haha/,classpath:/hehe/]
    

静态资源访问前缀

默认无前缀(/**),为了拦截器配置方便,官方推荐静态资源加访问前缀。(方便放行静态资源)

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

当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找

webjar

  • 适用webjar,webjar是一个网站,将我们常用的jquery、bootstrap等封装成一个jar包供我们使用

  • 自动映射 /webjars/** (jar包下,也有META-INF/resources目录,所以也可以访问)

  • 例子:如:maven引用方式如下:

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

2.欢迎页支持

官方文档

两种情况

  1. 默认欢迎页:静态资源路径下 index.html

    • 可以配置静态资源路径

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

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

3.自定义 Favicon

名称为:favicon.ico 放在静态资源目录下即可。

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

4.静态资源配置原理

  1. SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)

    1. spring-boot-autoconfigure依赖中org.springframework.boot.autoconfigure.web.servlet下是关于servlet的配置,如下几个常见的servlet配置:
      在这里插入图片描述
    2. 其中WebMvcAutoConfiguration是主要配置SpringMVC
      在这里插入图片描述
  2. 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 {}
    

    导入了相关依赖,容器中就有了相关组件,满足Conditional注解要求,即生效。

  3. 内部有一个配置类:

    @Configuration(proxyBeanMethods = false)
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
            
    }
    
    1. @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })

      1. 配置文件的相关属性和xxx进行了绑定。

        WebMvcProperties==spring.mvc
        ResourceProperties==spring.resources

        @ConfigurationProperties(
            prefix = "spring.mvc"
        )
        public class WebMvcProperties {...}
        ==================================================
        @Deprecated
        @ConfigurationProperties(
            prefix = "spring.resources",
            ignoreUnknownFields = false
        )
        public class ResourceProperties extends Resources {...}
        
    2. 只有一个有参构造器

          //有参构造器所有参数的值都会从容器中确定
      //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;
      }
      
    3. 资源处理的默认规则

      protected void addResourceHandlers(ResourceHandlerRegistry registry) {
      	super.addResourceHandlers(registry);
      	if (!this.resourceProperties.isAddMappings()) {//isAddMappings ==> 配置文件中的add-mappings
      		logger.debug("Default resource handling disabled");
      	} else {
      		ServletContext servletContext = this.getServletContext();
              //webjars的访问规则
      		this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
      		this.addResourceHandler(registry, 			this.mvcProperties.getStaticPathPattern(), (registration) -> {
      registration.addResourceLocations(this.resourceProperties.getStaticLocations());//getStaticPathPattern()默认值:/**  //getStaticLocations()=>默认的四个位置:private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
                  if (servletContext != null) {
                      registration.addResourceLocations(new Resource[]{new ServletContextResource(servletContext, "/")});
                  }
      		});
      	}
      }
      
      private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
      	this.addResourceHandler(registry, pattern, (registration) -> {
      		registration.addResourceLocations(locations);
      	});
      }
      
      private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
      	if (!registry.hasMappingForPattern(pattern)) {
      		ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
      		customizer.accept(registration);
              
      //缓存规则=>可以配缓存
      registration.setCachePeriod(this.getSeconds(this.resourceProperties.getCache().getPeriod()));
      registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
      registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
              
      		this.customizeResourceHandlerRegistration(registration);
      	}
      }
      
      /******************************************************************/
      spring:
        resources:
          add-mappings: false(禁用所有静态资源规则)
          cache:
      		period: 11000 (配缓存策略)
      /*********************************************************************/  
      
    4. 欢迎页的处理规则

      // HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。  
      @Bean
      public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
      	WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
                  		welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
                  welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
      	return welcomePageHandlerMapping;
      }
      
      ============
      	WelcomePageHandlerMapping(TemplateAvailabilityProviders 			templateAvailabilityProviders, 
                                ApplicationContext applicationContext, 
                                Resource welcomePage, String staticPathPattern) {
          	// 此处说明了:要用欢迎页功能,必须是/**
              if (welcomePage != null && "/**".equals(staticPathPattern)) {
                  logger.info("Adding welcome page: " + welcomePage);
                  this.setRootViewName("forward:index.html");
              } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
                  logger.info("Adding welcome page template: index");
                  // 调用Controller 处理 /index
                  this.setRootViewName("index");
              }
      
          }
      
  4. favicon

    • 跟代码没关系了,浏览器默认会自动发送/favicon.ico请求访问项目静态资源下的 favicon
    • 所以如果静态资源访问前缀更改了,自然就找不到了。

请求参数处理

1.请求映射

请求映射:将接收到的请求映射处理器的方法上。

如:

@RequestMapper("/bug.jpg")
public String hello() {
    return "aaa";
}

1.1 rest使用与原理

相关注解:@xxxMapping

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

以前:增删改查

  • /getUser 获取用户 、 /deleteUser 删除用户 、 /editUser 修改用户 、 /saveUser 保存用户

现在:

  • /user

    method = GET 获取用户 、DELETE 删除用户 、 PUT 修改用户 、 POST 保存用户

因为:有一个核心Filter:HiddenHttpMethodFilter

Springboot中Rest用法:

  1. 表单中:method=post,隐藏域 _method=put(实际的请求类型)

    因为默认是filter配置读取_method参数。

  2. SpringBoot中需要手动开启

    1. 底层:因为Springboot自动配置中有以下代码。(WebMvcAutoConfiguration类中)

      @Bean           
      @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
      @ConditionalOnProperty(prefix ="spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
      public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
      	return new OrderedHiddenHttpMethodFilter();
      }
      
      默认enabled为false
      
    2. 所以需要在application文件中选择开启

      spring:
      	mvc:
      		hiddenmethod:
      			filter:
      				enabled: true #开启页面表单的Rest功能,默认是false
      
  3. 处理器映射
    如何把_method 这个名字换成我们自己喜欢的:自定义filter。

    @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-张三";
    }
    
    //自定义filter
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");
        return methodFilter;
    }
    
  4. Rest原理:HiddenHttpMethodFilter类:

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;//原生的请求
        //1.检测请求是否正常,并且是POST
        if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
            //2.获取到_method的值。
            String paramValue = request.getParameter(this.methodParam);//this.methodParam默认值:_method
            if (StringUtils.hasLength(paramValue)) {
                //会把_method的值转换成大写
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                //ALLOWED_METHODS是一个list集合,包括:HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()
                if (ALLOWED_METHODS.contains(method)) {
                    //3.包装模式requestWrapper,包装了原生的请求:重写了getMethod方法,返回的是传入的值。
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }
        //4.过滤器链放行的时候用requestWrapper。以后的方法调用getMethod()是调用requesWrapper的。
        filterChain.doFilter((ServletRequest)requestToUse, response);
    }
    
    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;
    
        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }
    
        //重写了getMethod()方法,返回的是传入的值
        public String getMethod() {
            return this.method;
        }
    }
    //所以HttpMethodRequestWrapper继承了原生HttpServletRequest,是对请求的一个包装。
    public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {...}
    

    基于表单提交要使用REST的时候说明

    1. 表单提交会带上 _method= … (值是小写也没关系,底层会把它转换成大写!)

    2. 请求过来被HiddenHttpMethodFilter拦截

    3. 检测请求是否正常,并且是POST

    4. 获取到 _method的值。

      兼容以下请求;PUTDELETEPATCH

    5. 原生request(post),包装模式requestWrapper重写了getMethod方法,返回的是传入的值。

    6. 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。

    用客户端工具发Rest风格请求: 如PostMan直接发送Put、delete等方式请求,就不会过这个Filter

  5. 派生了新的注解:

        //@RequestMapping(value = "/user",method = RequestMethod.GET) 等同于:
    	@GetMappping("/user")
        public String getUser(){
            return "GET-张三";
        }
    
    ===================================
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @RequestMapping(
        method = {RequestMethod.GET}
    )
    public @interface GetMapping {...}
    ====================================
    
        //@RequestMapping(value = "/user",method = RequestMethod.POST)
    	@PostMapping("/user")
        public String saveUser(){
            return "POST-张三";
        }
    
        //@RequestMapping(value = "/user",method = RequestMethod.PUT)
    	@PutMapping("/user")
        public String putUser(){
            return "PUT-张三";
        }
    
        //@RequestMapping(value = "/user",method = RequestMethod.DELETE)
    	@DeleteMapping("/user")
        public String deleteUser(){
            return "DELETE-张三";
        }
    

1.2 请求映射原理

SpringMVC功能核心:DispatcherServlet类

  1. DispatcherServlet最终会继承于HttpServlet。继承树如下:
    在这里插入图片描述

    1. FrameworkServlet重写了HttpServlet的doGet和doPost方法,

      都用本类的processRequest方法处理: this.processRequest(request, response);
      processRequest内部核心是:this.doService(request, response);,此类中的doService是一个抽象方法。

    2. DispatcherServlet对doService()做了实现。其中核心是:
      this.doDispatch(request, response);。此方法才是SpringMVC最核心的方法,每个请求进来都要调用这个方法。

  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;
            Object dispatchException = null;
    
            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                
                // 找到当前请求使用哪个Handler(Controller的方法)处理
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    this.noHandlerFound(processedRequest, response);
                    return;
                }
                
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                
                String method = request.getMethod();
                boolean isGet = HttpMethod.GET.matches(method);
                if (isGet || HttpMethod.HEAD.matches(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
    
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
    
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
    
                this.applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }
    
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            } else if (multipartRequestParsed) {
                this.cleanupMultipart(processedRequest);
            }
    
        }
    
    }
    
    @Nullable
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        	HandlerMapping:处理器映射规则。/xxx->>xxxx
            if (this.handlerMappings != null) {
                Iterator var2 = this.handlerMappings.iterator();
    			
                //找到对应的处理器映射规则
                while(var2.hasNext()) {
                    HandlerMapping mapping = (HandlerMapping)var2.next();
                    HandlerExecutionChain handler = mapping.getHandler(request);
                    if (handler != null) {
                        return handler;
                    }
                }
            }
    
            return null;
    }
    
    • this.handlerMappings :默认配置有5个:
      在这里插入图片描述

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

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

    • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。

    • 访问/能访问到index.html;

    • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

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

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

2.请求的参数获取

2.1 使用注解

  1. @PathVariable(获取路径变量) }

  2. @RequestHeader(获取请求头)

  3. @ModelAttribute

  4. @RequestParam(获取请求参数)

  5. @MatrixVariable(矩阵变量)

  6. @CookieValue(获取cookie的值)

  7. @RequestBody

  8. @RequestAttribute(获取request域属性)

矩阵变量的使用:

  1. 矩阵变量需要在SpringBoot中手动开启

    1. 方法一: 在这里插入图片描述
    2. 方法二:
      在这里插入图片描述
  2. 根据RFC3986的规范,矩阵变量应当绑定在路径变量中

  3. 若是有多个矩阵变量,应当使用英文符号‘ ; ’进行分隔

  4. 若是一个矩阵变量有多个值,应当使用英文符号‘ , ’进行分隔,或者命名多个重复的key即可

  5. 如:/cars/sell;low=34;brand=byd,audi,yd(sell是路径,后面的才是矩阵量)、/boss/1;age=20/2;age=10

  6. 使用场景:

    1. 页面开发,cookie禁用了,要用到session里面的内容。
      (session依赖于cookie:session.set(a,b)–> jsessionid --> cookie --> 每次发请求携带)
    2. 矩阵变量:url重写:/abc;jsessionid=xxxx 把cookie的值使用矩阵变量的方式进行传递

使用示例:

@RestController
public class ParameterTestController {


    //  car/2/owner/zhangsan
    @GetMapping("/car/{id}/owner/{username}")
    public Map<String,Object> getCar(@PathVariable("id") Integer id,
                                     @PathVariable("username") String name,
                                     @PathVariable Map<String,String> pv,//map泛型一定是String, String,获取所有的值
                                     @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){//可以声明为Cookie类型


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

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


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


    //1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd  第一个分号前的路径,后面才是矩阵变量
    //2、SpringBoot默认是禁用了矩阵变量的功能
    //      手动开启:原理。对于路径的处理:使用UrlPathHelper进行解析。里面有个属性:
    //             		 removeSemicolonContent(移除分号内容)(配成false就可):支持矩阵变量的
    //3、矩阵变量必须有url路径变量才能被解析(如下:{...}
    @GetMapping("/cars/{path}")//注意这里要用 {path}
    public Map carsSell(@MatrixVariable("low") Integer low,
                        @MatrixVariable("brand") List<String> brand,
                        @PathVariable("path") String path){
        Map<String,Object> map = new HashMap<>();

        map.put("low",low);
        map.put("brand",brand);
        map.put("path",path);//结果是:sell
        return map;
    }

    // /boss/1;age=20/2;age=10

    @GetMapping("/boss/{bossId}/{empId}")
    public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
                    @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
        Map<String,Object> map = new HashMap<>();

        map.put("bossAge",bossAge);
        map.put("empAge",empAge);
        return map;

    }

}

2.2 Servlet API

Servlet API:

  1. WebRequest
  2. ServletRequest
  3. MultipartRequest
  4. HttpSession
  5. javax.servlet.http.PushBuilder
  6. Principal
  7. InputStream
  8. Reader
  9. HttpMethod
  10. Locale
  11. TimeZone
  12. ZoneId

例如:

@GetMapping("/goto")
public String goToPage(HttpServletRequest request) {
    request.setAttribute("msg", "成功了...");
    return "forward:/success";
}

原理:

servletAPI的参数解析器: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);
    }

2.3 复杂参数

  1. Map
  2. Model(map、model里面的数据会被放在request的请求域 ,相当于 request.setAttribute)
  3. Errors/BindingResult
  4. RedirectAttributes( 重定向携带数据)
  5. ServletResponse(原生Servlert API里的response)
  6. SessionStatus
  7. UriComponentsBuilder
  8. ServletUriComponentsBuilder

Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据,

原理

…(按参数处理过程走,到找参数解析器的时候)

  1. Map类型的参数,参数解析器是:MapMethodProcessor。

  2. Model类型的参数,参数解析器是:ModelMethodProcessor。

  3. 解析参数:

    1. Map、Model类型的参数,都会返回 mavContainer.getModel()== BindingAwareModelMap (是Model 也是Map,继承或实现了相关接口),从而获取到值。
      在这里插入图片描述
      在这里插入图片描述
  4. 请求参数处理完成后,执行目标方法完成,将所有数据放在ModelAndViewContainer,包含要去的页面地址View,还包含Model。
    在这里插入图片描述

  5. 处理派发结果
    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);
                }
            });
        }
    

2.4 自定义对象参数

@GetMapping("/saveuser")
public String saveUser(Person person) {
    ...
}

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

/**
 *     姓名: <input name="userName"/> <br/>
 *     年龄: <input name="age"/> <br/>
 *     生日: <input name="birth"/> <br/>
 *     宠物姓名:<input name="pet.name"/><br/>
 *     宠物年龄:<input name="pet.age"/>
 */
@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;

}

原理?

…根据参数流程走,到找参数处理器的时候,

  1. 支持自定义对象参数的处理器:ServletModelAttributeMethodProcessor

    怎么判断支持自定义对象参数的处理器:
    if (resolver.supportsParameter(parameter)) {

      public boolean supportsParameter(MethodParameter parameter) {
          return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType());//判断是不是简单数据类型
      }
    

    判断是不是简单数据类型的关键方法:

    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));
    }
    
  2. 解析参数
    在这里插入图片描述

       
    @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中
        在这里插入图片描述

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

    • GenericConversionService:

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

        124个converter
        在这里插入图片描述

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

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

      自定义 Converter,举例:

      <form>
          <!--不使用级联,而是自定义:类名,然后属性值为一个字符串(中间用逗号分割) -->
          <input name="pet" value="啊猫,3"/>
          <input type="submit" value="保存">
      </form>
      
          //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);
                  }
      
                  //此方法可以添加converter
                  @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;
                          }
                      });
                  }
              };
          }
      

3.参数处理原理

在DispatcherServlet中处理请求,…,开始参数处理

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;
        Object dispatchException = null;

        try {
            processedRequest = this.checkMultipart(request);
            multipartRequestParsed = processedRequest != request;
            
            // 找到当前请求使用哪个Handler(Controller的方法)处理
            mappedHandler = this.getHandler(processedRequest);
            if (mappedHandler == null) {
                this.noHandlerFound(processedRequest, response);
                return;
            }

            //为当前Handler 找一个适配器 HandlerAdapter
            HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
            
            String method = request.getMethod();
            boolean isGet = HttpMethod.GET.matches(method);
            if (isGet || HttpMethod.HEAD.matches(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
			
            //实际执行目标方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            this.applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        } catch (Exception var20) {
            dispatchException = var20;
        } catch (Throwable var21) {
            dispatchException = new NestedServletException("Handler dispatch failed", var21);
        }

        this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
    } catch (Exception var22) {
        this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
    } catch (Throwable var23) {
        this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }

    }

}

总的步骤:

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

  2. 为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter(其实就是一个大的反射处理器)

    HandlerAdapter:默认有4个
    在这里插入图片描述
    4个Adapter都对应不同的功能:

    ​ 0 - 支持方法上标注@RequestMapping

    ​ 1 - 支持函数式编程的

    ​ …

  3. 适配器执行目标方法并确定方法参数的每一个值。下面排列为执行顺序:

    //所在类:DispatcherServlet
    
    // Actually invoke the handler.
    //DispatcherServlet -- doDispatch
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    //所在类:RequestMappingHandlerAdapter
    
    mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法
    

    在这里插入图片描述

    //所在类:ServletInvocableHandlerMethod
    
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); //真正执行目标方法
    
    //获取方法的参数值
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    

    在这里插入图片描述
    在这里插入图片描述

    如何获取参数值?

    ============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 {
                    //解析参数:调用各自 HandlerMethodArgumentResolver 的resolveArgument方法即可
                    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;
        }
    
    1. 挨个判断所有参数解析器哪个支持解析这个参数

      ==================HandlerMethodArgumentResolverComposite==================
      @Nullable
      private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
          HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
          if (result == null) {
              Iterator var3 = this.argumentResolvers.iterator();
      
              while(var3.hasNext()) {
                  HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
                  if (resolver.supportsParameter(parameter)) {
                      result = resolver;
                      this.argumentResolverCache.put(parameter, resolver);//缓存起来,第一次会挨个判断,尔后不用挨个判断,直接调map的get方法即可,会快点。
                      break;
                  }
              }
          }
      
          return result;
      }
      

      servletAPI的解析器: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);
       }
      
    2. 解析这个参数的值
      调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可

    3. 目标方法执行完成

    4. 处理结果…

参数解析器

HandlerMethodArgumentResolver

  • 默认配置有26个

    • SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
      在这里插入图片描述
  • 作用:确定将要执行的目标方法的每一个参数的值是什么。

  • 参数解析器的接口:
    在这里插入图片描述

    • 两个方法:
      1. supportsParmeter:当前解析器是否支持解析这种参数
      2. resolveArgument:支持就调用 resolveArgument

返回值处理器

在这里插入图片描述

数据响应与内容协商

在这里插入图片描述

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>

其用的json依赖是:
在这里插入图片描述

加@ResponseBody就能给前端自动返回json数据,如:

@ResponseBody
@GetMapping("/hello")
public String test() {
    return "hello";
}
返回值解析器原理

有一个接口,该接口有两个方法:
在这里插入图片描述

  1. 返回值处理器判断是否支持这种类型返回值 supportsReturnType
  2. 返回值处理器调用 handleReturnValue 进行处理
  3. RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
    1. 利用 MessageConverters 进行处理 将数据写为json

    2. 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
      在这里插入图片描述

    3. 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,

    4. SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?

      1. 得到MappingJackson2HttpMessageConverter可以将对象写为json
      2. 利用MappingJackson2HttpMessageConverter将对象转为json再写出去。

返回值处理器
在这里插入图片描述

处理完请求参数后,处理返回值:

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    //确定请求参数,获取返回值
    Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
    this.setResponseStatus(webRequest);
    if (returnValue == null) {
        if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
            this.disableContentCachingIfNecessary(webRequest);
            mavContainer.setRequestHandled(true);
            return;
        }
    } else if (StringUtils.hasText(this.getResponseStatusReason())) {
        mavContainer.setRequestHandled(true);
        return;
    }

    mavContainer.setRequestHandled(false);
    Assert.state(this.returnValueHandlers != null, "No return value handlers");

    try {
        //在这里处理返回值
        this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
    } catch (Exception var6) {
        if (logger.isTraceEnabled()) {
            logger.trace(this.formatErrorForReturnValue(returnValue), var6);
        }

        throw var6;
    }
}
@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);
}
====================RequestResponseBodyMethodProcessor=============================== 
@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);
    }

SpringMVC到底支持哪些返回值

  1. ModelAndView
  2. Model
  3. View
  4. ResponseEntity
  5. ResponseBodyEmitter
  6. StreamingResponseBody
  7. HttpEntity
  8. HttpHeaders
  9. Callable
  10. DeferredResult
  11. ListenableFuture
  12. CompletionStage
  13. WebAsyncTask
  14. 标注了 @ModelAttribute 且为对象类型的
  15. 标注了@ResponseBody 注解 —> 处理器:RequestResponseBodyMethodProcessor;
HTTPMessageConverter原理

(消息转换器)

  • MessageConverter规范
    在这里插入图片描述
    HttpMessageConverter的作用: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。

    • 例子:Person对象转为JSON。或者 JSON转为Person。(请求/响应)
  • 默认的MessageConverter
    在这里插入图片描述

    能力不同:

    • 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

    • 8 - true

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

最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
在这里插入图片描述

2. 内容协商

内容协商:根据客户端接收能力不同,返回不同媒体类型的数据。

例如:需要返回xml型数据:

  1. 引入xml依赖

     <dependency>
                <groupId>com.fasterxml.jackson.dataformat</groupId>
                <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
    
  2. 使用工具postman分别测试返回json和xml

    • 只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
      在这里插入图片描述
  3. 开启浏览器参数方式内容协商功能

    • 为了方便内容协商,开启基于请求参数的内容协商策略
    spring:
        contentnegotiation:
          favor-parameter: true  #开启请求参数内容协商模式
    
    • 发请求:

    • 基于请求参数的内容协商原理:
      在这里插入图片描述

      1. 确定客户端接收什么样的内容类型;

        底层最终是(获取请求头中的format的值)
        在这里插入图片描述
        ​ getParameterName() = format

      2. 最终进行内容协商返回给客户端接收的类型的值即可。

内容协商原理

  1. 判断当前响应头中是否已经有确定的媒体类型。(MediaType)

  2. 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)
    (假如是application/xml)

    1. contentNegotiationManager 内容协商管理器 默认使用基于请求头的内容协商的策略
      在这里插入图片描述
    • HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
      在这里插入图片描述
  3. 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(例如返回对象:Person)

    • 导入了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()));
              }
      
  4. 找到支持操作Person的converter,把converter支持的媒体类型统计出来。
    在这里插入图片描述

    1. 此时客户端需要(application/xml)。服务端具备能力(以上10种,包括 json、xml)

      浏览器默认优先使用xml类型。

  5. 进行内容协商的最佳匹配媒体类型

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

自定义 MessageConverter

实现多协议数据兼容(json、xml、x-guigu等) 的大体流程如下:

  1. @ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

  2. Processor 处理方法返回值。通过 MessageConverter 处理

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

  4. 内容协商找到最终的 messageConverter

好处:

一个标注了@ResponseBody的方法处理请求时可以根据不同需要响应不同类型数据。例如可以满足如下情况:

  1. 浏览器发请求直接返回xml
  2. 如果是ajax请求返回json
  3. 如果是某app发请求,返回自定义协议数据

如何添加自定义的MessageConverter?

(想要配置SpringMVC的什么功能 ——> 一个入口:给容器中添加一个 WebMvcConfigurer)

@Bean
public WebMvcConfigurer webMvcConfigurer(){
	return new WebMvcConfigurer() {
		@Override
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters){

        }
    }
}

有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。

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


视图解析与模板引擎

视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染(springboot支持有几种模板引擎(有相关的starter))。

springboot支持打包(jar),但jsp不支持在压缩包内编译,所以SpringBoot默认不支持 JSP。

1. 视图解析

在这里插入图片描述

视图解析原理流程

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

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

  3. 任何目标方法执行完成以后都会返回 ModelAndView(含有数据和视图地址)。

  4. 如果方法返回空,默认规则是跳转到与请求名称相同的页面。

  5. processDispatchResult方法 :处理派发结果(页面该如何响应)

    1. render(mv, request, response):进行页面渲染逻辑

      1. 根据方法的String返回值得到 View 对象【View对象定义了页面的渲染逻辑】

        1. 所有的视图解析器尝试是否能根据当前返回值得到View对象
          在这里插入图片描述

          1. 举例返回值: redirect:/main.html 得到了–> RedirectView
        2. RedirectView 如何渲染【重定向到一个页面】

          1. 获取目标url地址
          2. response.sendRedirect(encodedURL);
        3. ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
          在这里插入图片描述
          在这里插入图片描述

      2. view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作

        视图对象的定义:
        在这里插入图片描述

视图解析:

  • 返回值以 forward: 开始:
    • new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
  • 返回值以 redirect: 开始:
    • new RedirectView() --> 底层render方法就是重定向
  • 返回值是普通字符串:
    • new ThymeleafView()—>

可以自定义视图解析器+自定义视图,做到更强大的功能。

2. 模板引擎-Thymeleaf

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模板引擎
  • 面向服务器端(后端)的,比较简单易上手的。
  • 缺点,不适于高并发,当遇到高并发时,应考虑用别的模板引擎或者前后端分离。

基本语法

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

    文本值: ‘one text’ , ‘Another one!’ ,…

    数字: 0 , 34 , 3.0 , 12.3 ,…

    布尔值: true , false

    空值: null

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

  2. 文本操作

    • 字符串拼接: +

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

  3. 数学运算

  • 运算符: + , - , * , / , %
  1. 布尔运算
  • 运算符: and , or
  1. 一元运算:
  • ! , not
  1. 比较运算

    • 比较: > , < , >= , <= ( gt , lt , ge , le )
    • 等式: == , != ( eq , ne )
  2. 条件运算

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

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

    • Default: (value) ?: (defaultvalue)

  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

迭代

<tr th:each="prod : ${prods}">
        <td th:text="${prod.name}">Onions</td>
        <td th:text="${prod.price}">2.41</td>
        <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onions</td>
  <td th:text="${prod.price}">2.41</td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

条件运算

<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>

属性优先级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-woPGuAwl-1627625468799)(D:/笔记/typora_images/1605498132699-4fae6085-a207-456c-89fa-e571ff1663da.png)]

thymeleaf使用

  1. 引入Starter

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    
    1. 自动配置好了thymeleaf

      @Configuration(proxyBeanMethods = false)
      @EnableConfigurationProperties(ThymeleafProperties.class)
      @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
      @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
      public class ThymeleafAutoConfiguration { }
      
    2. 自动配好的策略

      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
      
  2. 页面开发

    <!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>
        <a href="www.atguigu.com" th:href="${link}">去百度</a>  <br/>
        <a href="www.atguigu.com" th:href="@{link}">去百度2</a>
    </h2>
    </body>
    </html>
    

构建后台管理系统

  1. 项目创建
  • thymeleaf、web-starter、devtools、lombok
  1. 静态资源处理
  • 自动配置好了,我们只需要把所有静态资源放到 static 文件夹下
  1. 路径构建
  • th:action="@{/login}"
  1. 模板抽取
  • th:insert/replace/include
  1. 页面跳转

        @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";
            }
    
        }
    
  2. 数据渲染

        @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>
    

拦截器

1. 使用

  1. 编写拦截器

    • 实现HandlerInterceptor 接口。例:

      /**
       * 登录检查
       * 1、配置好拦截器要拦截哪些请求
       * 2、把这些配置放在容器中
       */
      @Slf4j
      public class LoginInterceptor implements HandlerInterceptor {
      
          /**
           * 目标方法执行之前
           * @param request
           * @param response
           * @param handler
           * @return
           * @throws Exception
           */
          @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","请先登录");
      //        re.sendRedirect("/");
              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","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
        }
    }
    

2. 拦截器原理

  1. 根据当前请求,找到HandlerExecutionChain【 可以处理请求的handler以及handler的所有 拦截器】
    在这里插入图片描述

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

    1. 如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle

    2. 如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;

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

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

  5. 倒序执行所有拦截器的postHandle方法。

  6. 前面的步骤有任何异常都会直接倒序触发 afterCompletion

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

在这里插入图片描述

文件上传

1. 使用

  1. 页面表单

    <form method="post" action="/upload" enctype="multipart/form-data">
        <input type="file" name="file"><br>
        <input type="submit" value="提交">
    </form>
    
  2. 文件上传代码

        /**
         * MultipartFile 自动封装上传过来的文件
         * @param email
         * @param username
         * @param headerImg
         * @param photos
         * @return
         */
        @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.getSize(),photos.length);
    
            if(!headerImg.isEmpty()){
                //保存到文件服务器,OSS服务器
                String originalFilename = headerImg.getOriginalFilename();
                headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
            }
    
            if(photos.length > 0){
                for (MultipartFile photo : photos) {
                    if(!photo.isEmpty()){
                        String originalFilename = photo.getOriginalFilename();
                        photo.transferTo(new File("H:\\cache\\"+originalFilename));
                    }
                }
            }
    
    
            return "main";
        }
    

2. 自动配置原理

  1. 文件上传自动配置类:MultipartAutoConfiguration MultipartProperties
  • 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
  1. 原理步骤

    ======================DispatcherServlet=========================================
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
        try {
            try {
                ModelAndView mv = null;
                Object dispatchException = null;
    
                try {
                    processedRequest = this.checkMultipart(request);//判断是不是文件上传请求
                    multipartRequestParsed = processedRequest != request;
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }
    
                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    String method = request.getMethod();
                    boolean isGet = HttpMethod.GET.matches(method);
                    if (isGet || HttpMethod.HEAD.matches(method)) {
                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                            return;
                        }
                    }
    
                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }
    
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }
    
                    this.applyDefaultViewName(processedRequest, mv);
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                } cat
    
    ======================DispatcherServlet=========================================
    protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
        //this.multipartResolver:文件上传解析器
        if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
            if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
                if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
                    this.logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
                }
            } else if (this.hasMultipartException(request)) {
                this.logger.debug("Multipart resolution previously failed for current request - skipping re-resolution for undisturbed error rendering");
            } else {
                try {
                    return this.multipartResolver.resolveMultipart(request);//封装文件上传请求
                } catch (MultipartException var3) {
                    if (request.getAttribute("javax.servlet.error.exception") == null) {
                        throw var3;
                    }
                }
    
                this.logger.debug("Multipart resolution failed for error dispatch", var3);
            }
        }
    
        return request;
    }
    
    1. 请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求

    2. 参数解析器来解析请求中的文件内容封装成MultipartFile
      (参数解析器:RequestPartMethodArgumentResolver)

    3. 将request中文件信息封装为一个Map:MultiValueMap<String, MultipartFile>

    4. FileCopyUtils。实现文件流的拷贝

异常处理

1. 错误处理

1.1 默认规则

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

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

  • 要对其进行自定义

    • error添加View解析
  • 要完全替换默认行为,

    • 可以实现 ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
  • error/下的4xx,5xx页面会被自动解析;
    在这里插入图片描述

1.2 定制错误处理逻辑

  1. 自定义错误页

    • error/404.html error/5xx.html;
    • 默认规则:有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
  2. @ControllerAdvice + @ExceptionHandler处理全局异常;

    举例:

    /**
    * 处理整个web controller的异常
    */
    @Slf4j
    @ControllerAdvice
    public class GlobalExceptionHandler {
        
        @ExceptionHandler({ArithmeticException.class, NullPointerException.class}) //处理指定异常
        public String handleArithException(Exception e) {
            log.error("异常是{}", e);
            return "login";//视图地址,与@RequestMapping的返回类型/返回值用法一样。
        }
    }
    
    1. 底层是 ExceptionHandlerExceptionResolver 支持的
  3. @ResponseStatus 自定义异常 ;

    如:处理用户太多异常

    @ResponseStatus(value= HttpStatus.FORBIDDEN, reason="用户数量太多")
    public class UserTooManyException extends RuntimeException {
        public UserTooManyException(String message) {
            super(message);
        }
    }
    

    在这里插入图片描述

    • 底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息组装成ModalAndView返回;
    • 底层调用 response.sendError(statusCode, resolvedReason) => 也就是让tomcat发送/error
  4. Spring底层的异常,如 参数类型转换异常;

    • DefaultHandlerExceptionResolver 处理框架底层的异常。
      • response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); 发送/error请求,然后被底层的BasicErrorController处理。
    • 所以springboot不会用tomcat的下面这种页面
      在这里插入图片描述
  5. 自定义实现 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 (IOException e) {
                e.printStackTrace();
            }
            return new ModelAndView();
        }
    }
    

    可以看到,优先级为最高:
    在这里插入图片描述

  6. ErrorViewResolver 实现自定义处理异常;

    • response.sendError(…)。error请求就会转给controller
    • 如果你的异常没有任何人能处理。tomcat底层 response.sendError(…)。error请求就会转给controller
    • basicErrorController 要去的页面地址是 根据的是ErrorViewResolver 解析的规则 ;

1.3 异常处理自动配置原理

  • ErrorMvcAutoConfiguration 类: 自动配置异常处理规则

    1. 给容器中放了组件:类型:DefaultErrorAttributes -> id:errorAttributes

      public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
      
      • DefaultErrorAttributes:定义错误页面中可以包含哪些数据(携带数据到错误页面)。
        在这里插入图片描述
        在这里插入图片描述

        常用的数据:

        • message:错误信息
        • status:错误代码
    2. 给容器中放了组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)

      1. 处理默认 /error 路径的请求。(所以配置文件可以改错误请求path:server.error.path=

        @Controller
        @RequestMapping({"${server.error.path:${error.path:/error}}"})
        public class BasicErrorController extends AbstractErrorController {
        
        • 页面响应: new ModelAndView("error", model);
          • 视图解析器往容器中找error视图来解析。(下面配的两个组件:视图解析器找到error视图(View))
      2. 为什么错误页面即可以响应页面也可以响应json数据?

        • 因为此类中有两个方法:

          //写出去json 
          @RequestMapping
              public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
          
          //错误页
          @RequestMapping(
                  produces = {"text/html"}
              )
              public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
          
    3. 给容器中放了组件: View->id是error;(响应默认错误页)

      		@Bean(
                  name = {"error"}
              )
              @ConditionalOnMissingBean(
                  name = {"error"}
              )
              public View defaultErrorView() {
                  return this.defaultErrorView;
              }
      
      private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView();
      
      • 如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
    4. 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。

      		@Bean
              @ConditionalOnMissingBean
              public BeanNameViewResolver beanNameViewResolver() {
                  BeanNameViewResolver resolver = new BeanNameViewResolver();
                  resolver.setOrder(2147483637);
                  return resolver;
              }
      
    5. 给容器中放的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver

      1. 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
      =======================DefaultErrorViewResolver=============================
      public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
              ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
              if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
                  modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);//(String)SERIES_VIEWS.get(status.series())是http状态码
              }
      
              return modelAndView;
          }
      
          private ModelAndView resolve(String viewName, Map<String, Object> model) {
              String errorViewName = "error/" + viewName;//解释了为什么会根据状态码自动跳转到error包下的对应页面
              TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
              return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
          }
      

我们想自定义哪些规则,只要自定义对应的组件即可。

1.4 异常处理步骤流程

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

    =====================DispatcherServlet===========================================
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
  2. 进入视图解析流程(页面渲染)

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

  3. mv = processHandlerException;:处理handler发生的异常,处理完成返回ModelAndView;

    1. 遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【 HandlerExceptionResolver处理器异常解析器】
      在这里插入图片描述

    2. 系统默认的 异常解析器;
      在这里插入图片描述

      1. DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
    3. 默认没有任何人能处理异常(系统默认的异常解析器都处理不了异常),所以异常会被抛出

      1. 如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理

      2. 解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
        在这里插入图片描述

        1. 默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
        2. 模板引擎最终响应这个页面 error/500.html

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

以前使用原生的组件时,配置写在web.xml中,现在看看Springboot的用法吧。

1. 使用Servlet API

  • @ServletComponentScan(basePackages = "com.atguigu.admin")指定原生Servlet组件都放在那里。如:

    @ServletComponentScan(basePackages = "com.atguigu.admin") 
    @SpringBootApplication
    public class BootWebApplication {
        public static void main(String[] args) {
            SpringApplication.run(BootWebApplication.class, args);
    	}
    }
    
  • @WebServlet、@WebFilter、@WebListener是Servlet3.0引入的注解。

  • @WebServlet:用于标注原生Servlet。如:

    @WebServlet(urlPatterns = "/my") //@WebServlet是Servlet3引入的注解
    public class MyServlet extends HttpServlet {
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.getWriter().write("6666");
        }
    }
    
    • 效果:直接响应,没有经过Spring的拦截器?
  • @WebFilter:标注原生Filter。如:

    @WebFilter(urlPatterns = "/css/*") //*是Servlet的写法,**是Spring的写法
    public class MyFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            
        }
    
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    }
    
  • @WebListener

扩展:DispatchServlet 如何注册进来?(DispatchServlet 注入原理)

  1. Springboot启动时会开启许多自动配置类,其中就有DispatcherServletAutoConfiguration。

    @AutoConfigureOrder(-2147483648)
    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnWebApplication(
        type = Type.SERVLET
    )
    @ConditionalOnClass({DispatcherServlet.class})
    @AutoConfigureAfter({ServletWebServerFactoryAutoConfiguration.class})
    public class DispatcherServletAutoConfiguration {
    
    1. 给容器中放了DispatcherServlet,并将DispatcherServlet 相关属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。

      @Bean(
          name = {"dispatcherServlet"}
      )
      public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
          DispatcherServlet dispatcherServlet = new DispatcherServlet();
          dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
          dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
          dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
          dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
          dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
          return dispatcherServlet;
      }
      
    2. 通过 ServletRegistrationBean<DispatcherServlet> 把 DispatcherServlet 配置进来。

      @Bean(
          name = {"dispatcherServletRegistration"}
      )
      @ConditionalOnBean(
          value = {DispatcherServlet.class},
          name = {"dispatcherServlet"}
      )
      public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
          DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());//webMvcProperties.getServlet().getPath()) => /
          registration.setName("dispatcherServlet");
          registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
          multipartConfig.ifAvailable(registration::setMultipartConfig);
          return registration;
      }
      
      public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet> implements DispatcherServletPath {
      

      默认映射的是 / 路径。

在这里插入图片描述

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

映射A: /my/

映射B: /my/1

如果传进来请求是/my/1,优选映射B

2. 使用RegistrationBean

ServletRegistrationBean,

FilterRegistrationBean,

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());
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
        return filterRegistrationBean;
    }

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

嵌入式Servlet容器

springboot应用启动无须外置服务器。默认启动自带的tomcat。

1. 切换嵌入式Servlet容器

  • 默认支持的webServer:

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
  • 切换服务器
    在这里插入图片描述

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <!--首先要排除tomcat场景-->
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--然后引入其他服务器场景,如undertow-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    

原理

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

2. 定制Servlet容器

方式:

  1. 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>

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

  3. 直接自定义 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);
    }

}

定制化原理

1. 定制化的常见方式

  1. 修改配置文件;(常用。全局配置)

  2. xxxxxCustomizer;(定制化器)

  3. 编写自定义的配置类 xxxConfiguration + @Bean :替换、增加容器中默认组件;例如:视图解析器(增加)

  4. Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件(常用。全局配置)

    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer {
        
        @Bean //给容器中替换或者扩展一些组件
        ...
    }
    
  5. 修改WebMvcRegistrations

  6. 在实现WebMvcConfigurer 的自定义配置类上加@EnableWebMvc(类里再配合@Bean的使用) :

  • 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能

原理

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

  2. 一旦使用 @EnableWebMvc ,会 @Import(DelegatingWebMvcConfiguration.class)

  3. DelegatingWebMvcConfiguration 的 作用:(只保证SpringMVC最基本的使用)

    1. 把所有系统中的 WebMvcConfigurer 拿过来。(所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效)

    2. 自动配置了一些非常底层的组件。(如RequestMappingHandlerMapping)、这些组件依赖的组件都是从容器中获取

    3. WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

      • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
        
      • 所以 @EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。

  4. … …

2. 原理分析套路

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值