《Spring 实战》笔记7:Spring MVC 的高级技术

Spring MVC 配置的替代方案

在第 5 章中,我们通过扩展 AbstractAnnotationConfigDispatcherServletInitializer 快速搭建了 Spring MVC 环境。在这个便利的基础类中,假设我们需要基本的 DispatcherServletContextLoaderListener 环境,并且 Spring 配置是使用 Java 的,而不是 XML。

尽管对很多 Spring 应用来说,这是一种安全的假设,但是并不一定能满足我们的要求。除了 DispatcherServlet 以外,我们还可能需要额外的 ServletFilter,我们可能还需要对 DispatcherServlet 本身做一些额外的配置;或者,如果我们需要将应用部署到 Servlet3.0 之前的容器中,那么还需要将 DispatcherServlet 配置到传统的 web.xml 中。

自定义 DispatcherServlet 配置

SpittrWebAppInitializer (第五章)中继承了 AbstractAnnotationConfigDispatcherServletInitializer ,还有其他可重载的方法 customizeRegistration()

AbstractAnnotationConfigDispatcherServletInitializerDispatcherServlet 注册到 Servlet 容器中之后,就会调用 customizeRegistration(),并将 Servlet 注册后得到的 Registration.Dynamic
传递进来。通过重载 customizeRegistration() 方法,我们可以对 DispatcherServlet 进行额外的配置。

如果计划使用 Servlet 3.0 对 multipart 配置的支持,那么需要使用 DispatcherServletregistration 来启用 multipart 请求。我们可以重载 customizeRegistration() 方法来设置 MultipartConfigElement,如下所示:

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}

借助 customizeRegistration() 方法中的 ServletRegistration.Dynamic,我们能够完成多项任务,包括通过调用 setLoadOnStartup() 设置 load-on-startup 优先级,通过 setInitParameter() 设置初始化参数,通过调用 setMultipartConfig() 配置 Servlet3.0 对 multipart 的支持。在前面的样例中,我们设置了对 multipart 的支持,将上传文件的临时存储目录设置在 "tmpspittr/uploads" 中。

添加其他的 Servlet 和 Filter

按照 AbstractAnnotationConfigDispatcherServletInitializer 的定义,它会创建 DispatcherServletContextLoaderListener

如果我们想往 Web 容器中注册其他组件的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现 Spring 的 WebApplicationInitializer 接口。

// 通过实现 WebApplicationInitializer 来注册 Servlet
public class MyServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 注册 Servlet
        ServletRegistration.Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.getClass());
        // 映射 Servlet
        myServlet.addMapping("/custom/**");
    }
}
// 注册 Filter 的 WebApplicationInitializer
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
    filter.addMappingForUrlPatterns(null, false, "/custom/*");
}

如果你只是注册 Filter,并且该 Filter 只会映射到 DispatcherServlet 上的话,只需重
AbstractAnnotationConfigDispatcherServletInitializergetServletFilters() 方法。

@Override
protected Filter[] getServletFilters() {
    return new Filter[] { new MyFilter() };  // 可以返回任意数量的 Filter
}

在 web.xml 中声明 DispatcherServlet

在 web.xml 中搭建 Spring MVC

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- 指定了一个 XML 文件的地址,这个文件定义了根应用上下文,它会被 ContextLoaderListener 加载。从中加载bean定义 -->
        <param-value>/WEB-INF/spring/root-context.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- DispatcherServlet 会根据 Servlet 的名字找到一个文件,并基于该文件加载应用上下文。 -->
        <!-- contextConfigLocation 不是必须的,如果不配置默认在:WEB-INF/servletName -servlet.xml -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

设置 web.xml 使用基于 Java 的配置

在本书中的大部分内容中,我们都更倾向于使用 Java 配置而不是 XML 配置。因此,我们需要让 Spring MVC 在启动的时候,从带有 @Configuration 注解的类上加载配置。

要在 Spring MVC 中使用基于 Java 的配置,我们需要告诉 DispatcherServletContextLoaderListener 使用 AnnotationConfigWebApplicationContext,这是一个 WebApplicationContext 的实现类,

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
            http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <context-param>
        <param-name>contextClass</param-name>
        <!-- 使用 Java 配置 -->
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- 指定根配置类 -->
        <param-value>com.habuma.spitter.config.RootConfig</param-value>
    </context-param>
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <!-- 使用 Java 配置 -->
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- 指定 DispatcherServlet 配置类 -->
            <param-value>
                com.habuma.spitter.config.WebConfigConfig
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

处理 multipart 形式的数据

配置 multipart 解析器

DispatcherServlet 并没有实现任何解析 multipart 请求数据的功能。它将该任务委托给了 Spring 中 MultipartResolver 策略接口的实现,通过这个实现类来解析 multipart 请求中的内容。从 Spring 3.1 开始,Spring 内置了两个 MultipartResolver 的实现供我们选择:

  • CommonsMultipartResolver:使用 Jakarta Commons FileUpload 解析 multipart 请求;
  • StandardServletMultipartResolver:依赖于 Servlet 3.0multipart 请求的支持
    (始于Spring 3.1),优选方案,不需要依赖任何其他的项目。

使用 Servlet 3.0 解析 multipart 请求

兼容 Servlet 3.0 的 StandardServletMultipartResolver 没有构造器参数,也没有要设置的属性。这样,在 Spring 应用上下文中,将其声明为 bean 就会非常简单,如下所示:

@Bean
public MultipartResolver multipartResolver() throws IOException {
    return new StandardServletMultipartResolver();
}

我们必须要在 web.xml 或 Servlet 初始化类中,将 multipart 的具体细节作为 DispatcherServlet 配置的一部分。

如果我们采用 Servlet 初始化类的方式来配置 DispatcherServlet 的话,这个初始化类应该已经实现了 WebApplicationInitializer,那我们可以在 Servlet registration 上调用 setMultipartConfig() 方法,传入一个 MultipartConfigElement 实例。如下是最基本的 DispatcherServlet multipart 配置,它将临时路径设置为 tmp/spittr/uploads

DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));

如果我们配置 DispatcherServlet 的 Servlet 初始化类继承了 AbstractAnnotationConfigDispatcherServletInitializerAbstractDispatcherServletInitializer 的话,那么我们不会直接创建 DispatcherServlet 实例并将其注册到 Servlet 上下文中。这样的话,将不会有对 Dynamic Servlet registration 的引用供我们使用了。但是,我们可以通过重载 customizeRegistration() 方法(它会得到一个 Dynamic 作为参数)来配置 multipart 的具体细节:

@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}

到目前为止,我们所使用是只有一个参数的 MultipartConfigElement 构造器,这个参数指定的是文件系统中的一个绝对目录,上传文件将会临时写入该目录中。但是,我们还可以通过其他的构造器来限制上传文件的大小。除了临时路径的位置,其他的构造器所能接受的参数如下:

  • 上传文件的最大容量(以字节为单位)。默认是没有限制的。
  • 整个 multipart 请求的最大容量(以字节为单位),不会关心有多少个 part 以及每个 part 的大小。默认是没有限制的。
  • 在上传的过程中,如果文件大小达到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为 0,也就是所有上传的文件都会写入到磁盘上。
// 限制文件的大小不超过2MB,整个请求不超过4MB,而且所有的文件都要写到磁盘中
@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194304, 0));
}
<!-- 使用 web.xml 来配置 MultipartConfigElement -->
<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <load-on-startup>1</load-on-startup>
    <multipart-config>
        <location>/tmp/spittr/uploads</location>
        <max-file-size>2097152</max-file-size>
        <max-request-size>4194304</max-request-size>
    </multipart-config>
</servlet>

配置 Jakarta Commons FileUpload multipart 解析器

通常来讲,StandardServletMultipartResolver 会是最佳的选择

Spring 内置了 CommonsMultipartResolver,可以作为 StandardServletMultipartResolver 的替代方案。

CommonsMultipartResolver 声明为 Spring bean 的最简单方式如下:

@Bean
public MultipartResolver multipartResolver() {
    return new CommonsMultipartResolver();
}

CommonsMultipartResolver 不会强制要求设置临时文件路径。默认情况下,这个路径就是 Servlet 容器的临时目录。不过,通过设置 uploadTempDir 属性,我们可以将其指定为一个不同的位置,与MultipartConfigElement 有所不同,我们无法设定 multipart 请求整体的最大容量

@Bean
public MultipartResolver multipartResolver() throws IOException {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
    multipartResolver.setMaxUploadSize(2097152);  // 最大的文件容量设置为2MB
    multipartResolver.setMaxInMemorySize(0);  // 最大的内存大小设置为0字节
    return multipartResolver;
}

处理 multipart 请求

<form method="POST" th:object="${spitter}" enctype="multipart/form-data">
...
    <label>Profile Picture</label>:
    <input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif" /><br/>
...
</form>

修改 processRegistration() 方法,使其能够接受上传的图片。其中一种方式是添加 byte 数组参数,并为其添加 @RequestPart 注解。如下为示例:

@RequestMapping(value="/register", method=POST)
public String processRegistration(
        // 如果用户提交表单的时候没有选择文件,那么这个数组会是空(而不是null)。
        @RequestPart("profilePicture") byte[] profilePicture,
        @Valid Spitter spitter, Errors errors) {
    ...
}

接受 MultipartFile

// Spring 所提供的 MultipartFile 接口,用来处理上传的文件
public interface MultipartFile {
    String getName();
    String getOriginalFilename();  // 原始的文件名
    String getContentType();  // 内容类型
    boolean isEmpty();
    long getSize();  // 大小
    byte[] getBytes() throws IOException;
    InputStream getInputStream() throws IOException;  // 将文件数据以流的方式进行读取
    void transferTo(File dest) throws IOException;  // 将上传的文件写入到文件系统中
    // file.transferTo(new File("/data/spittr/" + file.getOriginalFilename()));
}

以 Part 的形式接受上传的文件

如果你需要将应用部署到 Servlet 3.0 的容器中,那么会有 MultipartFile 的一个替代方案。Spring MVC 也能接受 javax.servlet.http.Part 作为控制器方法的参数。

@RequestMapping(value="/register", method=POST)
public String processRegistration(
        @RequestPart("profilePicture") Part profilePicture,
        @Valid Spitter spitter, Errors errors) {
    ...
}

Part 方法的名称与 MultipartFile 方法的名称是完全相同的。有一些比较类似,但是稍有差异,比如 getSubmittedFileName() 对应于 getOriginalFilename()。类似地,write() 对应于 transferTo()

public interface Part {
    public InputStream getInputStream() throws IOException;
    public String getContentType();
    public String getName();
    public String getSubmittedFileName();
    public long getSize();
    public void write(String fileName) throws IOException;
    public void delete() throws IOException;
    public String getHeader(String name);
    public Collection<String> getHeaders(String name);
    public Collection<String> getHeaderNames();
}

如果在编写控制器方法的时候,通过 Part 参数的形式接受文件上传,那么就没有必要配置 MultipartResolver 了。只有使用 MultipartFile 的时候,我们才需要 MultipartResolver

处理异常

Spring 提供了多种方式将异常转换为响应:

  • 特定的 Spring 异常将会自动映射为指定的 HTTP 状态码;
  • 异常上可以添加 @ResponseStatus 注解,从而将其映射为某一个 HTTP 状态码;
  • 在方法上可以添加 @ExceptionHandler 注解,使其用来处理异常。

将异常映射为 HTTP 状态码

Spring 异常 HTTP 状态码
BindException 400 - Bad Request
ConversionNotSupportedException 500 - Internal Server Error
HttpMediaTypeNotAcceptableException 406 - Not Acceptable
HttpMediaTypeNotSupportedException 415 - Unsupported Media Type
HttpMessageNotReadableException 400 - Bad Request
HttpMessageNotWritableException 500 - Internal Server Error
HttpRequestMethodNotSupportedException 405 - Method Not Allowed
MethodArgumentNotValidException 400 - Bad Request
MissingServletRequestParameterException 400 - Bad Request
MissingServletRequestPartException 400 - Bad Request
NoSuchRequestHandlingMethodException 404 - Not Found
TypeMismatchException 400 - Bad Request

@ResponseStatus 注解:将异常映射为特定的状态码

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}

编写异常处理的方法

如果响应中不仅包含状态码,还要包含所产生的错误信息,需要按照请求的方式来处理异常。

@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
  try {
    spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
        form.getLongitude(), form.getLatitude()));
    return "redirect:/spittles";
  } catch (DuplicateSpittleException e) {     //捕获异常
    return "error/duplicate";
  }
}

运行起来没什么问题,但是这个方法有些复杂。该方法可以有两个路径,每个路径会有不同的输出。如果能让 saveSpittle() 方法只关注正确的路径,而让其他方法处理异常的话,那么它就能简单一些。

// 首先将 saveSpittle() 方法中的异常处理方法剥离掉
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
    spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
        form.getLongitude(), form.getLatitude()));
    return "redirect:/spittles";
    return "error/duplicate";
}

它只关注成功保存Spittle的情况,所以只需要一个执行路径,很容易理解和测试。

// 为 SpittleController 添加一个新的方法,它会处理抛出 DuplicateSpittleException 的情况:
@ExceptionHandler(DuplicateSpittleException.class)
public String handleNotFound() {
  return "error/duplicate";
}

方法上加上 @ExceptionHandler 注解后,当方法抛出异常的时候,将委托该方法来处理,它能够处理同一个控制器中所有的方法抛出的异常。

为控制器添加通知

如果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在所有的控制器方法中重复相同的 @ExceptionHandler 方法。或者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类要扩展这个类,从而继承通用的 @ExceptionHandler 方法。

Spring 3.2 为这类问题引入了一个新的解决方案:控制器通知。控制器通知(controller advice)是任意带有 @ControllerAdvice 注解的类,这个类会包含一个或多个如下类型的方法:

  • @ExceptionHandler 注解标注的方法;
  • @InitBinder 注解标注的方法;
  • @ModelAttribute 注解标注的方法。

在带有 @ControllerAdvice 注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有 @RequestMapping 注解的方法上。

@ControllerAdvice 注解本身已经使用了 @Component,因此 @ControllerAdvice 注解所标注的类将会自动被组件扫描获取到,就像带有 @Component 注解的类一样。

@ControllerAdvice 最为实用的一个场景就是将所有的 @ExceptionHandler 方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。例如,我们想将 DuplicateSpittleException 的处理方法用到整个应用程序的所有控制器上。如下的程序清单展现的 AppWideExceptionHandler 就能完成这一任务,这是一个带有 @ControllerAdvice 注解的类。

@ControllerAdvice
public class AppWideExceptionHandler {
    @ExceptionHandler(DuplicateSpittleException.class)
    public String duplicateSpittleHandler() {
        return "error/duplicate";
    }
}

现在,如果任意的控制器方法抛出了 DuplicateSpittleException,不管这个方法位于哪个控制器中,都会调用这个 duplicateSpittleHandler() 方法来处理异常。

跨重定向请求传递数据

通过 URL 模板进行重定向

@RequestMapping(value="/register", method=POST)
public String processRegistration(Spitter spitter, Model model) {
    spitterRepository.save(spitter);
    model.addAttribute("username", spitter.getUsername());
    model.addAttribute("spitterId", spitter.getId());
    return "redirect:/spitter/{username}";
}

因为模型中的 spitterId 属性没有匹配重定向 URL 中的任何占位符,所以它会自动以查询参数的形式附加到重定向 URL 上

如果 username 属性的值是 habuma 并且 spitterId 属性的值是 42,那么结果得到的重定向 URL 路径将会是 spitter/habuma?spitterId=42

只能用来发送简单的值,如 String 和数字的值。在 URL 中,并没有办法发送更为复杂的值,但这正是 flash 属性能够提供帮助的领域。

使用 flash 属性

假设我们不想在重定向中发送 username 或 ID 了,而是要发送实际的 Spitter 对象。

有个方案是将 Spitter 放到会话中。会话能够长期存在,并且能够跨多个请求。所以我们可以在重定向发生之前将 Spitter 放到会话中,并在重定向后,从会话中将其取出。当然,我们还要负责在重定向后在会话中将其清理掉。

实际上,Spring 也认为将跨重定向存活的数据放到会话中是一个很不错的方式。Spring 提供了将数据发送为 flash 属性(flash attribute)的功能。按照定义,flash 属性会一直携带这些数据直到下一次请求,然后才会消失。

Spring 提供了通过 RedirectAttributes 设置 flash 属性的方法,这是Spring 3.1引入的 Model 的一个子接口。RedirectAttributes 提供了 Model 的所有功能,除此之外,还有几个方法是用来设置 flash 属性的。

具体来讲,RedirectAttributes 提供了一组 addFlashAttribute() 方法来添加 flash 属性。重新看一下 processRegistration() 方法,我们可以使用 addFlashAttribute() 将 Spitter 对象添加到模型中:

@RequestMapping(value="/register", method=POST)
public String processRegistration(Spitter spitter, RedirectAttributes model) {
    spitterRepository.save(spitter);
    model.addAttribute("username", spitter.getUsername());
    model.addFlashAttribute("spitter", spitter);
    return "redirect:/spitter/{username}";
}
@RequestMapping(value="/{username}", method=GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
    检查是否存有 key 为 spitter 的 model 属性。如果有 spitter 属性,那就什么都不用做了。
    if (!model.containsAttribute("spitter")) {
        model.addAttribute(spitterRepository.findByUsername(username));
    }
    return "profile";
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Spring 实战》笔记7:Spring MVC 的高级技术

文章字数:4.4k

本文作者:Bin

发布时间:2019-08-10, 09:37:27

最后更新:2019-08-28, 20:32:24

原始链接:http://coolview.github.io/2019/08/10/Spring/%E3%80%8ASpring%20%E5%AE%9E%E6%88%98%E3%80%8B%E7%AC%94%E8%AE%B07%EF%BC%9ASpring%20MVC%20%E7%9A%84%E9%AB%98%E7%BA%A7%E6%8A%80%E6%9C%AF/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录