SpringMVC高级技术 SpringMVC的高级技术

SpringMVC的高级技术

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/denghj618/article/details/82054754

SpringMVC的高级技术

本章的主要内容包括:

  • SpringMVC配置的替代方案
  • 处理文件上传
  • 在控制器中处理异常
  • 使用flash属性

1 SpringMVC配置的替换方案

在之前的内容中,通过扩展AbstractAnnotationConfigDispatcherServletInitializer快速搭建了SpringMVC环境。在这个便利的基础类中,假设我们需要基本的DispatcherServlet和ContextLoaderListener环境,并且Spring配置是使用Java的,而不是XML。
尽管对很多Spring应用来说,这是一种安全的假设,但是这并不一定能满足我们的要求。除了DispatcherServlet以外,我们可能还需要额外的Servlet和Filter;我们可能还需要对DispatcherServlet本身做一些额外的配置;或者我们需要将应用部署到Servlet3.0之前的容器中,那么还需要将DispatcherServlet配置到web.xml中。

1.1 自定义DispatcherServlet配置

在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器之后,就会调用customizeRegistration(),并将Servlet注册后得到的Registration.Dynamic传递进来。通过重载customizeRegistration()方法,我们可以对DispatcherServlet进行额外的配置。
借助customizeRegistration()方法中的ServletRegistration.Dynamic,我们能够完成多项任务,包括通过调用setLoadOnStartup()设置load-on-startup优先级,通过调用setInitParameter()设置初始化参数,通过调用setMultipartConfig()配置Servlet3.0对multipart的支持。

1.2 添加其他的Servlet和Filter

按照AbstractAnnotationConfigDispatcherServletInitializer的定义,它会创建DispatcherServlet和ContextLoaderListener。但是,如果需要注册其他的Servlet、Filter或Listener的话,那该怎么办?
基于Java的初始化器(initializer)的一个好处就是我们可以定义任意数量的初始化类器类。因此如果我们想往Web容器中注册其他组件的话,只需要创建一个新的初始化容器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口。
自定义Servlet代码如下:

package com.godman.spittr.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPut(req, resp);
    }
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

自定义initializer代码如下:

package com.godman.spittr.config;

import com.godman.spittr.servlet.MyServlet;
import org.springframework.web.WebApplicationInitializer;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class MyServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        ServletRegistration.Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
        myServlet.addMapping("/custom/**");
    }
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

类似地,可以创建新的WebApplicationInitializer实现来注册Listener和Filter。
自定义Filter代码如下:

package com.godman.spittr.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.IOException;

public class MyFilter implements Filter {

    private static Logger logger = LoggerFactory.getLogger(MyFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        logger.info("MyFilter innit successfully!");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        logger.info("MyFilter destroyed");
    }
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

自定义initializer代码如下:

package com.godman.spittr.config;

import com.godman.spittr.filter.MyFilter;
import org.springframework.web.WebApplicationInitializer;

import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public class MyFilterInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //注册filter
        FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
        //添加filter的映射路径
        filter.addMappingForUrlPatterns(null, false, "/custom/*");
    }
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

如果要将应用部署到支持Servlet3.0的容器中,那么WebApplicationInitializer只提供了一种通用的方式,实现在Java中注册Servlet、Filter或Listener。不过,如果只是注册Filter,并且该Filter只会映射到DispatcherServlet上的话,那么在AbstractAnnotationConfigDispatcherServletInitializer上还有一种快捷方式。所需要做的仅仅是重载Filters()方法。

@Override
protected Filter[] getServletFilters() {
    return new Filter[] {new MyFilter()};
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5

在这里没有必要声明它的映射路径,getServletFilters()方法返回的所有Filter都会映射到DispatcherServlet上。
假如需要将应用部署到不支持Servlet3.0的容器中(或者只是希望使用web.xml文件),那么完全可以通过web.xml配置SpringMVC。

1.3 在web.xml中声明DispatcherServlet

在典型的SpringMVC应用中,我们会需要DispatcherServlet和ContextLoaderListener。AbstractAnnotationConfigDispatcherServletInitializer会自动注册它们,但是在web.xml文件中就需要手动配置了。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <!--设置根上下文配置文件的位置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>

    <!--注册ContextLoaderListener-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!--注册DispatcherServlet-->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!--将DispatcherServlet映射到"/"-->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

DispatcherServlet和ContextLoaderListener都会加载一个Spring应用上下文。上下文参数contextConfigLocation指定了一个XML文件地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。DispatcherServlet会根据servlet的名字找到一个文件,并根据该文件加载应用上下文。在这里DispatcherServlet的名字是dispatcher,因此DispatcherServlet会从”/WEB-INF/dispatcher.xml”文件中加载其应用上下文。
如果希望指定DispatcherServlet配置文件的位置的话,那么可以在Servlet上指定一个contextConfigLocation初始化参数

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

要在SpringMVC中使用基于Java的配置,我们需要告诉DispatcherServlet和ContextLoaderListene使用AnnotationConfigWebApplicationContext,这是一个WebApplicationContext的实现类,它会加载Java配置类,而不是使用XML。要实现这种配置,我们可以使用contextClass上下文参数以及DispatcherServlet的初始化参数:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--使用java配置-->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>

    <!--指定根配置类-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.godman.spittr.config.RootConfig</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <!--指定DispatcherServlet配置类-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.godman.spittr.config.WebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

2 处理multipart形式的数据

multipart格式的数据会将一个表单拆分为多个部分(part),每部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制。看起来multipart请求很复杂,但在SpringMVC中处理它们却很容易。在编写控制器方法处理文件上传之前,我们必须先配置一个multipart解析器,通过它来告诉DiispatcherServlet如何读取multipart请求。

2.1 配置multipart解析器

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

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
  • StandardServletMultipartResolver:依赖于Servlet3.0对multipart请求的支持。

一般来讲,优先选择StandardServletMultipartResolver,因为它不需要再额外依赖其他的项目。但是,如果需要将应用部署到Servlet3.0之前的容器中,或还没有使用Spring3.1或更高的版本则需要使用CommonsMultipartResolver。

使用Servlet3.0解析multipart请求

兼容Servlet3.0的StandardServletMultipartResolver没有构造器参数,也没有要设置的任何属性。这样在Spring应用上下文中将其声明为bean非常简单:

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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5

如果我们需要为StandardServletMultipartResolver配置属性,如临时文件路径、限制用户上传文件大小等,需要在Servlet中指定multipart的配置。我们至少需要指定在文件上传的过程中,所写入的临时文件路径如果不设置这个最基本的配置的话,StandardServletMultipartResolver就无法正常工作。也就是说,我们需要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。
如果我们配置DispatcherServlet的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的话,我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("C:/Coding/workspace/inteliji_space/upload"));
    super.customizeRegistration(registration);
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

  • 上传文件的最大容量(以字节为单位)。默认是没有限制
  • 这个multipart请求的最大容量(以字节为单位),不会关心有多少个part以及每个part的大小。默认是没有限制
  • 在上传的过程中,如果文件大小达到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为0,也就是所上传的文件都会写入到磁盘上。

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement(“C:/Coding/workspace/inteliji_space/upload”, 2097152, 4194304, 0));
    }

如果使用web.xml配置MultipartConfigElement的话,如下:

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <!--指定DispatcherServlet配置类-->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.godman.spittr.config.WebConfig</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <!--为StandardServletMultipartResolver配置MultipartConfigElement参数-->
    <multipart-config>
        <location>C:/Coding/workspace/inteliji_space/upload</location>
        <max-file-size>2097152</max-file-size>
        <max-request-size>4194304</max-request-size>
    </multipart-config>
</servlet>

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

配置Jakarta Commons FileUpload multipart解析器

当需要将应用部署到非Servlet3.0的容器中,可以使用CommonsMultipartResolver进行替换,其声明如下:

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

      
      
  • 1
  • 2
  • 3
  • 4
  • 5

与StandardServletMultipartResolver不同的是,CommonsMultipartResolver不会强制要求设置临时文件路径。默认情况下,这个路径就是Servlet容器的临时目录,不过通过设置uploadTempDir属性,我们可以将其指定为一个不同的位置:

@Bean
public MultipartResolver multipartResolver() throws IOException {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("C:/Coding/workspace/inteliji_space/upload"));
    return multipartResolver;
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

明显这里还可以设置其他的multipart上传细节,也就是设置CommonsMultipartResolver的属性。

@Bean
public MultipartResolver multipartResolver() throws IOException {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("C:/Coding/workspace/inteliji_space/upload"));
    multipartResolver.setMaxUploadSize(1024*1024*4);
    multipartResolver.setMaxInMemorySize(0);
    return multipartResolver;
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.2 处理multipart请求

现在已经在Spring中(或Servlet容器中)配置好了对multipart请求的处理,那么接下来就可以编写控制器方法来接收上传的文件。要实现这一点,最常见的方式就是在某个控制器方法参数上添加@RequestPart注解。

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter, Errors errors, Model model){
    spitterRepository.save(spitter);
    model.addAttribute(spitter);
    if(errors.hasErrors()){
        return "registerForm";
    }
    return "redirect:/spitter/" + spitter.getUsername();
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

当提交表单时会,profilePicture属性将会给定一个byte数组,这个数组中包含了请求中对应part的数据(通过@RequestPart指定)。现在我们已经拿到了byte数组形式的图片数据,并且根据它能够得到图片的大小,但是对于其他的内容如文件内容、原始文件名称,就无法得知了。

接受MultipartFile

因此Spring提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象。

package org.springframework.web.multipart;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.InputStreamSource;

public interface MultipartFile extends InputStreamSource {
    String getName();

    String getOriginalFilename();

    String getContentType();

    boolean isEmpty();

    long getSize();

    byte[] getBytes() throws IOException;

    InputStream getInputStream() throws IOException;

    void transferTo(File var1) throws IOException, IllegalStateException;
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

将方法改为使用MultipartFile接受参数:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture")MultipartFile profilePicture, @Valid Spitter spitter, Model model) throws IOException {
    System.out.println(profilePicture.getOriginalFilename());
    profilePicture.transferTo(new File("C:/Coding/workspace/inteliji_space/upload/"+profilePicture.getOriginalFilename()));
    spitterRepository.save(spitter);
    model.addAttribute(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

以Part形式接受上传的文件

如果需要将应用部署到Servlet3.0的容器中,那么会有MultipartFile的一个替代方案。SpringMVC也能接受javax.servlet.http.Part作为控制器方法的参数。Part接口与MultipartFile非常类似:

package javax.servlet.http;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;

public interface Part {
    InputStream getInputStream() throws IOException;

    String getContentType();

    String getName();

    String getSubmittedFileName();

    long getSize();

    void write(String var1) throws IOException;

    void delete() throws IOException;

    String getHeader(String var1);

    Collection<String> getHeaders(String var1);

    Collection<String> getHeaderNames();
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在此,不再做详细说明。

3 处理异常

不管发生什么事情,Servlet请求的输出都是一个Servlet响应。如果在处理请求的时候出现了异常,那它的输出仍然会是Servlet响应。异常必须要以某种方式转换为响应。Spring提供了多种方式将异常转换为响应:

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

处理异常的最简单方式就是将其映射到HTTP状态码上,进而放到响应之中。接下来,我们看一下如何将异常映射为耨一个HTTP状态码。

3.1 将异常映射为HTTP状态码

在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。如BindException会转换为404,ConversionNotSupportedException会转化为500等。这些异常一般会由Spring自身抛出,作为DispatcherServlet处理过程中或执行校验时出现问题的结果。但是这些内置的映射对于应用所抛出的异常它们就无能为力了。不过,Spring提供了一种机制,能够通过@ResponseStatus注解将异常映射为HTTP状态码。
例如,我们自定义一个异常,如下:

package com.godman.spittr.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Spittle not found")
public class SpitteNotFoundException extends RuntimeException {
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在引入@ResponseStatus注解之后,如果控制器方法抛出了SpitteNotFoundException异常的话,响应将会具有404状态码,这是因为Spittle not found。

3.2 编写处理异常的方法

如果我们想在响应中不仅包含状态码,还要包含所产生的错误,此时的话,我们就不能将异常视为HTTP错误了,而是要按照处理请求的方式来处理异常了。
假设在保存spittle时,如果内容已经存在相同的则会抛出SpittleExistException异常。其保存方法代码如下:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(Spittle spittle, Model model){
    spittleRepository.save(spittle);
    return "redirect:/spittles";
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

现在我们为SpittleController添加一个新的方法,它会处理抛出SpittleExistException的情况:

@ExceptionHandler(SpittleExistException.class)
public String handleSpittleExist(){
    return "error/spittleExist";
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5

handleSpittleExist()方法上添加了@ExceptionHandler注解,当抛出SpittleExistException异常的时候,将会委托该方法来处理。它返回的是一个String,这与处理请求的方法是一致的,指定了要渲染的逻辑视图名。值得一提的是,对于@ExceptionHandler注解标注的方法来说,它能够处理同一个控制器中所有处理器方法所抛出的异常。

4 为控制器添加方法

如果控制中的特定切面能够运用到整个应用程序中的所有控制器中,那么将会便利很多。如果在多个控制器中抛出某个特定的异常,那么需要在所有的控制器中添加重复的@ExceptionHandler方法,或者创建一个基类,所有的控制器要扩展这个基类,从而继承通用的@ExceptionHandler方法。Spring3.2为这类方法提供了新的解决方案:控制器通知。控制器通知(controller advice)是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:

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

在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序的所有控制器中带有@RequestMapping注解的方法上。@ControllerAdvice注解本身已经使用了@Component,因此@ControllerAdvice注解标注的类将会自动被组件扫描获取到,就像带有@Component注解一样。
@ControllerAdvice注解最为实用的一个场景就是将所有的@ExceptionHandler方法集中在一个类中,这样所有控制器的异常就能在同一个地方进行一致的处理。如下:

package com.godman.spittr.handler;

import com.godman.spittr.exception.SpittleExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class AppWideExceptionHandler {

    @ExceptionHandler(SpittleExistException.class)
    public String spittleExistHandler(){
        return "error/spittleExist";
    }
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

现在,不管哪个控制器中抛出了SpittleExistException,都会调用这个spittleExistHandler()方法来处理异常。

5 跨重定向请求传递数据

当一个处理器方法完成之后,该方法所指定的模型数据就会复制到请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发过程中,请求属性能够得以保存。但是,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。
显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一些其他方案,能够从发起重定向的方法传递数据给处理重定向方法中:

  • 使用URL模板以及路径变量和/或查询参数的形式传递参数;
  • 通过flash属性发送数据

5.1 通过URL模板进行重定向

通过路径变量和查询参数传递数据看起来非常简单。

return "redirect:/spitter/" + spitter.getUsername();

      
      
  • 1
  • 2

但是,构建URL或SQL查询语句的时候,使用String连接是很危险的。吃了连接String的方式来构建重定向URL,Spring还提供了使用模板的方式来定义重定向URL。如下:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture")MultipartFile profilePicture, @Valid Spitter spitter, Model model) throws IOException {
    System.out.println(profilePicture.getOriginalFilename());
    profilePicture.transferTo(new File("C:/Coding/workspace/inteliji_space/upload/"+profilePicture.getOriginalFilename()));
    spitterRepository.save(spitter);
    model.addAttribute(spitter);
    model.addAttribute("username", spitter.getUsername());
    return "redirect:/spitter/{username}";
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在,username作为占位符填充到了URL模板中,而不是直接连接到重定向String中,所以username中所有不安全字符都会进行转义。除此之外模型中其他的原始类型值都可以添加到URL中作为查询参数。显然,这种方式只能用来发送简单的值,如String和数字的值。而更为复杂的值则需要使用flash属性。

5.2 使用flash属性

假如需要发送的是Spitter对象,如果我们只发送ID的话,那么处理重定向的方法还需要从数据库中查询才能得到Spitter对象。但是其实在重定向之前我们已经拿到了Spitter对象。如果我们能够将Spitter对象放到一个位置,使其能够在重定向中活过来,在重定向之后在将其删除,就可以实现在重定向中传递复杂对象。
Spring也认为将跨重定向存活的数据放到会话中是一个很不错的方式。但是,Spring认为我们并不需要管理这些数据,相反,Spring提供了将数据发送为flash属性(flash attribute)的功能。按照定义,flash属性会一直携带这些数据知道下一次请求才会消失。
Spring提供了通过RedirectAttributes设置flash属性的方法,这个Spring3.1引入的Model的一个子接口。RedirectAttributes提供 了一组addFlashAttribute()方法来添加flash属性

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture")MultipartFile profilePicture, @Valid Spitter spitter, RedirectAttributes model) throws IOException {
    System.out.println(profilePicture.getOriginalFilename());
    profilePicture.transferTo(new File("C:/Coding/workspace/inteliji_space/upload/"+profilePicture.getOriginalFilename()));
    spitterRepository.save(spitter);
    model.addFlashAttribute("spitter", spitter);
    model.addAttribute("username", spitter.getUsername());
    return "redirect:/spitter/{username}";
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在重定向执行之前,所有的flash属性都会赋值到会话中。在重定向之后,存在会话中的flash属性会被去除,并从会话转移到模型中。处理重定向的方法就能从模型中访问Spitter对象了。

@RequestMapping(value="/{username}", method = RequestMethod.GET)
public String showSpitterProfile(@PathVariable String username, Model model){
    if(!model.containsAttribute("spitter")){
        model.addAttribute(spitterRepository.findByUsername(username));
    }
    return "profile";
}

      
      
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里可以看到,showSpitterProfile()做的第一件事情就是检查是否存有key为spitter的model属性。如果模型中包含spitter属性就什么也不需要做了。

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/denghj618/article/details/82054754

SpringMVC的高级技术

本章的主要内容包括:

  • SpringMVC配置的替代方案
  • 处理文件上传
  • 在控制器中处理异常
  • 使用flash属性

1 SpringMVC配置的替换方案

在之前的内容中,通过扩展AbstractAnnotationConfigDispatcherServletInitializer快速搭建了SpringMVC环境。在这个便利的基础类中,假设我们需要基本的DispatcherServlet和ContextLoaderListener环境,并且Spring配置是使用Java的,而不是XML。
尽管对很多Spring应用来说,这是一种安全的假设,但是这并不一定能满足我们的要求。除了DispatcherServlet以外,我们可能还需要额外的Servlet和Filter;我们可能还需要对DispatcherServlet本身做一些额外的配置;或者我们需要将应用部署到Servlet3.0之前的容器中,那么还需要将DispatcherServlet配置到web.xml中。

1.1 自定义DispatcherServlet配置

在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器之后,就会调用customizeRegistration(),并将Servlet注册后得到的Registration.Dynamic传递进来。通过重载customizeRegistration()方法,我们可以对DispatcherServlet进行额外的配置。
借助customizeRegistration()方法中的ServletRegistration.Dynamic,我们能够完成多项任务,包括通过调用setLoadOnStartup()设置load-on-startup优先级,通过调用setInitParameter()设置初始化参数,通过调用setMultipartConfig()配置Servlet3.0对multipart的支持。

1.2 添加其他的Servlet和Filter

按照AbstractAnnotationConfigDispatcherServletInitializer的定义,它会创建DispatcherServlet和ContextLoaderListener。但是,如果需要注册其他的Servlet、Filter或Listener的话,那该怎么办?
基于Java的初始化器(initializer)的一个好处就是我们可以定义任意数量的初始化类器类。因此如果我们想往Web容器中注册其他组件的话,只需要创建一个新的初始化容器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口。
自定义Servlet代码如下:

package com.godman.spittr.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPut(req, resp);
    }
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

自定义initializer代码如下:

package com.godman.spittr.config;

import com.godman.spittr.servlet.MyServlet;
import org.springframework.web.WebApplicationInitializer;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class MyServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        ServletRegistration.Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
        myServlet.addMapping("/custom/**");
    }
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

类似地,可以创建新的WebApplicationInitializer实现来注册Listener和Filter。
自定义Filter代码如下:

package com.godman.spittr.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.IOException;

public class MyFilter implements Filter {

    private static Logger logger = LoggerFactory.getLogger(MyFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        logger.info("MyFilter innit successfully!");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        logger.info("MyFilter destroyed");
    }
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

自定义initializer代码如下:

package com.godman.spittr.config;

import com.godman.spittr.filter.MyFilter;
import org.springframework.web.WebApplicationInitializer;

import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public class MyFilterInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //注册filter
        FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
        //添加filter的映射路径
        filter.addMappingForUrlPatterns(null, false, "/custom/*");
    }
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

如果要将应用部署到支持Servlet3.0的容器中,那么WebApplicationInitializer只提供了一种通用的方式,实现在Java中注册Servlet、Filter或Listener。不过,如果只是注册Filter,并且该Filter只会映射到DispatcherServlet上的话,那么在AbstractAnnotationConfigDispatcherServletInitializer上还有一种快捷方式。所需要做的仅仅是重载Filters()方法。

@Override
protected Filter[] getServletFilters() {
    return new Filter[] {new MyFilter()};
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

在这里没有必要声明它的映射路径,getServletFilters()方法返回的所有Filter都会映射到DispatcherServlet上。
假如需要将应用部署到不支持Servlet3.0的容器中(或者只是希望使用web.xml文件),那么完全可以通过web.xml配置SpringMVC。

1.3 在web.xml中声明DispatcherServlet

在典型的SpringMVC应用中,我们会需要DispatcherServlet和ContextLoaderListener。AbstractAnnotationConfigDispatcherServletInitializer会自动注册它们,但是在web.xml文件中就需要手动配置了。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <!--设置根上下文配置文件的位置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>

    <!--注册ContextLoaderListener-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!--注册DispatcherServlet-->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!--将DispatcherServlet映射到"/"-->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

DispatcherServlet和ContextLoaderListener都会加载一个Spring应用上下文。上下文参数contextConfigLocation指定了一个XML文件地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。DispatcherServlet会根据servlet的名字找到一个文件,并根据该文件加载应用上下文。在这里DispatcherServlet的名字是dispatcher,因此DispatcherServlet会从”/WEB-INF/dispatcher.xml”文件中加载其应用上下文。
如果希望指定DispatcherServlet配置文件的位置的话,那么可以在Servlet上指定一个contextConfigLocation初始化参数

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

要在SpringMVC中使用基于Java的配置,我们需要告诉DispatcherServlet和ContextLoaderListene使用AnnotationConfigWebApplicationContext,这是一个WebApplicationContext的实现类,它会加载Java配置类,而不是使用XML。要实现这种配置,我们可以使用contextClass上下文参数以及DispatcherServlet的初始化参数:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--使用java配置-->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>

    <!--指定根配置类-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.godman.spittr.config.RootConfig</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <!--指定DispatcherServlet配置类-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.godman.spittr.config.WebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

2 处理multipart形式的数据

multipart格式的数据会将一个表单拆分为多个部分(part),每部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制。看起来multipart请求很复杂,但在SpringMVC中处理它们却很容易。在编写控制器方法处理文件上传之前,我们必须先配置一个multipart解析器,通过它来告诉DiispatcherServlet如何读取multipart请求。

2.1 配置multipart解析器

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

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
  • StandardServletMultipartResolver:依赖于Servlet3.0对multipart请求的支持。

一般来讲,优先选择StandardServletMultipartResolver,因为它不需要再额外依赖其他的项目。但是,如果需要将应用部署到Servlet3.0之前的容器中,或还没有使用Spring3.1或更高的版本则需要使用CommonsMultipartResolver。

使用Servlet3.0解析multipart请求

兼容Servlet3.0的StandardServletMultipartResolver没有构造器参数,也没有要设置的任何属性。这样在Spring应用上下文中将其声明为bean非常简单:

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

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

如果我们需要为StandardServletMultipartResolver配置属性,如临时文件路径、限制用户上传文件大小等,需要在Servlet中指定multipart的配置。我们至少需要指定在文件上传的过程中,所写入的临时文件路径如果不设置这个最基本的配置的话,StandardServletMultipartResolver就无法正常工作。也就是说,我们需要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。
如果我们配置DispatcherServlet的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的话,我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("C:/Coding/workspace/inteliji_space/upload"));
    super.customizeRegistration(registration);
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

  • 上传文件的最大容量(以字节为单位)。默认是没有限制
  • 这个multipart请求的最大容量(以字节为单位),不会关心有多少个part以及每个part的大小。默认是没有限制
  • 在上传的过程中,如果文件大小达到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为0,也就是所上传的文件都会写入到磁盘上。

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement(“C:/Coding/workspace/inteliji_space/upload”, 2097152, 4194304, 0));
    }

如果使用web.xml配置MultipartConfigElement的话,如下:

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <!--指定DispatcherServlet配置类-->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.godman.spittr.config.WebConfig</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <!--为StandardServletMultipartResolver配置MultipartConfigElement参数-->
    <multipart-config>
        <location>C:/Coding/workspace/inteliji_space/upload</location>
        <max-file-size>2097152</max-file-size>
        <max-request-size>4194304</max-request-size>
    </multipart-config>
</servlet>

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

配置Jakarta Commons FileUpload multipart解析器

当需要将应用部署到非Servlet3.0的容器中,可以使用CommonsMultipartResolver进行替换,其声明如下:

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

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

与StandardServletMultipartResolver不同的是,CommonsMultipartResolver不会强制要求设置临时文件路径。默认情况下,这个路径就是Servlet容器的临时目录,不过通过设置uploadTempDir属性,我们可以将其指定为一个不同的位置:

@Bean
public MultipartResolver multipartResolver() throws IOException {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("C:/Coding/workspace/inteliji_space/upload"));
    return multipartResolver;
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

明显这里还可以设置其他的multipart上传细节,也就是设置CommonsMultipartResolver的属性。

@Bean
public MultipartResolver multipartResolver() throws IOException {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("C:/Coding/workspace/inteliji_space/upload"));
    multipartResolver.setMaxUploadSize(1024*1024*4);
    multipartResolver.setMaxInMemorySize(0);
    return multipartResolver;
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.2 处理multipart请求

现在已经在Spring中(或Servlet容器中)配置好了对multipart请求的处理,那么接下来就可以编写控制器方法来接收上传的文件。要实现这一点,最常见的方式就是在某个控制器方法参数上添加@RequestPart注解。

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter, Errors errors, Model model){
    spitterRepository.save(spitter);
    model.addAttribute(spitter);
    if(errors.hasErrors()){
        return "registerForm";
    }
    return "redirect:/spitter/" + spitter.getUsername();
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

当提交表单时会,profilePicture属性将会给定一个byte数组,这个数组中包含了请求中对应part的数据(通过@RequestPart指定)。现在我们已经拿到了byte数组形式的图片数据,并且根据它能够得到图片的大小,但是对于其他的内容如文件内容、原始文件名称,就无法得知了。

接受MultipartFile

因此Spring提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象。

package org.springframework.web.multipart;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.InputStreamSource;

public interface MultipartFile extends InputStreamSource {
    String getName();

    String getOriginalFilename();

    String getContentType();

    boolean isEmpty();

    long getSize();

    byte[] getBytes() throws IOException;

    InputStream getInputStream() throws IOException;

    void transferTo(File var1) throws IOException, IllegalStateException;
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

将方法改为使用MultipartFile接受参数:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture")MultipartFile profilePicture, @Valid Spitter spitter, Model model) throws IOException {
    System.out.println(profilePicture.getOriginalFilename());
    profilePicture.transferTo(new File("C:/Coding/workspace/inteliji_space/upload/"+profilePicture.getOriginalFilename()));
    spitterRepository.save(spitter);
    model.addAttribute(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

以Part形式接受上传的文件

如果需要将应用部署到Servlet3.0的容器中,那么会有MultipartFile的一个替代方案。SpringMVC也能接受javax.servlet.http.Part作为控制器方法的参数。Part接口与MultipartFile非常类似:

package javax.servlet.http;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;

public interface Part {
    InputStream getInputStream() throws IOException;

    String getContentType();

    String getName();

    String getSubmittedFileName();

    long getSize();

    void write(String var1) throws IOException;

    void delete() throws IOException;

    String getHeader(String var1);

    Collection<String> getHeaders(String var1);

    Collection<String> getHeaderNames();
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

在此,不再做详细说明。

3 处理异常

不管发生什么事情,Servlet请求的输出都是一个Servlet响应。如果在处理请求的时候出现了异常,那它的输出仍然会是Servlet响应。异常必须要以某种方式转换为响应。Spring提供了多种方式将异常转换为响应:

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

处理异常的最简单方式就是将其映射到HTTP状态码上,进而放到响应之中。接下来,我们看一下如何将异常映射为耨一个HTTP状态码。

3.1 将异常映射为HTTP状态码

在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。如BindException会转换为404,ConversionNotSupportedException会转化为500等。这些异常一般会由Spring自身抛出,作为DispatcherServlet处理过程中或执行校验时出现问题的结果。但是这些内置的映射对于应用所抛出的异常它们就无能为力了。不过,Spring提供了一种机制,能够通过@ResponseStatus注解将异常映射为HTTP状态码。
例如,我们自定义一个异常,如下:

package com.godman.spittr.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Spittle not found")
public class SpitteNotFoundException extends RuntimeException {
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在引入@ResponseStatus注解之后,如果控制器方法抛出了SpitteNotFoundException异常的话,响应将会具有404状态码,这是因为Spittle not found。

3.2 编写处理异常的方法

如果我们想在响应中不仅包含状态码,还要包含所产生的错误,此时的话,我们就不能将异常视为HTTP错误了,而是要按照处理请求的方式来处理异常了。
假设在保存spittle时,如果内容已经存在相同的则会抛出SpittleExistException异常。其保存方法代码如下:

@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(Spittle spittle, Model model){
    spittleRepository.save(spittle);
    return "redirect:/spittles";
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

现在我们为SpittleController添加一个新的方法,它会处理抛出SpittleExistException的情况:

@ExceptionHandler(SpittleExistException.class)
public String handleSpittleExist(){
    return "error/spittleExist";
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

handleSpittleExist()方法上添加了@ExceptionHandler注解,当抛出SpittleExistException异常的时候,将会委托该方法来处理。它返回的是一个String,这与处理请求的方法是一致的,指定了要渲染的逻辑视图名。值得一提的是,对于@ExceptionHandler注解标注的方法来说,它能够处理同一个控制器中所有处理器方法所抛出的异常。

4 为控制器添加方法

如果控制中的特定切面能够运用到整个应用程序中的所有控制器中,那么将会便利很多。如果在多个控制器中抛出某个特定的异常,那么需要在所有的控制器中添加重复的@ExceptionHandler方法,或者创建一个基类,所有的控制器要扩展这个基类,从而继承通用的@ExceptionHandler方法。Spring3.2为这类方法提供了新的解决方案:控制器通知。控制器通知(controller advice)是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:

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

在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序的所有控制器中带有@RequestMapping注解的方法上。@ControllerAdvice注解本身已经使用了@Component,因此@ControllerAdvice注解标注的类将会自动被组件扫描获取到,就像带有@Component注解一样。
@ControllerAdvice注解最为实用的一个场景就是将所有的@ExceptionHandler方法集中在一个类中,这样所有控制器的异常就能在同一个地方进行一致的处理。如下:

package com.godman.spittr.handler;

import com.godman.spittr.exception.SpittleExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class AppWideExceptionHandler {

    @ExceptionHandler(SpittleExistException.class)
    public String spittleExistHandler(){
        return "error/spittleExist";
    }
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

现在,不管哪个控制器中抛出了SpittleExistException,都会调用这个spittleExistHandler()方法来处理异常。

5 跨重定向请求传递数据

当一个处理器方法完成之后,该方法所指定的模型数据就会复制到请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发过程中,请求属性能够得以保存。但是,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。
显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一些其他方案,能够从发起重定向的方法传递数据给处理重定向方法中:

  • 使用URL模板以及路径变量和/或查询参数的形式传递参数;
  • 通过flash属性发送数据

5.1 通过URL模板进行重定向

通过路径变量和查询参数传递数据看起来非常简单。

return "redirect:/spitter/" + spitter.getUsername();

  
  
  • 1
  • 2

但是,构建URL或SQL查询语句的时候,使用String连接是很危险的。吃了连接String的方式来构建重定向URL,Spring还提供了使用模板的方式来定义重定向URL。如下:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture")MultipartFile profilePicture, @Valid Spitter spitter, Model model) throws IOException {
    System.out.println(profilePicture.getOriginalFilename());
    profilePicture.transferTo(new File("C:/Coding/workspace/inteliji_space/upload/"+profilePicture.getOriginalFilename()));
    spitterRepository.save(spitter);
    model.addAttribute(spitter);
    model.addAttribute("username", spitter.getUsername());
    return "redirect:/spitter/{username}";
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在,username作为占位符填充到了URL模板中,而不是直接连接到重定向String中,所以username中所有不安全字符都会进行转义。除此之外模型中其他的原始类型值都可以添加到URL中作为查询参数。显然,这种方式只能用来发送简单的值,如String和数字的值。而更为复杂的值则需要使用flash属性。

5.2 使用flash属性

假如需要发送的是Spitter对象,如果我们只发送ID的话,那么处理重定向的方法还需要从数据库中查询才能得到Spitter对象。但是其实在重定向之前我们已经拿到了Spitter对象。如果我们能够将Spitter对象放到一个位置,使其能够在重定向中活过来,在重定向之后在将其删除,就可以实现在重定向中传递复杂对象。
Spring也认为将跨重定向存活的数据放到会话中是一个很不错的方式。但是,Spring认为我们并不需要管理这些数据,相反,Spring提供了将数据发送为flash属性(flash attribute)的功能。按照定义,flash属性会一直携带这些数据知道下一次请求才会消失。
Spring提供了通过RedirectAttributes设置flash属性的方法,这个Spring3.1引入的Model的一个子接口。RedirectAttributes提供 了一组addFlashAttribute()方法来添加flash属性

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture")MultipartFile profilePicture, @Valid Spitter spitter, RedirectAttributes model) throws IOException {
    System.out.println(profilePicture.getOriginalFilename());
    profilePicture.transferTo(new File("C:/Coding/workspace/inteliji_space/upload/"+profilePicture.getOriginalFilename()));
    spitterRepository.save(spitter);
    model.addFlashAttribute("spitter", spitter);
    model.addAttribute("username", spitter.getUsername());
    return "redirect:/spitter/{username}";
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在重定向执行之前,所有的flash属性都会赋值到会话中。在重定向之后,存在会话中的flash属性会被去除,并从会话转移到模型中。处理重定向的方法就能从模型中访问Spitter对象了。

@RequestMapping(value="/{username}", method = RequestMethod.GET)
public String showSpitterProfile(@PathVariable String username, Model model){
    if(!model.containsAttribute("spitter")){
        model.addAttribute(spitterRepository.findByUsername(username));
    }
    return "profile";
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里可以看到,showSpitterProfile()做的第一件事情就是检查是否存有key为spitter的model属性。如果模型中包含spitter属性就什么也不需要做了。

猜你喜欢

转载自blog.csdn.net/weixin_41374449/article/details/82148425