【SpringBoot】web 开发

1. 简单功能分析

1.1 静态资源访问

1、静态资源目录

当前项目类路径下: /static/public/resourcesMETA/resources 都默认作为静态资源目录

如下示例:

在这里插入图片描述

在中四个静态资源目录中放入四张图片,然后直接启动项目,看能否直接访问到静态资源:

  • http://localhost:8080/bug.jpg
  • http://localhost:8080/time.jpg
  • http://localhost:8080/dog.jpg
  • http://localhost:8080/cat.jpg

结果都能访问到,说明这四个目录下的文件都可以作为静态资源读取

那如果静态资源路径和请求路径冲突时会如何?

我们新建个 Controller

@RestController
public class HelloController {
    @RequestMapping("/bug.jpg")
    public String hello() {
        return "aaaaa";
    }
}

重启后,访问 http://localhost:8080/bug.jpg 时,界面如下:

在这里插入图片描述

原理

默认静态资源映射的是 /**,动态请求默认也会拦截所有请求. 请求进来,先去找 Controller 看能不能处理. 不能处理的所有请求又都交给静态资源处理器. 静态资源就回去上面的四个静态目录中查找,静态资源也找不到,就会返回 404.

我们还可以改变默认的静态资源目录位置:

spring:
  web:
    resources:
      static-locations: classpath:/haha
      # static-locations: [classpath:/haha, classpath:/hehe/] 设置多个静态资源目录时的列表写法

2、静态资源访问前缀

默认无前缀.

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

以后访问地址就是: http://localhost:8080/res/bug.jpg

方便拦截器编写,放行带访问前缀的请求

3、WebJars

还有一种特殊的静态资源目录的映射:/webjars/**

官方地址:https://www.webjars.org/

把 js 文件打成 jar 包的形式,和 java 库统一格式被 Maven 管理:

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

此时我们访问 http://localhost:8080/webjars/jquery/3.6.0/jquery.js:

可以看到可以访问到 jquery.js 文件

在这里插入图片描述

1.2 欢迎页支持

1、静态资源路径下 index.html

我们在静态资源目录下新建一个 index.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>飞鸟尽,良弓藏,狡兔死,走狗烹</h1>
</body>
</html>

此时访问 http://localhost:8080:

在这里插入图片描述

可以配置静态资源路径,但是不能配置前缀,否则导致欢迎页失效

2、controller 能处理的 /index 请求

可以转发或重定向到某个页面

1.3 自定义 Favicon

将网站图标命名为 favicon.ico 放到静态资源目录中,会自动解析该图标为站点图标

在这里插入图片描述

最好编译之前 maven clean 一下,不然有时候会有改了但是不起作用的情况

1.4 静态资源配置原理

  • SpringBoot 启动默认加载 XXXAutoConfiguration (自动配置类)

  • Spring MVC 功能的自动配置类大多集中在 WebMvcAutoConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
            ValidationAutoConfiguration.class })
    public class WebMvcAutoConfiguration {}
    
  • 给容器中配了什么

    @Configuration(proxyBeanMethods = false)
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({ WebMvcProperties.class,
                                    org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
    
  • 配置文件的相关属性和 XX 进行了绑定:WebMvcProperties(prefix = "spring.mvc")ResourceProperties(prefix = "spring.resources")

1、静态内部类(也是配置类)只有一个有参构造器:

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

2、静态资源处理的默认规则

// WebMvcAutoConfiguration 类中
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
   super.addResourceHandlers(registry);
   if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
   ServletContext servletContext = getServletContext();
   addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
   addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
      registration.addResourceLocations(this.resourceProperties.getStaticLocations());
      if (servletContext != null) {
         registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
      }
   });
}

我们在该方法一开始打上断点:

在这里插入图片描述

可以看到,关键在于 isAddMappings() 返回的是 true 还是 false,如果是 true,则执行后面的默认配置,否则直接返回.

然后我们进入该方法可以看到:

在这里插入图片描述

它返回的其实就是个属性,默认为 true

在这里插入图片描述

与配置文件中 spring.web.resources.add-mappings 一一对应:

在这里插入图片描述

设为 false 后将禁用静态资源的范围.

下面看看默认的静态资源处理的默认规则,首先获取 Servlet 上下文环境:

在这里插入图片描述

然后首先注册处理 webjars 的相关规则:

在这里插入图片描述

进入该方法:

在这里插入图片描述

可以看到内部调用了同名的重载方法,多传了一个 lambda 表达式:

(registration) -> registration.addResourceLocations(locations)

进入该重载方法:

在这里插入图片描述

如果对该 pattern 注册过 handler 了,则直接返回,否则注册一个新的 handler 来处理该 pattern 对应的静态资源请求.

这里没注册过,所以会继续执行:

在这里插入图片描述

进入 addResourceHandler() 方法内部可以发现:

在这里插入图片描述

其实就是创建资源处理器实例,里面主要是封装的 pattern 信息,该处理器在接收到 requests 时自动匹配处理.

pattern 可以是一个或多个,只要处理的过程是一样的就行.

这里第二行对应前面的 registry.hasMappingForPattern(pattern) 方法,注册进去了才能找得到. 我们可以进入该方法内部看看:

在这里插入图片描述

创建完 /webjars/** 对应的资源处理器后,继续执行:

在这里插入图片描述

这里是一个函数式接口 Consumeraccept() 方法,该方法简单来说,就是消费(使用)一个东西. 具体执行的函数过程是传进来 lambda 表达式:

(registration) -> registration.addResourceLocations(locations)

我们前面注册了资源处理器,但是它的资源路径并没有封装进去,这里就是将指定类型的请求映射到本地资源路径地址前缀,具体来说,这里是:

/webjars/**   ==>   classpath:/META-INF/resources/webjars/

下一步是设置该资源在浏览器的缓存时间,配置文件中可以自定义该时间:

spring:
  web:
    resources:
      cache:
        period: 1234

在这里插入图片描述

然后,为了兼容性,将缓存控制转换为 Http 的缓存控制:

在这里插入图片描述

上面都是默认配置,下面看看有没有个性化的配置,有则覆盖:

在这里插入图片描述

下面继续,看静态资源路径的配置规则:

在这里插入图片描述

可以发现,整体流程和处理 webjars 的配置规则大体是一样的:

  • 首先,pattern/webjars/** 变为 this.mvcProperties.getStaticPathPattern(),这个是个什么玩意儿呢?其实点进去之后发现就是 /**,而且这里适合配置文件绑定的:

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

    这里的赋值会覆盖默认的 /**其实就更改了静态资源访问前缀!

  • 其次,传入的 lambda 表达式不同,这里传进去的处理过程是:

    (registration) -> {
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());
        if (servletContext != null) {
            registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
        }
    }
    

具体过程继续 debug,进入到 addResourceHandler() 方法内:

在这里插入图片描述

一样的先判断容器中是否已经存在该请求路径对应的处理器(注意,拦截器默认也是 /**,优先 Controller 处理,没有才到这):

在这里插入图片描述

然后创建对应 static-path-pattern (默认 /**)的资源处理器:

在这里插入图片描述

下一步就是利用消费者接口执行 lambda 表达式的方法了:

在这里插入图片描述

这一步是给该资源处理器添加本地要映射的路径,默认是:

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

优先级就是源码里这四个位置的顺序

再下面还会判断 Servlet 上下文是否为空,如果不为空,还要给它映射路径添加一个 /,这也就是为什么Controller 能处理资源路径请求的原因,这里注册进去了,而且优先级高!

在这里插入图片描述

之后也是一样的配置缓存,个性化定制覆盖默认配置等.

3、欢迎页的处理规则

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

这里需要了解 HadddlerMapping,详细内容见 https://e-thunder.blog.csdn.net/article/details/113699510.

简单来说,它就是处理映射器,保存了每一个 Handler 能处理哪些请求.

在这里插入图片描述

可以看到首先 new 一个 WelcomePageHandlerMapping,有一个参数是 this.mvcProperties.getStaticPathPattern(),这个 staticPathPattern 默认是 /**,它也与配置文件关联,可以配置如下参数覆盖原配置:

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

这个在之前也说过,就是更改静态资源访问前缀.

我们进入该构造方法中:

在这里插入图片描述

可以看到,如果找到 index.html 并且 static-path-pattern=/** 才能启动欢迎页,这里底层写死了,所以就解释为啥使用访问前缀时,无法启动欢迎页了,但是,也不绝对,从代码上看,如果存在欢迎页模板,也可以启动(实际上是调用 Controller 处理),这里等后面看到了再研究.

4、favicon

这个是浏览器默认的使用当前项目下的 favicon:

项目地址/favicon.*

如果加了静态访问前缀:

项目地址/访问前缀/favicon.*

则自然找不到.

2. 请求参数处理

2.1 请求映射

1、Rest 原理

以前的方式是通过 URI 来区分请求的类别:

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

现在我们可以通过 HTTP 的请求方式来区分:

/user    GET-获取用户    
/user    DELETE-删除用户     
/user    PUT-修改用户      
/user    POST-保存用户

其核心是 HiddenHttpMethodFilter

因为表单中的提交方法只有 GET 和 POST,如果想提交 PUT 和 DELETE,需要利用其它手段.

在表单中,我们需要加一个隐藏域

<input name="_method" type="hidden" value="方法"/>

这里方法就是 DELETEPUT,但是,必须得开启这个功能:

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true

因为:

@Bean
// 没有自定义配置,SpringBoot 会帮你配
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
// 这个属性为 true 才开启,默认是 false,所以当然要人为开启了
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
}

例如下面的 HTML:

<form action="/user" method="get">
    <input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="POST">
    <input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input name="_method" type="hidden" value="DELETE"/>
    <input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input name="_method" type="hidden" value="PUT"/>
    <input value="REST-PUT 提交" type="submit"/>
</form>

表示的就是表单提交的四种请求方式.

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

  • 表单提交会带上 _method=PUT
  • 请求过来会被 HiddenHttpMethodFilter 拦截
    • 判断请求是不是 POST 方式并且不包含错误
      • 如果都满足则可以获取到 _method 的值,如果该值有长度(就是不为空),则先将其全部转为大写字母
      • 兼容以下请求:PUTDELETEPATCH,当传入的请求在这三个允许的请求之一
      • 原生 request(post),包装模式 HttpMethodRequestWrapper 重写了 getMethod() 方法,返回的是传入的值
      • 此后,包装过的请求的请求方式变成 _method 的传入的请求方式的值.
// HiddenHttpMethodFilter 类
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
	
    // 原生 request
    HttpServletRequest requestToUse = request;
	
    // 请求是 POST 且没有错误
    if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
        // 获取 _method 的值
        String paramValue = request.getParameter(this.methodParam);
        // 如果 _method 的值不为空
        if (StringUtils.hasLength(paramValue)) {
            // 转换大写
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            // _method 的值在 PUT、DELETE 和 PATCH 之一时,执行包装方法
            if (ALLOWED_METHODS.contains(method)) {
                // 该包装方法将请求方式替换为 _method 里设置的方式
                requestToUse = new HttpMethodRequestWrapper(request, method);
            }
        }
    }
    // 此时继续执行的请求就是包装后的请求了
    filterChain.doFilter(requestToUse, response);
}

如何改变默认的 _method 呢?例如改为 _m 也能实现一样的功能:

<form action="/user" method="post">
    <input name="_m" type="hidden" value="PUT"/>
    <input value="REST-PUT 提交" type="submit"/>
</form>

我们可以回头看看:

在这里插入图片描述

容器中没有配置 HiddenHttpMethodFilter 组件才执行,而 HiddenHttpMethodFilter 类中,默认定义的是:

在这里插入图片描述

所以我们可以自己放一个 HiddenHttpMethodFilter 来覆盖原有默认配置:

@Configuration(proxyBeanMethods = false)
public class WebConfig {
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
        val hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        hiddenHttpMethodFilter.setMethodParam("_m");
        return hiddenHttpMethodFilter;
    }
}

2、请求映射原理

SpringBoot 底层也是 SpringMVC,所以核心也是 DispatcherServlet,之前在 SpringMVC 的学习笔记里记录了从浏览器发送请求到响应的全过程,这里再重新过一遍增加记忆.

首先我们可以看看 DispatcherServlet 的继承树:

在这里插入图片描述

它实际上也是个 Servlet,所以必然会重写 doGet()doPost() 等方法:

在这里插入图片描述

可以看到,这些方法是在 FrameworkServlet 类中重写的,并且重写的方法体中都调用了同一个函数 processRequest(request, response),我们进入该函数看看:

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
	
    // 初始化过程
    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;

    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    initContextHolders(request, localeContext, requestAttributes);

    try {
        doService(request, response);  // 核心
    }
    catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }

    finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        logResult(request, response, failureCause, asyncManager);
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}

可以看到核心就是调用 doService() 方法,而在 FrameworkServlet 中,该方法是抽象方法:

在这里插入图片描述

所以肯定是调用它的重写方法,即该方法在 DispatcherServlet 中. 而在 doService() 方法中,去除初始化的一些代码,核心是:

在这里插入图片描述

即调用 doDispatch() 方法.

至此我们可以得出结论:SpringMVC 的功能分析都从 DispatcherServletdoDispatch() 方法开始.

我们在这个方法上打上断点,以测试前面 Rest 测试的 GET 请求(/user)为例:

在这里插入图片描述

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 的哪个方法来处理当前请求
         mappedHandler = getHandler(processedRequest);
	// ......
}

进入 getHandler() 方法:

在这里插入图片描述

可以发现,它是通过 HandlerMapping(处理器映射) 来寻找当前请求由谁处理(for 循环匹配),默认有 5 个 HandlerMapping.

RequestMappingHandlerMapping:保存了所有@RequestMapping(请求) 和 handler(处理器) 的映射规则.

这里也反应了首页的欢迎页为甚可以访问到,因为首先会去在 RequestMappingHandlerMapping 里找,但是没人能处理 /,所以会再次进入到 WelcomePageHandlerMapping 寻找对应的 handler:

在这里插入图片描述

我们也可以自定义 HandlerMapping

2.2 普通参数与基本注解

1、注解

  • @PathVariable:路径变量

    @RestController
    public class ParameterTestController {
        @GetMapping("/car/{id}/owner/{username}")
        public Map<String, Object> getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, // Rest 风格绑定参数
                                          @PathVariable Map<String, String> kv) {  // 也可以直接用一个 Map<String,String>接收所有 Rest 变量
            Map<String, Object> map = new HashMap<>();
            map.put("id", id);
            map.put("name", name);
            map.put("kv", kv);
            return map;
        }
    }
    

    此时访问 http://localhost:8080/car/1/owner/ice,可以看到:

    在这里插入图片描述

  • @RequestHeader:获取请求头

    @RestController
    public class ParameterTestController {
        @GetMapping("/car")
        public Map<String, Object> getCar(@RequestHeader("User-Agent") String userAgent, // 可以获取指定请求头的内容
                                          @RequestHeader Map<String,String> header) { //  也可以直接用一个 Map<String,String>接收所有请求头的键值对
            Map<String, Object> map = new HashMap<>();
            map.put("userAgent", userAgent);
            map.put("header", header);
            return map;
        }
    }
    

    此时访问 http://localhost:8080/car,可以看到:

    在这里插入图片描述

  • @RequestParam:获取请求参数

    @RestController
    public class ParameterTestController {
        @GetMapping("/car")
        public Map<String, Object> getCar(@RequestParam("age") Integer age, @RequestParam("sex") String sex,
                                          @RequestParam("inters") List<String> inters,
                                          @RequestParam Map<String, String> params) {
            Map<String, Object> map = new HashMap<>();
            map.put("age", age);
            map.put("sex", sex);
            map.put("inters", inters);
            map.put("params", params);
            return map;
        }
    }
    

    此时访问 http://localhost:8080/car?age=18&sex=%E7%94%B7&inters=Basketball&inters=game,可以看到:

    在这里插入图片描述

    注意,使用 Map 整体接收参数时,inters 只保存了一个值

  • @CookieValue:获取 Cookie 的值

    @RestController
    public class ParameterTestController {
        @GetMapping("/car")
        public Map<String, Object> getCar(@CookieValue("_ga") String _ga,
                                          @CookieValue("_ga") Cookie cookie) {
            Map<String, Object> map = new HashMap<>();
            map.put("_ga", _ga);
            map.put("cookie", cookie);
            return map;
        }
    }
    

    此时访问 http://localhost:8080/car,可以看到:

    在这里插入图片描述

  • @RequestBody:获取请求体

    这个是针对 POST 请求的.

    【HTML】

    <form action="/save" method="post">
        测试@RequestBody获取数据 <br/>
        用户名:<input name="userName"/> <br>
        邮箱:<input name="email"/>
        <input type="submit" value="提交"/>
    </form>
    

    【Controller】

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

    我们通过表单提交来测试:

    在这里插入图片描述

    结果为:

    在这里插入图片描述

  • @RequestAttribute:获取 request 域属性

    @Controller
    public class RequestController {
    
        @GetMapping("/goto")
        public String goToPage(HttpServletRequest request) {
            request.setAttribute("msg", "成功了");
            request.setAttribute("code", 200);
            return "forward:/success";
        }
    
        @ResponseBody
        @GetMapping("/success")
        public Map<String, Object> success(@RequestAttribute("msg") String msg,
                                           @RequestAttribute("code") Integer code,
                                           HttpServletRequest request) {
            Object msg1 = request.getAttribute("msg");
            Object code1 = request.getAttribute("code");
            Map<String, Object> map = new HashMap<>();
            map.put("requestMethod_msg",msg1);
            map.put("annotation_msg",msg);
            map.put("requestMethod_code",code1);
            map.put("annotation_code",code);
            return map;
        }
    }
    

    此时访问 http://localhost:8080/goto,可以看到:

    在这里插入图片描述

  • @MatrixVariable:矩阵变量

    传统请求:/cars/{path}?xxx=xxx&aaa=aaa

    矩阵变量:/cars/{path;low=34,brand=byd,audi,yd}

    <a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a>
    <a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a>
    <a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>
    

    SpringBoot 默认禁用矩阵变量功能,需要手动开启:

    @Configuration(proxyBeanMethods = false)
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            UrlPathHelper urlPathHelper = new UrlPathHelper();
            urlPathHelper.setRemoveSemicolonContent(false); // 不移除分号后面的内容
            configurer.setUrlPathHelper(urlPathHelper);
        }
    }
    

    【Controller】:

    @GetMapping("/cars/{path}")
    public Map carsSell(@MatrixVariable("low") String low,
                        @MatrixVariable("brand") List<String> brand,
                        @PathVariable("path") String path) {
        Map<String, Object> map = new HashMap<>();
        map.put("low", low);
        map.put("brand", brand);
        map.put("path", path);
        return map;
    }
    

    访问 URL:http://localhost:8080/cars/sell;low=34;brand=byd,audi,yd:

    在这里插入图片描述

    访问 URL:http://localhost:8080/cars/sell;low=34;brand=byd;brand=audi;brand=yd:

    在这里插入图片描述

    还有一种麻烦的情况,访问:http://localhost:8080/boss/1;age=20/2;age=10:

    此时 Controller 应该这么写:

    @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、Servlet API

WebRequestServletRequestMultipartRequestHttpSessionjavax.servlet.http.PushBuilderPrincipalInputStreamReaderHttpMethodLocaleTimeZoneZoneId 类型都可以得到支持,是在 ServletRequestMethodArgumentResolver 类的 supportsParameter() 方法判断的:

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

3、复杂参数

MapErrorsBindingResultModelRedirectAttributesServletResponseSessionStatusUriComponentsBuilderServletUriComponentsBuilder

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

【例】

@GetMapping("/params")
public String testParam(Map<String, Object> map,
                        Model model,
                        HttpServletRequest request,
                        HttpServletResponse response) {

    map.put("hello", "world666");
    model.addAttribute("world", "hello666");
    request.setAttribute("message", "hello world");
    Cookie cookie = new Cookie("c1", "v1");
    response.addCookie(cookie);
    return "forward:/success";
}

@ResponseBody
@GetMapping("/success")
public Map<String, Object> success(HttpServletRequest request) {
    Object hello = request.getAttribute("hello");
    Object world = request.getAttribute("world");
    Object message = request.getAttribute("message");


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


    map.put("hello", hello);
    map.put("world", world);
    map.put("message", message);
    return map;
}

结果:

在这里插入图片描述

在这里插入图片描述

4、自定义对象参数

自定义的 POJO 等

2.3 参数处理原理

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

以下面这个请求为例:

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


    map.put("age", age);
    map.put("inters", inters);
    map.put("params", params);
    map.put("_ga", _ga);
    System.out.println(cookie.getName() + "===>" + cookie.getValue());
    return map;
}

访问 URL 是:http://localhost:8080/car/1/owner/ice?age=18&inters=baksetball&inters=game

前的面步骤已经基介绍过了,这里从下图所示位置开始看:

在这里插入图片描述

这里返回了用来处理该请求的 HandlerAdapter

然后执行拦截器方法 mappedHandler.applyPreHandle(processedRequest, response),没有问题才执行真正的处理方法:

在这里插入图片描述

进入该方法:

在这里插入图片描述

再进入内部调用的方法,也就是进入了 RequestMappingHandlerAdapterhandleInternal() 方法:

在这里插入图片描述

进入该方法:

在这里插入图片描述

默认有 27 个参数解析器:

在这里插入图片描述

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

SpringMVC 的目标方法能写多少种参数类型,就取决于这里有多少种参数解析器!

这些参数解析器实际上实现自一个接口:

在这里插入图片描述

它首先通过 supportsParameter() 判断是否支持这种参数,如果支持,则执行 resolveArgument() 进行解析

与此同时,紧接着后面的是返回值处理器,能决定目标方法能写多少种返回值:

在这里插入图片描述

可以看到,默认有 15 种返回值处理器:

在这里插入图片描述

再往下,看真正执行目标方法的地方:

在这里插入图片描述

进入该方法:

在这里插入图片描述

通过 debug 可以发现 invokeForRequest() 才是真正执行目标方法的地方,我们进入该方法:

public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                               Object... providedArgs) throws Exception {
    // 获取所有方法参数
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    // 反射调用目标方法
    return doInvoke(args);
}

我们进入 getMethodArgumentValues() 看它如何获取的:

// InvocableHandlerMethod 类
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
                                           Object... providedArgs) throws Exception {
    // 获取方法所有参数的详细信息,包括注解
    MethodParameter[] parameters = getMethodParameters();
    // 判断参数是否为空,如果为空当然不用处理,直接返回
    if (ObjectUtils.isEmpty(parameters)) {
        return EMPTY_ARGS;
    }

    // 声明 Object 数组长度与参数列表长度一致
    Object[] args = new Object[parameters.length];
    // for 循环中的过程就是如何根据参数信息,解析出实参
    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;
        }
        // 判断现有的参数解析器是否支持这种参数类型
        // 其内部是通过增强 for 循环遍历所有的解析器,来判断是否支持
        // 核心代码在 HandlerMethodArgumentResolverComposite 类的 getArgumentResolver()方法
        if (!this.resolvers.supportsParameter(parameter)) {
            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        }
        try {
            // 解析变量的值
            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        }
        catch (Exception ex) {
            // Leave stack trace for later, exception may actually be resolved and handled...
            if (logger.isDebugEnabled()) {
                String exMsg = ex.getMessage();
                if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                    logger.debug(formatArgumentError(parameter, exMsg));
                }
            }
            throw ex;
        }
    }
    return args;
}

目标方法执行完,将所有的数据都放在 ModelAndViewContainer,包含要去的页面地址 View,还包含 Model 数据

最后要处理派发结果:

在这里插入图片描述

3. 内容协商

根据客户端接收能力不同,返回不同媒体类型的数据.

1、支持返回 xml 的依赖

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

2、原理

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

  2. 获取客户端支持接收的内容类型(获取客户端请求头 Accept 字段)

  3. 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

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

  5. 客户端需要【application/xml】,服务端能力【10种、json、xml】

    在这里插入图片描述

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

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

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

为了方便内容协商,开启基于请求参数的内容协商功能

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

此时,访问 http://localhost:8080/test/person 默认返回:

在这里插入图片描述

访问 http://localhost:8080/test/person?format=xml 返回:

在这里插入图片描述

访问 http://localhost:8080/test/person?format=json 返回:

在这里插入图片描述

4、自定义 MessageConverter

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

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

4. 模板引擎 Thymeleaf

4.1 基本语法

1、表达式

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

2、字面量

  • 文本值
  • 数字
  • 布尔值(true,false)
  • 空值(null)
  • 变量(不能有空格)

3、文本操作

字符串拼接:+

变量替换:the name is ${name}

4、数学运算

+-*/%

5、布尔运算

andor!not

6、比较运算

><>=<===!=

7、条件运算

If-then: (if) ? (then)

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

Default: (value) ?: (defaultvalue)

8、特殊操作

无操作:_

4.2 设置属性值-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.3 迭代

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

4.4 条件运算

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

4.5 属性优先级

在这里插入图片描述

4.6 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 {}

自动配好的策略:

  • 所有 Thymeleaf 的配置值都在 ThymeleafProperties

    在这里插入图片描述

    前缀、后缀、编码都已经指定好了

  • 配置好了 SpringTemplateEngine

  • 配置好了 ThymeleafViewResolver

  • 我们只需要直接开发页面

3、页面开发

【success.html】

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
    <a href="http://www.atguigu.com" th:href="${link}">去百度</a> <br>
    <a href="http://www.atguigu.com" th:href="@{link}">去百度</a>
</h2>
</body>
</html>

注意,要引入命名空间:<html lang="en" xmlns:th="http://www.thymeleaf.org">

【ViewController】

@Controller
public class ViewTestController {

    @RequestMapping("/nuist")
    public String nuist(Model model){
        model.addAttribute("msg","你好");
        model.addAttribute("link","https://www.baidu.com");
        return "success";
    }
}

启动项目,访问:http://localhost:8080/nuist:

在这里插入图片描述

检查网页源代码:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>你好</h1>
<h2>
    <a href="https://www.baidu.com">去百度</a> <br>
    <a href="link">去百度</a>
</h2>
</body>
</html>

可以看到,@{} 传入的只是字符串,它是相对项目根路径访问地址,比如这里是 http://localhost:8080/link

5. 视图解析原理

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

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

3、任何目标方法执行完以后都会返回 ModelAndView 对象(数据和视图地址)

4、doDispatch() 中的 processDispatchResult() 处理派发结果(页面如何响应)

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                   @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
                                   @Nullable Exception exception) throws Exception {

    // ...

    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response); // 渲染视图
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }
    // ...
}

可以看到,其内部是调用 render() 方法来渲染视图的:

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
   // ...

   View view;
   String viewName = mv.getViewName(); // 得到 Controller 返回的视图字符串,如:redirect:/main.html
   if (viewName != null) {
      // 利用视图解析器解析视图名,返回 View 对象
      view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
      if (view == null) {
         throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
               "' in servlet with name '" + getServletName() + "'");
      }
   }
   else {
      // No need to lookup: the ModelAndView object contains the actual View object.
      view = mv.getView();
      if (view == null) {
         throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
               "View object in servlet with name '" + getServletName() + "'");
      }
   }

   // ...
   try {
      if (mv.getStatus() != null) {
         response.setStatus(mv.getStatus().value());
      }
       // 调用 view 的 render 方法来这真正渲染视图
      view.render(mv.getModelInternal(), request, response);
   }
   catch (Exception ex) {
      if (logger.isDebugEnabled()) {
         logger.debug("Error rendering view [" + view + "]", ex);
      }
      throw ex;
   }
}

下面先看看 resolveViewName() 方法是如何根据视图名解析视图的:

在这里插入图片描述

可以发现,是遍历默认的视图解析器 XxxViewResolver ,调用其 resolveViewName() 方法来解析视图名,如果能解析,则返回该解析的 View 对象

接着再看看 view 的 render() 方法如何渲染视图的:

public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
                   HttpServletResponse response) throws Exception {

    // ...
    renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); // 渲染视图
}

唉,又套娃了,继续看看 renderMergedOutputModel()

protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
                                       HttpServletResponse response) throws IOException {

    // ...

    // Redirect
    sendRedirect(request, response, targetUrl, this.http10Compatible);
}

继续 sendRedirect()

protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
                            String targetUrl, boolean http10Compatible) throws IOException {

    String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl));
    if (http10Compatible) {
        HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
        if (this.statusCode != null) {
            response.setStatus(this.statusCode.value());
            response.setHeader("Location", encodedURL);
        }
        else if (attributeStatusCode != null) {
            response.setStatus(attributeStatusCode.value());
            response.setHeader("Location", encodedURL);
        }
        else {
            // Send status code 302 by default.
            response.sendRedirect(encodedURL);
        }
    }
    else {
        HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
        response.setStatus(statusCode.value());
        response.setHeader("Location", encodedURL);
    }
}

底层归根到底就是 Servlet 的底层 response.sendRedirect(encodedURL); 进行重定向.

其他的视图解析类似.

6. 拦截器

Spring MVC 拦截器的顶级接口是 HandlerInterceptor

public interface HandlerInterceptor {
	// 目标方法执行前执行
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

	// 目标方法执行完执行
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

	// 请求完成之后(视图渲染之后)执行
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}

以登录拦截为例:

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 登录检查逻辑
        HttpSession session = request.getSession();
        Object user = session.getAttribute("user");
        if (user!=null){
            return true;
        }
        response.sendRedirect("/");
        return false;
    }
}

将拦截器配置到 Spring 容器中

@Configuration
// 自定义 MVC 配置都要实现 WebMvcConfigurer 接口,重写方法即可
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
            .addPathPatterns("/**") // 所以请求都被拦截了,包括静态资源
            .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); // 放行的请求
    }
}
  • 一般是按照顺序执行拦截器 preHandle() 方法的,当遇到返回为 false 时,它会倒序执行已经执行过的拦截器的 afterCompletion() 方法

  • 后面遇到任何异常都会倒序执行拦截器的 afterCompletion() 方法

  • 页面完成渲染之后,也会倒序触发拦截器 afterCompletion() 方法

7. 文件上传

前端提交文件的元素为:

<input type="file" name="headerImage" id="exampleInputFile"> <!-- 单文件上传 -->
<input type="file" name="photos" multiple> <!-- 多文件上传 -->

并且,表单 form 必须设为 post 提交 以及 `enctype=“multipart/form-data”:

<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">

处理上传文件的 Controller 如下所示:

@Controller
public class FormTestController {
    
    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
                         @RequestPart("headerImage") MultipartFile headerImage,
                         @RequestPart("photos") MultipartFile[] photos) throws IOException {
        
        if (!headerImage.isEmpty()) {
            String originalFilename = headerImage.getOriginalFilename();
            headerImage.transferTo(new File("E:/cache/" + originalFilename));
        }
        if (photos.length > 0) {
            for (MultipartFile photo : photos) {
                if (!photo.isEmpty()) {
                    String originalFilename = photo.getOriginalFilename();
                    photo.transferTo(new File("E:/cache/" + originalFilename));
                }
            }
        }
        return "main";
    }
}

E:/cache 地址确定存在,否则报错!

当然,上传文件还有一些配置,比如文件最大的大小,我们可以在配置文件中直接设置属性值:

spring:
  servlet:
    multipart:
      max-file-size: 10MB

8. 错误处理

8.1 默认规则

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

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

    • 浏览器客户端

      在这里插入图片描述

    • Postman 客户端

      在这里插入图片描述

  • error/ 下的 4xx5xx 页面会被自动解析

    在这里插入图片描述

8.2 异常处理的自动配置原理

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

    • 配置了 DefaultErrorAttributes 类型组件 id:errorAttributes

      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
    • 配置了 BasicErrorController 类型组件 id:basicErrorController(内容协商适配响应)

      • 处理默认 /error 路径的请求,页面响应 new ModelAndView("error", model)
    • 配置了 View 类型组件 id:error

      @Bean(name = "error")
      @ConditionalOnMissingBean(name = "error")
      public View defaultErrorView() {
          return this.defaultErrorView; // 默认是一个我白页 StaticView
      }
      
    • 配置了组件 BeanNameViewResolver (视图解析器),按照返回的视图名作为组件的 id 去容器中找 View 对象

    • 配置了组件 DefaultErrorViewResolverid:conventionErrorViewResolver

      • 如果发生错误,会以 HTTP 的状态码作为视图页地址,找到真正的页面:

        String errorViewName = "error/" + viewName;
        

8.3 定制错误处理逻辑

1、自定义错误页

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

2、@ControllerAdvice + @ExceptionHandler 处理异常

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler({ArithmeticException.class, NullPointerException.class}) // 能处理的异常
    public String handleMathException(Exception exception) {
        System.out.println("异常是:" + exception);
        return "login"; // 视图地址
    }
}

3、@ResponseStatus + 自定义异常

@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "you reason")  // 传入该异常的返回码以及原因
public class XException extends RuntimeException {

    public XException(String msg) {
        super(msg);
    }
}

4、自定义异常解析器

@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            response.sendError(511,"我喜欢的错误"); // response.sendError() /error 请求就会转给 controller
        } catch (IOException e) {
            e.printStackTrace();
        }
        // return new ModelAndView(); 或者返回要跳转的页面及数据
    }
}

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

9.1 使用 Servlet API

首先编写一个 Servlet:

@WebServlet(urlPatterns = "/my") // servlet 3.0 以上提供的注解
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("66666");
    }
}

然后在主启动类添加注解 @ServletComponentScan

@SpringBootApplication
@ServletComponentScan(basePackages = "com.ice.admin")
public class SpringbootWebAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebAdminApplication.class, args);
    }
}

访问 http://localhost:8080/my:

在这里插入图片描述

它直接响应,没有经过 Spring 的拦截器

拦截器和监听器也一样:

【拦截器】

@WebFilter(urlPatterns = {"/css/*","/images/*"})  // Servlet 的写法是 *,Spring 是* *
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("MyFilter 初始化完成...");
    }

    @Override
    public void destroy() {
        System.out.println("MyFilter 销毁...");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("MyFilter 执行...");
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

【监听器】

@WebListener
public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("监听到项目初始化完成...");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("监听到项目销毁...");
    }
}

注意,他们都要统一咋主启动类添加 @ServletComponentScan 注解扫描原生组件!

9.2 使用 RegistrationBean

@Configuration
public class MyRegisterConfig {
    @Bean
    public ServletRegistrationBean myServlet() {
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet, "/my", "/my01");
    }

    @Bean
    public FilterRegistrationBean myFilter1() {
        MyFilter myFilter = new MyFilter();
        return new FilterRegistrationBean(myFilter, myServlet()); // 拦截指定 Servlet
    }

    @Bean
    public FilterRegistrationBean myFilter2() {
        MyFilter myFilter = new MyFilter();
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my", "/css/*"));
        return filterRegistrationBean;
    }

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值