• 04-Web开发(中)


    1. 数据响应与内容协商

    1.1 返回值处理流程

    (1)执行目标方法,获取方法返回值 returnValue。

    (2)returnValueHandlers 调用 handleReturnValue() 进行处理 → 循环遍历〈返回值处理器集合〉,找到 support 处理返回值标了@ResponseBody 注解的 → RequestResponseBodyMethodProcessor。

    返回值处理器集合如下:

    【补充】除了 RequestResponseBodyMethodProcessor,ServletModelAttributeMethodProcessor、ModelMethodProcessor 同样既存在于 argumentResolvers[] 中,也存在于 returnValueHandlers[] 中 ~

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }
    
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass()
            , ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
    }
    

    (3)RequestResponseBodyMethodProcessor 内部是利用 MessageConverters 进行处理(writeWithMessageConverters 方法)→ 该方法实现在父类中(如下)

    • 内容协商(双重 for 循环)
      • 浏览器默认会以请求头的方式(Accept)告诉服务器他能接受什么样的内容类型,封装成 acceptableTypes;
      • 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,封装成 producibleTypes;
    • 协商出最终返回类型后,RequestResponseBodyMethodProcessor 会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理(把 returnValue 转换成协商结果类型)?

    (4)最终利用 MappingJackson2HttpMessageConverter.write() 把对象转为 JSON(利用底层 ObjectMapper 转换的)写入 outputBuffer 中。

    上述 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 支持任何类型。

    public abstract class AbstractJackson2HttpMessageConverter
                        extends AbstractGenericHttpMessageConverter<Object> {
        // ...
    }
    
    public abstract class AbstractGenericHttpMessageConverter<T> extends ...{
        @Override
        protected boolean supports(Class<?> clazz) {
            return true;
        }
    }
    

    1.2 Request/ResponseBody

    测试代码:

    @ResponseBody
    @PostMapping("echo")
    public Person echo(@RequestBody Person person) {
        return person;
    }
    

    涉及到的参数解析器、返回值处理器为同一个类 —— RequestResponseBodyMethodProcessor,其父类是 AbstractMessageConverterMethodProcessor:

    class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {...}
    
    abstract class AbstractMessageConverterMethodProcessor
            extends AbstractMessageConverterMethodArgumentResolver
            implements HandlerMethodReturnValueHandler {...}
    

    下面涉及到的方法均是这两个类中的方法~

    1.2.1 @RequestBody

    调用过程都是一样的,就是具体到某个解析器,其各自处理方式不同(就看栈顶 3 个 方法):

    RequestResponseBodyMethodProcessor:

    @Override
    public Object resolveArgument(
            MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
    
      parameter = parameter.nestedIfOptional();
    
      // ===== ↓↓↓↓↓ Step Into ↓↓↓↓↓ =====
      Object arg = readWithMessageConverters(
                      webRequest, parameter, parameter.getNestedGenericParameterType());
    
      String name = Conventions.getVariableNameForParameter(parameter);
    
      if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if (arg != null) {
          validateIfApplicable(binder, parameter);
          if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
          }
        }
        if (mavContainer != null) {
          mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
      }
    
      return adaptArgumentIfNecessary(arg, parameter);
    }
    
    @Override
    protected <T> Object readWithMessageConverters(
            NativeWebRequest webRequest, MethodParameter parameter, Type paramType) {
    
      HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
      Assert.state(servletRequest != null, "No HttpServletRequest");
      ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
    
      // ===== ↓↓↓↓↓ Step Into ↓↓↓↓↓ =====
      Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
    
      if (arg == null && checkRequired(parameter)) {
        throw new HttpMessageNotReadableException("Required request body is missing: " +
            parameter.getExecutable().toGenericString(), inputMessage);
      }
      return arg;
    }
    

    AbstractMessageConverterMethodArgumentResolver:

    1.2.2 @ResponseBody

    上文 #1.1 小节差不多都提到了,其中(3)的配图就是 AbstractMessageConverterMethodArgumentResolver 中的 writeWithMessageConverters 方法,就顺着那张截图的最后一句代码 Step Into:

    【小结】当处理方法上标 @ResponseBody,则会选用 RequestResponseBodyMethodProcessor 返回值处理器,其处理流程是:先进行内容协商,确定合适的返回类型;然后循环遍历 messageConverters 集合选择能够将 returnValue 转换成协商结果类型的 HttpMessageConverter,若找到了就调用 write 写给客户端,响应成功;若找不到可用的 HttpMessageConverter 则报错。

    1.3 内容协商

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

    1.3.1 PostMan 展示效果

    引入 XML 依赖:

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

    再查看 messageConverters 集合:

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

    1.3.2 内容协商原理

    RequestResponseBodyMethodProcessor

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
    
      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);
    }
    

    AbstractMessageConverterMethodProcessor

    // ===> 第 1 次循环是来统计支持处理返回值类型的 converter 能将其转成哪些 MediaType
    protected List<MediaType> getProducibleMediaTypes(
        HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
    
      Set<MediaType> mediaTypes = (Set<MediaType>)
          request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
      if (!CollectionUtils.isEmpty(mediaTypes)) {
        return new ArrayList<>(mediaTypes);
      } else if (!this.allSupportedMediaTypes.isEmpty()) {
    
        // - 统计支持处理的 converter 们所支持的 MediaType -
        List<MediaType> result = new ArrayList<>();
        for (HttpMessageConverter<?> converter : this.messageConverters) {
          if (converter instanceof GenericHttpMessageConverter && targetType != null) {
            if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
              result.addAll(converter.getSupportedMediaTypes());
            }
          } else if (converter.canWrite(valueClass, null)) {
            result.addAll(converter.getSupportedMediaTypes());
          }
        }
    
        return result; // 服务端能处理的 MediaType 集合(见下图)
      } else {
        return Collections.singletonList(MediaType.ALL);
      }
    }
    
    protected <T> void writeWithMessageConverters(
        @Nullable T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage,
        ServletServerHttpResponse outputMessage) {
    
      // ...
    
      MediaType selectedMediaType = null;
      MediaType contentType = outputMessage.getHeaders().getContentType();
      boolean isContentTypePreset = contentType != null && contentType.isConcrete();
      if (isContentTypePreset) {
        if (logger.isDebugEnabled()) {
          logger.debug("Found 'Content-Type:" + contentType + "' in response");
        }
        selectedMediaType = contentType;
      } else {
    
      // ===== else Start ======
    
      HttpServletRequest request = inputMessage.getServletRequest();
      // 客户端可接受的类型 → 见下个方法(内容协商管理器默认基于请求头的内容协商策略)
      List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
      // 服务端可处理的类型 → 见上个方法
      List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
    
      if (body != null && producibleTypes.isEmpty()) {
        throw new HttpMessageNotWritableException(
            "No converter found for return value of type: " + valueType);
      }
    
      // 内容协商!
      List<MediaType> mediaTypesToUse = new ArrayList<>();
        for (MediaType requestedType : acceptableTypes) {
          for (MediaType producibleType : producibleTypes) {
            if (requestedType.isCompatibleWith(producibleType)) {
        	  mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
        	}
          }
      }
      if (mediaTypesToUse.isEmpty()) {
        if (body != null) throw new HttpMediaTypeNotAcceptableException(producibleTypes);
        return;
      }
    
      MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
    
      // 第 1 个就是内容协商の最佳匹配 ~
      for (MediaType mediaType : mediaTypesToUse) {
        if (mediaType.isConcrete()) {
          selectedMediaType = mediaType;
          break;
        } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
          selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
          break;
        }
      }
    
      // ===== else End ======
    
      }
    
      // ===> 第 2 次循环来找能将返回类型转成指定 selectedMediaType 类型的 converter
      if (selectedMediaType != null) {
        selectedMediaType = selectedMediaType.removeQualityValue();
        for (HttpMessageConverter<?> converter : this.messageConverters) {
          GenericHttpMessageConverter genericConverter =
            (converter instanceof GenericHttpMessageConverter
                ? (GenericHttpMessageConverter<?>) converter : null);
          // - 判断 -
          if (genericConverter != null ? ((GenericHttpMessageConverter) converter)
                .canWrite(targetType, valueType, selectedMediaType)
                : converter.canWrite(valueType, selectedMediaType)) {
    
            body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
                    (Class<? extends HttpMessageConverter<?>>)
                            converter.getClass(), inputMessage, outputMessage);
            if (body != null) {
              Object theBody = body;
              addContentDispositionHeader(inputMessage, outputMessage);
              if (genericConverter != null) {
                // ======== ObjectWriter.ToXmlGenerator ========
                genericConverter.write(body, targetType, selectedMediaType, outputMessage);
              } else {
                ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
              }
            } else {
              if (logger.isDebugEnabled()) {
                logger.debug("Nothing to write: null body");
              }
            }
            return;
          }
        }
      }
    
      // ...
    }
    
    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
    			throws HttpMediaTypeNotAcceptableException {
        return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
    }
    

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

    (2)获取客户端(PostMan、Browser)支持接收的内容类型(底层通过「内容协商管理器」);

    (3)遍历循环所有 MessageConverter,看谁支持操作 handle-ret 类型,将所有支持的 converters 所能处理成的 MediaType 打包返回;

    (4)acceptableTypes、producibleTypes 双重 for 进行内容协商,选中最佳匹配类型;

    (5)循环遍历 converters,用「支持将对象转为最佳匹配类型的 converter」进行转换。

    1.3.3 基于请求参数的内容协商

    通过 #1.2.2 某图可以看出,内容协商处理器 contentNegotiationManager 默认只有一种策略 —— HeaderContentNegotiationStrategy,即通过 HttpHeaders.ACCEPT 来获取 acceptableTypes,但如果不想用这种方式呢,在 PostMan 测试,请求头可以随便改,但如果用 Browser 测试呢?

    查看 ContentNegotiationStrategy 的实现类:

    所以,针对这种情况,为方便内容协商,开启「基于请求参数」的内容协商功能 —— ParameterContentNegotiationStrategy。

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

    WebMvcProperties:

    /**
     * Whether a request parameter ("format" by default) should be used to determine
     * the requested media type.
     */
    private boolean favorParameter = false;
    

    测试:

    http://localhost:8080/test/person?format=json
    http://localhost:8080/test/person?format=xml
    

    底层:

    1.3.4 自定义 MessageConverter

    Quiz:通过自定义 Accept 方式(走请求头策略),要求 Server 返回的 Person 对象是以属性间 ; 隔开的形式。

    (1)先来看下 messageConverters 封装原理

    (2)问题切入口

    public interface WebMvcConfigurer {
      // 覆盖
      default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {}
      // 扩展
      default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {}
      // ...
    }
    

    (3)MyMessageConverter

    public class MyMessageConverter implements HttpMessageConverter<Person> {
        @Override
        public boolean canRead(Class<?> clazz, MediaType mediaType) {
            return false;
        }
    
        @Override
        public boolean canWrite(Class<?> clazz, MediaType mediaType) {
            return clazz.isAssignableFrom(Person.class);
        }
    
        @Override
        public List<MediaType> getSupportedMediaTypes() {
            return MediaType.parseMediaTypes("application/x-1101");
        }
    
        @Override
        public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage)
                throws IOException, HttpMessageNotReadableException {
            return null;
        }
    
        @Override
        public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage)
                throws IOException, HttpMessageNotWritableException {
            // 自定义协议数据的写出
            String data = person.getName() + ";" + person.getAge();
            OutputStream outputStream = outputMessage.getBody();
            outputStream.write(data.getBytes());
        }
    }
    

    (4)将 MyMessageConverter 添加到 messageConverters 中

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new MyMessageConverter());
            }
    
            // ...
    
        }
    }
    

    (5)流程概览

    1.3.5 以参数方式内容协商扩展

    上文可以看到,ParameterContentNegotiationStrategy 要求请求参数 format 只能写 json 和 xml,无法满足现在的要求,How?

    (1)自定义 ParameterContentNegotiationStrategy

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
      return new WebMvcConfigurer() {
        /**
         * 自定义内容协商策略
         * @param configurer
         */
        @Override
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
          Map<String, MediaType> mediaTypes = new HashMap<>(16);
          mediaTypes.put("json", MediaType.APPLICATION_JSON);
          mediaTypes.put("xml", MediaType.APPLICATION_XML);
          mediaTypes.put("tree", MediaType.parseMediaType("application/x-1101"));
          ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(mediaTypes);
          configurer.strategies(Arrays.asList(strategy));
        }
        // ...
      }
    }
    

    注意!上述这种写法就没有默认的请求头策略咯~ 如此以来,你就是在 Accept 里写出花,Server 还是会给你返 Json 格式。

    ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
    HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
    configurer.strategies(Arrays.asList(paramStrategy, headerStrategy));
    

    所以说,有时候我们添加的自定义功能会覆盖默认配置,导致一些默认的功能失效,这点要注意下。

    (2)走一遍源码

    2. 模板引擎 Thymeleaf

    2.1 引入 Starter

    (1)导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    

    (2)查看自动配置类

    @ConfigurationProperties(prefix = "spring.thymeleaf")
    public class ThymeleafProperties {
        private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
        public static final String DEFAULT_PREFIX = "classpath:/templates/";
        public static final String DEFAULT_SUFFIX = ".html";
    }
    
    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(ThymeleafProperties.class)
    @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
    @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
    public class ThymeleafAutoConfiguration {
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnMissingBean(name = "defaultTemplateResolver")
        static class DefaultTemplateResolverConfiguration {...}
    
        @Configuration(proxyBeanMethods = false)
        protected static class ThymeleafDefaultConfiguration {...}
    
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnWebApplication(type = Type.SERVLET)
        @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
        static class ThymeleafWebMvcConfiguration {...}
    
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnWebApplication(type = Type.REACTIVE)
        @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
        static class ThymeleafReactiveConfiguration {...}
    
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnWebApplication(type = Type.REACTIVE)
        @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
        static class ThymeleafWebFluxConfiguration {...}
    
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass(LayoutDialect.class)
        static class ThymeleafWebLayoutConfiguration {...}
    
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass(DataAttributeDialect.class)
        static class DataAttributeDialectConfiguration {...}
    
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass({ SpringSecurityDialect.class })
        static class ThymeleafSecurityDialectConfiguration {...}
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass(Java8TimeDialect.class)
        static class ThymeleafJava8TimeDialect {...}
    }
    

    (3)简单测试

    @Controller
    public class ThymeleafController {
        @GetMapping("test")
        public String test1(HttpServletRequest request) {
            request.setAttribute("msg", "Future is coming.");
            request.setAttribute("link", "www.gotoFuture.com");
            return "success";
        }
    }
    

    2.2 初步结合 AdminEX

    https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

    2.2.1 Controller

    IndexController

    @GetMapping(value={"", "login"})
    public String loginPage() {
        System.out.println("重定向到登录页");
        return "login";
    }
    
    @PostMapping("login")
    public String login(HttpSession session, User user, Model model) {
      if (StringUtils.hasLength(user.getUsername()) && StringUtils.hasLength(user.getPassword())) {
        session.setAttribute("loginUser", user);
      } else {
        model.addAttribute("msg", "账号/密码错误!");
        return "login";
      }
      System.out.println("防止表单重复提交(step1): 采用重定向到主页,一旦登录成功就和/login撇开关系");
      return "redirect:main.html";
    }
    
    @GetMapping("main.html")
    public String mainPage(HttpSession session, Model model) {
        if (session.getAttribute("loginUser") == null) {
            System.out.println("转发到登录页");
            model.addAttribute("msg", "请先登录!");
            return "login";
        }
        System.out.println("防止表单重复提交(step2): 刷新主页抵达该处理方法,再转发回主页~");
        return "main";
    }
    

    TableController

    @GetMapping("/basic_table")
    public String basicTable() {
        return "table/basic_table";
    }
    
    @GetMapping("/dynamic_table")
    public String dynamicTable(Model model) {
        List<User> list = Arrays.asList(
                new User("zhangsan", "123"),
                new User("lisi", "123"),
                new User("wangwu", "123"),
                new User("zhaoliu", "123")
        );
        model.addAttribute("users", list);
        return "table/dynamic_table";
    }
    
    @GetMapping("/responsive_table")
    public String responsiveTable() {
        return "table/responsive_table";
    }
    
    @GetMapping("/editable_table")
    public String editableTable() {
        return "table/editable_table";
    }
    
    @GetMapping("/pricing_table")
    public String pricingTable() {
        return "table/pricing_table";
    }
    

    2.2.2 HTML

    main.html、table/*.html 具有共同的左侧菜单和顶部导航栏,故抽取成 common.html:

    <div class="left-side sticky-left-side" id="common-left-menu"></div>
    <div class="header-section" th:fragment="common-header">...</div>
    

    在其他页面中引入:

    <div th:replace="common::#common-left-menu"></div>
    <link th:replace="common::common-header"/>
    

    table/dynamic_table 有个数据列表的展示:

    <thead>
    <tr>
        <th>#</th>
        <th>username</th>
        <th>password</th>
    </tr>
    </thead>
    <tbody>
    <tr class="gradeX" th:each="user, status:${users}">
        <td th:text="${status.count}">Trident</td>
        <td th:text="${user.username}">Win 95+</td>
        <td th:text="${user.password}">Win 95+</td>
    </tr>
    </tbody>
    

    2.3 视图渲染过程

    ViewNameMethodReturnValueHandler

    ContentNegotiatingViewResolver 处理“请求重定向”和“请求转发”

    请求重定向

    请求转发

    3. 拦截器

    3.1 自定义拦截器

    public class LoginInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request,
                    HttpServletResponse response, Object handler) throws Exception {
            System.out.println("LoginInterceptor#preHandle");
    
            HttpSession session = request.getSession();
            if (session.getAttribute("loginUser") == null) {
                response.sendRedirect("/admin/login");
                return false;
            }
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response,
                    Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("LoginInterceptor#postHandle");
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                    Object handler, Exception ex) throws Exception {
            System.out.println("LoginInterceptor#afterCompletion");
        }
    }
    

    3.2 注册拦截器

    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer {
    
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
        registry
            .addInterceptor(new LoginInterceptor())
            .addPathPatterns("/**")
            .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**");
            // '/**' 把所有请求(包括静态)都被拦截,两种方式解决:
            // (1) 如上所示,静态资源挨个列出来
            // (2) 配置静态资源的访问前缀,记得改每个超链接
            // spring:
            //  mvc:
            //    static-path-pattern: xxx
      }
    }
    

    3.3 Debug 源码

    boolean applyPreHandle(HttpServletRequest request,
                HttpServletResponse response) throws Exception {
    
        for (int i = 0; i < this.interceptorList.size(); i++) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            if (!interceptor.preHandle(request, response, this.handler)) {
                triggerAfterCompletion(request, response, null);
                return false;
            }
            this.interceptorIndex = i;
        }
        return true;
    
    }
    
    void applyPostHandle(HttpServletRequest request, HttpServletResponse response,
                @Nullable ModelAndView mv) throws Exception {
    
        for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            interceptor.postHandle(request, response, this.handler, mv);
        }
    
    }
    
    void triggerAfterCompletion(HttpServletRequest request,
                HttpServletResponse response, @Nullable Exception ex) {
    
        for (int i = this.interceptorIndex; i >= 0; i--) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            try {
                interceptor.afterCompletion(request, response, this.handler, ex);
            } catch (Throwable ex2) {
                logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
            }
        }
    
    }
    

    4. 文件上传

    4.1 测试

    表单

    <form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="exampleInputEmail1">Email address</label>
            <input type="email" name="email" class="form-control"
                        id="exampleInputEmail1" placeholder="Enter email">
        </div>
        <div class="form-group">
            <label for="exampleInputPassword1">Password</label>
            <input type="password" name="password" class="form-control"
                        id="exampleInputPassword1" placeholder="Password">
        </div>
        <div class="form-group">
            <label for="exampleInputFile">File input</label>
            <input type="file" name="headerImg" id="exampleInputFile">
            <p class="help-block">Example block-level help text here.</p>
        </div>
        <div class="form-group">
            <label for="exampleInputFile">File input</label>
            <input type="file" name="photos" id="exampleInputFiles" multiple>
            <p class="help-block">Example block-level help text here.</p>
        </div>
        <div class="checkbox">
            <label>
                <input type="checkbox"> Check me out
            </label>
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    

    控制器

    @Slf4j
    @Controller
    public class FormController {
    
        @GetMapping("/form_layouts")
        public String layoutForm() {
            return "form/form_layouts";
        }
    
        @PostMapping("/upload")
        public String upload(
                @RequestParam("email") String email,
                @RequestParam("password") String password,
                @RequestPart("headerImg") MultipartFile headerImg,
                @RequestPart("photos") MultipartFile[] photos) {
            log.info("上传的文件:email={}, password={}, headerImg={}, photos.length={}",
                        email, password, headerImg.getSize(), photos.length);
            return "main";
        }
    }
    

    4.2 源码

    (1)checkMultipart(request)

    (2)mav = invokeHandlerMethod(...) → ... → Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs) 进入循环解析参数流程

  • 相关阅读:
    CSS+JS实现兼容性很好的无限级下拉菜单
    自动切换的JS菜单
    (2)C#连sqlite
    protobuf编译器
    (67) c# 序列化
    (66) c# async await
    (65)C# 任务
    mbatis 入门
    (64)C# 预处理器指令
    (63)C# 不安全代码unsafe
  • 原文地址:https://www.cnblogs.com/liujiaqi1101/p/15269785.html
Copyright © 2020-2023  润新知