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