Loading

38-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) 进入循环解析参数流程

posted @ 2021-09-14 21:47  tree6x7  阅读(48)  评论(0编辑  收藏  举报