Spring MVC里的C和M都看完了, 剩下的就是一个V也就是视图了. 之前只是知道返回一个ModelAndView给DispatcherServlet就结束了, 最后返回一个用模型数据渲染的视图, 现在就来仔细看看这个过程.

  1. 什么是视图
  2. 配置Thymeleaf模板引擎
  3. 文件上传

什么是视图

说到视图, 还是回到之前说的来, 我们会配置一个视图解析器, 用于将ModelAndView中的视图名称解析成视图路径下的一个文件, 进而去找到视图. 我们在配置类中使用的是InternalResourceViewResolver, 点开源代码可以看到继承了UrlBasedViewResolver, 继续追进去再到AbstractCachingViewResolver.

可以看到这个AbstractCachingViewResolver有一个方法public View resolveViewName(String viewName, Locale locale) throws Exception, 这个方法返回一个视图对象View. 这就是视图解析器的作用.

很显然, 通过刚才的分析, 我们可以知道, 视图对象在Spring中是一个org.springframework.web.servlet.View接口类, 继续点进这个接口, 源代码如下:

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface View {
    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
    String PATH_VARIABLES = View.class.getName() + ".pathVariables";
    String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";

    @Nullable
    default String getContentType() {
        return null;
    }

    void render(@Nullable Map<String, ?> var1, HttpServletRequest var2, HttpServletResponse var3) throws Exception;
}

除了静态的用来标识的字符串之外, 核心的就是两个方法, 一个是获取视图对应的媒体类型; 一个是render方法, 可以看到接受的三个对象分别是一个Map, 一个请求, 一个响应, 这个方法的作用不难想象, Map其实就是隐含模型数据, 用模型数据和请求数据, 向响应中写入结果, 也就是渲染视图.

这里同时说明, 每次的视图对象都是由解析器创建的. 那么Spring提供了哪些View接口的实现类呢?

所有视图的实现类都位于org.springframework.web.servlet.view包中, 其中有一个默认的InternalResourceView对象, 就是用于将JSP资源封装成一个View对象, 这也是InternalResourceViewResolver默认使用的对象. 所以现在明白了为什么什么也不设置, 就可以使用JSP技术了吧. 当然, 这也是Java Web容器的标准之一, 肯定是默认的了.

视图解析器配置之后是一个Bean, 视图对象也是一个Bean, 每次渲染视图的时候, 这些Bean都是被多线程重用的, 由于Bean本身就是多线程安全的, 所以没有问题.

这里再提一下视图解析器, 视图解析器可以同时配置多个, 每个视图解析器都实现了Ordered接口并且有一个数字,可以通过这个属性指定解析器的优先顺序.

最优先的是一个叫做ContentNegotiatingViewResolver的视图解析器, 根据MIME类型来选用对应的视图解析器, 而我们将一个名称解析为一个URL路径的InternalResourceViewResolver默认是最低优先级, 这是因为在Java Web容器中, Servlet以及本质就是Servlet的JSP技术是默认就可以渲染视图的.

所以到了这里我们就可以知道, 如果想要使用不同的视图来渲染数据, 首先需要实现一个视图解析器, 返回一个不同于JSP的View对象. 下边就以配置Thymeleaf来试验一下.

配置Thymeleaf模板引擎

JSP和Servlet的局限性在于, 必须在Java Web容器中使用, 很多其他类型的或者基于其他语言的Web容器并不支持Servlet技术, Thymeleaf则是独立的一套渲染模板的引擎, 所以可以用于各个场景, 比如生成HTML格式的电子邮件.

首先配置Thymeleaf的依赖:

<!-- Thymeleaf 的标准包-->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.0.11.RELEASE</version>
</dependency>

<!-- 将Thymeleaf与Spring4集成的包 -->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring4</artifactId>
    <version>3.0.11.RELEASE</version>
</dependency>

在Spring中使用Thymeleaf, 上边两个包都需要, 一个是Thymeleaf的核心功能, 一个是专门用于Spring来将Thymeleaf集成进自己体系的包.

然后我们需要来配置上边使用的三个对象, Thymeleaf要使用的模板引擎的后缀名就是html, 一步一步来.

首先配置视图路径解析器, 这个一看便知作用:

import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;


@Bean
public ITemplateResolver templateResolver() {
    SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
    templateResolver.setTemplateMode(TemplateMode.HTML);
    templateResolver.setPrefix("/WEB-INF/views/thymeleaf/");
    templateResolver.setSuffix(".html");
    templateResolver.setCharacterEncoding("UTF-8");
    templateResolver.setCacheable(false);
    return templateResolver;
}

这其中的返回类型和解析器类型, 全部都是Thymeleaf包提供的. 这里我们可以把自己原来配置的InternalResourceViewResolver的Bean删除了.

然后, 需要配置模板解析引擎, 相比JSP来说, 这是一个新东西, 因为JSP技术是Web容器自带的,无需配置, 而Thymeleaf就必须先配置这个东西作为一个Bean:

@Bean
public TemplateEngine templateEngine(ITemplateResolver templateResolver) {
    SpringTemplateEngine templateEngine = new SpringTemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
    return templateEngine;
}

这里使用了Spring的自动依赖注入, 因为之前已经配置了一个ITemplateResolver类型的Bean, 所以这里会自动将其传入, 组装成一个TemplateEngine类型的Bean.

最后配置视图解析器, 也就是相当于视图的Bean:

@Bean
public ViewResolver viewResolver(TemplateEngine templateEngine) {
    ThymeleafViewResolver resolver = new ThymeleafViewResolver();
    resolver.setTemplateEngine(templateEngine);
    return resolver;
}

同样也是依赖注入, 可以看到这三个Bean环环相扣, 持有引用, 可以协同工作. 配置了上边的一些东西之后, 就可以把原来解析JSP路径的解析器全部删掉了.

在views目录下创建thymeleaf目录, 然后创建一个index.html:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
</head>
<body>
<p>Thymeleaf</p>
</body>
</html>

然后编写一个最简单的控制器:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ThymeleafController {


    @RequestMapping("/")
    public String index() {
        return "index";
    }

}

之后启动服务器, 访问根路径, 发现已经正确的显示出了Thymeleaf页面.

视图还有很多种, 分别需要不同的解析器和视图对象, 比如Excel和PDF, 需要的时候再学吧.

今天虽然文字少了一点, 不过总算知道使用Spring Boot的时候是怎么来配置这些模板引擎的了, 只是把这个东西给后台化了.

文件上传

Spring MVC通过一个MultipartResolver实现文件上传. 这个东西需要配置成一个Bean, 但是这个东西依赖Apache Jakarta Commons io/FileUpload 包来实现. 文件上传就对应HTML FROM中的multipart.

依赖包的Maven配置如下:

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

来配置一个文件上传的MultipartResolver:

@Bean
public CommonsMultipartResolver commonsMultipartResolver() throws IOException {
    CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
    commonsMultipartResolver.setDefaultEncoding("UTF-8");
    commonsMultipartResolver.setMaxUploadSize(5242880L);
    commonsMultipartResolver.setUploadTempDir(new FileSystemResource("d:\\temp"));
    return commonsMultipartResolver;
}

然后是控制器:

@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
    System.out.println(file);
    System.out.println(file.getOriginalFilename());
    System.out.println(file.getSize());
    file.transferTo(new File("d:\\temp\\" + file.getOriginalFilename()));
    return "index";
}

上传的文件会自动放在MultipartFile对象中, 可以查看原来的文件名, 大小等. 还可以使用.transferTo()方法将其存放在指定的位置.

上传表单则必须要使用额外的enctype属性:

<form action="/upload" method="post" enctype="multipart/form-data">
    上传文件<input type="file" name="file">
    <button type="submit">提交</button>
</form>

之后尝试了一下, 发现可以成功的上传文件了.

至此, Spring MVC的主要内容就看完了, 其实核心不外乎两个, 一是从请求中读入各种数据并且组装成Web应用需要的数据对象, 二是将Web应用处理后的数据, 以各种方式组成响应返回.