Spring——SpringMVC(三)

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

本文主要依据《Spring实战》第七章内容进行总结

前两节中,我们主要介绍了Spring MVC的控制器和视图解析器,通过上面两节内容,我们对Spring MVC已经有了一个初步的了解,也能够使用Spring MVC进行基本的Web层编程了,但是Spring MVC的知识还远未结束,在本节,我们将继续介绍Spring MVC的一些高级技术。

1、Spring MVC配置的替代方案

1.1、自定义DispatcherServlet配置

Spring MVC(一) 小节中,我们介绍了如何使用Java代码配置DispatcherServlet,我们只需要扩展AbstractAnnotationConfigDispatcherServletInitializer类即可,我们重载了三个abstract方法,实际上还有更多的方法可以进行重载,从而实现额外的配置,其中一个方法就是customizeRegistration()。

在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中后,就会调用customizeRegistration()方法,并将Servlet注册后得到的ServletRegistration.Dynamic传递进来。通过重载customizeRegistration()方法,我们就可以对DispatcherServlet进行额外的配置,例如,通过调用Dynamic的setLoadOnStartup()设置load-on-startup优先级,通过调用setInitParameter()设置初始化参数,还有可以通过调用setMultipartConfig()来设置对multipart的支持,我们可以看如下的代码:

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

上面这段代码就是通过调用Dynamic的setMultipartConfig()来设置对multipart的支持,并设置上传文件的临时目录为f://tmp,具体的multipart功能我们在后面的内容会进行介绍。

1.2、添加其他的Servlet和Filter

按照AbstractAnnotationConfigDispatcherServletInitializer的定义,它会创建DispatcherServlet和ContextLoaderListener,但是如果我们想要注册其他的Servlet、Filter和Listener,那该怎么办呢?

基于Java的初始化器(initializer)的一个好处就在于我们可以定义任意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件的话,只需要创建一个新的初始化器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口。例如:

public class MyServletInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException {
        //1、注册Servlet
        Dynamic d = servletContext.addServlet("myServlet", MyServlet.class);
        //2、添加映射
        d.addMapping("/myservlet/**");
    }

}

上面这段代码就是新注册一个名为myServlet的Servlet,然后将其映射到一个路径上。我们也可以通过这种方式手动注册DispatcherServlet,但是这并没有必要,因为AbstractAnnotationConfigDispatcherServletInitializer能够很好地完成这项任务。

同样的,我们也可以使用这种方式来注册Filter、Listener:

public class MyFilterInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException {
        //1、注册Filter
        Dynamic d = servletContext.addFilter("myFilter", MyFilter.class);
        //2、添加映射路径
        d.addMappingForUrlPatterns(null, false, "/");

    }

}

需要注意的是,上面通过实现WebApplicationInitializer来注册Servlet、Filter、Listener只在Servlet 3.0以后的版本中才适用,如果容器不支持Servlet 3.0及以上的版本,我们不能通过以上的方式注册组件。在上面的例子中,我们注册了一过滤器MyFilter,如果这个Filter只会映射到DispatcherServlet上的话,AbstractAnnotationConfigDispatcherServletInitializer提供了一个更为快捷的方式,就是重载getServletFilters()方法:

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

这个方法返回一个Filter的数组,这个数组中的所有Filter都会映射到DispatcherServlet上。

1.3、在web.xml中声明DispatcherServlet

在典型的Spring MVC应用中,我们会需要DispatcherServlet和ContextLoaderListener,AbstractAnnotationConfigDispatcherServletInitializer会自动注册它们,但如果我们使用web.xml来进行配置的话,我们就需要手动注册它们了。下面的实例是一个简单的web.xml文件,它按照传统的方式搭建了DispatcherServelt和ContextLoaderListener。

<?xml version="1.0" encoding="UTF-8"?>  
<web-app version="3.0" 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_3_0.xsd">      
    <!-- 设置根上下文配置文件位置 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:root.xml</param-value>
    </context-param>

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

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

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

在这个配置文件中,上下文参数contextConfigLocation指定了一个XML文件的地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文,在上面的例子中,Servlet的名字是appServlet,所以DispatcherServlet会从“/WEB-INF/appServlet-servlet.xml”文件中加载其应用上下文。

如果需要指定DIspatcherServlet配置文件的位置的话,那么可以在Servlet上指定一个contextConfigLocation初始化参数,例如:

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:appServlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

在上面的例子中,DispatherServlet将会去类路径下的appServlet.xml中加载应用上下文的bean。

在上面的两个例子里,我们都是通过xml的形式配置Spring应用上下文的bean的,如果我们使用Java配置的形式配置bean,然后再在web.xml配置DispathcerServlet和ContextLoaderListener,那么它们该如何加载Java配置中的bean呢?

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

下面的web.xml配置就是基于Java配置的Spring MVC:

<?xml version="1.0" encoding="UTF-8"?>  
<web-app version="3.0" 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_3_0.xsd">      

    <!-- 使用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>config.WebConfig</param-value>
    </context-param>

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

    <!-- 注册DispathcherServlet -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <!-- 使用Java配置 -->
        <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>config.WebConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

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

2、处理multipart形式的数据

在Web应用中,允许用户上传文件的功能是很常见的,一般表单提交所形成的请求结果是很简单的,就是以”&”符分割的多个name-value对,而对于要上传文件的表单,表单的类型为multipart,它会将一个表单分割为多个部分(part),每个部分对应一个输入域,在一般的表单输入域中,它所对应的部分会放置文本型数据,如果是上传文件的话,它所对应的部分可以是二进制。

在Spring MVC中处理multipart请求很容易,在编写控制器方法处理文件上传之前,我们必须配置一个multipart解析器,通过它来告诉DispatcherServlet该如何读取multipart请求。

2.1、配置multipart解析器

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

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
  • StandardServletMultipartResolver:依赖于Servlet 3.0对multipart请求的支持,始于Spring 3.1。

其中StandardServletMultipartResolver使用Servlet提供的功能支持,不需要依赖于任何其他项目,但是如果我们需要将应用部署到Servlet 3.0之前的容器中,或者还没有使用Spring 3.1以及更高的版本,我们就需要使用CommonsMultipartResolver。下面,我们分别来看一下这两个multipart解析器。

2.1.1、StandardServletMultipartResolver

兼容Servlet 3.0的StandardServletMultipartResolver没有构造器参数,也没有要设置的属性:

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

这个类不需要设置任何的构造器参数和属性,我们可能会担心这个multipart解析器的功能是否会受到限制?比如我们该如何设置文件上传的路径?该如何限制用户上传的文件大小?

实际上,我们无须在Spring中配置StandardServletMultipartResolver,而是在Servlet中指定multipart的配置。因为StandardServletMultipartResolver使用的是Servlet所提供的功能支持,所以不用在StandardServletMultipartResolver中配置,而是在Servlet中配置。我们必须在web.xml中或者Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。

在1.1节中,我们已经看到了如何使用Servlet初始化类来配置multipart,只需要重载customizeRegistration()方法,调用ServletRegistration.Dynamic的setMultipartConfig()方法,传入一个MultipartConfigElement实例即可:

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

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

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

例如,我们想限制文件大小不超过2MB,整个请求不超过4MB,而且所有的文件都要写到磁盘中,而且所有的文件都要写入到磁盘中,下面的代码设置了这些临界值:

@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setMultipartConfig(new MultipartConfigElement("f://tmp", 2097152, 4194304, 0));
}

如果我们使用web.xml配置MultipartElement的话,那么可以使用<servlet> 中的<multipart-config> 元素:

<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>f://tmp</location>
        <max-file-size>2097152</max-file-size>
        <max-request-size>4194304</max-request-size>
        <file-size-threshold>0</file-size-threshold>
    </multipart-config>
</servlet>

2.1.2、CommonsMultipartResolver

我们在使用CommonsMultipartResolver作为multipart解析器时,需要引入commons-fileupload-1.3.1.jar和commons-io-2.2.jar,如果我们的项目使用了Maven,只需要在pom.xml文件中加入以下依赖即可:

<dependency>  
    <groupId>commons-fileupload</groupId>  
    <artifactId>commons-fileupload</artifactId>  
    <version>1.3.1</version>  
</dependency> 

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

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

乍一看,这样的声明和StandardServletMultipartResolver声明是一样的,都没有指定构造器参数和属性,实际上,CommonsMultipartResolver不会强制要求设置临时文件路径,默认情况下,这个路径就是Servlet容器的临时目录,不过通过设置uploadTempDir属性,我们可以将其指定为一个不同的位置。实际上,我们还可以通过CommonsMultipartResolver的setMaxUploadSize()方法设置上传文件的大小限制,setMaxInMemorySize()方法设置文件是否全部写入磁盘:

@Bean
public MultipartResolver multipartResolver() throws Exception {
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setUploadTempDir(new FileSystemResource("f://tmp"));
    multipartResolver.setMaxUploadSize(2097152);
    multipartResolver.setMaxInMemorySize(0);

    return multipartResolver;
}

与MultipartConfigElement不同的是,我们无法设定multipart请求整体的最大容量。

2.2、处理multipart请求

在上面一节中,我们已经配置好了对multipart请求的处理,现在我们可以在控制器方法中接收上传的文件了,最常见的方式就是在某个控制器方法参数上添加@RequestPart注解,下面我们来看几种接收文件的方式。

2.2.1、使用byte[]接收上传的文件

假设我们需要一个页面来上传每个学生的照片,我们可以编写如下的JSP:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>上传照片</title>
</head>
<body>
    <h1>请上传学生照片</h1>
    <form action="/uploadStudentPicture" method="POST" enctype="multipart/form-data">
        <input type="file" name="picture"/><br/>
        <input type="submit" value="提交" />
    </form>
</body>
</html>

在这个JSP页面中,我们将<form> 表单的enctype属性设置为multipart/form-data,这样浏览器就能以multipart数据的形式提交表单,而不是以表单数据的形式提交,在multipart中,每个输入域都会对应一个part。另外,在这个页面中,我们使用<input> 标签,并将其type属性设置为file,这样我们就能够在页面上传文件了,最终显示的页面形式如下:

这里写图片描述

页面完成之后,我们需要在控制器中添加处理文件的方法:

@RequestMapping(value="/uploadStudentPicture",method=RequestMethod.POST)
public String uploadStudentPicture(@RequestPart("picture") byte[] picture) {
    System.out.println(picture.length);

    return null;
}

在这个案例中,处理文件的方法只是简单地将文件的字节大小打印出来。我们在处理方法中传入一个byte数组,并使用@RequestPart注解进行标注,当注册表单提交的时候,picture属性将会给定一个byte数组,这个数组中包含了请求中对应part的数据(通过@RequestPart指定),如果用户提交表单的时候没有选择文件,那么这个数组会是空(不是null)。获取到图片数据后,uploadStudentPicture()方法就是将文件保存到某个位置了。

我们来测试一下上面的案例,假设我们有一个图片,图片大小为143785个字节,具体信息如下:

这里写图片描述

我们现将其上传,点击提交按钮,查看后台日志,可以看到,日志正确打印出了文件的大小:

……

五月 24, 2018 3:01:01 下午 org.apache.catalina.startup.Catalina start
信息: Server startup in 7068 ms
143785

另外,因为我们在multipart解析器中配置的临时文件路径为f://tmp,所以,我们在该路径下能够看到已经上传的文件:

临时目录

在这里我们看到,上传的文件文件类型被改成了tmp,而且文件名称也被改成了我们认不出的信息,尽管我们获取到了byte数组形式的图片数据,并且我们能够获取到它的大小,但是关于它的其他信息,我们就一无所知了,我们不知道文件名是什么,甚至文件的类型是什么也不知道,同时上传的文件信息也被修改了,我们无法得知正确的文件信息,我们需要使用其他的方式来获取文件的更多信息。

2.2.2、使用MultipartFile接收上传的文件

使用上传文件的原始byte比较简单但功能有限,所以Spring还提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象,如下是MultipartFile接口的定义:

public interface MultipartFile extends InputStreamSource {

    /**
     * Return the name of the parameter in the multipart form.
     * @return the name of the parameter (never {@code null} or empty)
     */
    String getName();

    /**
     * Return the original filename in the client's filesystem.
     * <p>This may contain path information depending on the browser used,
     * but it typically will not with any other than Opera.
     * @return the original filename, or the empty String if no file has been chosen
     * in the multipart form, or {@code null} if not defined or not available
     */
    String getOriginalFilename();

    /**
     * Return the content type of the file.
     * @return the content type, or {@code null} if not defined
     * (or no file has been chosen in the multipart form)
     */
    String getContentType();

    /**
     * Return whether the uploaded file is empty, that is, either no file has
     * been chosen in the multipart form or the chosen file has no content.
     */
    boolean isEmpty();

    /**
     * Return the size of the file in bytes.
     * @return the size of the file, or 0 if empty
     */
    long getSize();

    /**
     * Return the contents of the file as an array of bytes.
     * @return the contents of the file as bytes, or an empty byte array if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    byte[] getBytes() throws IOException;

    /**
     * Return an InputStream to read the contents of the file from.
     * The user is responsible for closing the stream.
     * @return the contents of the file as stream, or an empty stream if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    @Override
    InputStream getInputStream() throws IOException;

    /**
     * Transfer the received file to the given destination file.
     * <p>This may either move the file in the filesystem, copy the file in the
     * filesystem, or save memory-held contents to the destination file.
     * If the destination file already exists, it will be deleted first.
     * <p>If the file has been moved in the filesystem, this operation cannot
     * be invoked again. Therefore, call this method just once to be able to
     * work with any storage mechanism.
     * <p><strong>Note:</strong> when using Servlet 3.0 multipart support you
     * need to configure the location relative to which files will be copied
     * as explained in {@link javax.servlet.http.Part#write}.
     * @param dest the destination file
     * @throws IOException in case of reading or writing errors
     * @throws IllegalStateException if the file has already been moved
     * in the filesystem and is not available anymore for another transfer
     */
    void transferTo(File dest) throws IOException, IllegalStateException;

}

我们可以看到,MultipartFile提供了获取上传文件byte的方式,还能够获取原始的文件名、大小以及内容类型,它还提供了一个InputStream,用来将文件数据以流的方式进行读取。另外,MultipartFile提供了一个transferTo()方法它能够帮我们将上传的文件写入到文件系统中,这样我们可以修改一下上面的方法,将上传的图片写入到文件系统中:

@RequestMapping(value="/uploadStudentPicture",method=RequestMethod.POST)
public String uploadStudentPicture(@RequestPart("picture") MultipartFile picture) throws Exception {
    picture.transferTo(new File("f:/tmp/" + picture.getOriginalFilename()));

    return null;
}

在这里,我们将文件上传到f://tmp目录下,这样再测试应用,点击提交按钮之后,我们在f://tmp目录下看到如下内容:

乱码文件名

这一次,文件类型是正确的,为jpg格式,但是文件名被转换成了乱码,我们需要在web.xml中加一个Spring的转码拦截器,转成项目的默认编码格式:

<filter>   
    <filter-name>SpringCharacterEncodingFilter</filter-name>   
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>   
    <init-param>   
        <param-name>encoding</param-name>   
        <param-value>UTF-8</param-value>   
    </init-param>   
</filter>   
<filter-mapping>   
    <filter-name>SpringCharacterEncodingFilter</filter-name>   
    <url-pattern>/*</url-pattern>   
</filter-mapping> 

这样我们就能够获取到正确的文件名了:

正确编码的文件名

2.2.3、使用Part接收上传的文件

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

@RequestMapping(value="/uploadStudentPicture",method=RequestMethod.POST)
public String uploadStudentPicture(@RequestPart("picture") Part picture) throws Exception {
    picture.write("f:/tmp/" + picture.getSubmittedFileName());

    return null;
}

我们可以看到,Part接口的方法调用好像和MultipartFile是类似的,实际上,Part接口和Multipart并没有太大的差别:

public interface Part {

    /**
     * Gets the content of this part as an <tt>InputStream</tt>
     * 
     * @return The content of this part as an <tt>InputStream</tt>
     * @throws IOException If an error occurs in retrieving the contet
     * as an <tt>InputStream</tt>
     */
    public InputStream getInputStream() throws IOException;

    /**
     * Gets the content type of this part.
     *
     * @return The content type of this part.
     */
    public String getContentType();

    /**
     * Gets the name of this part
     *
     * @return The name of this part as a <tt>String</tt>
     */
    public String getName();

    /**
     * Gets the file name specified by the client
     *
     * @return the submitted file name
     *
     * @since Servlet 3.1
     */
    public String getSubmittedFileName();

    /**
     * Returns the size of this fille.
     *
     * @return a <code>long</code> specifying the size of this part, in bytes.
     */
    public long getSize();

    /**
     * A convenience method to write this uploaded item to disk.
     * 
     * <p>This method is not guaranteed to succeed if called more than once for
     * the same part. This allows a particular implementation to use, for
     * example, file renaming, where possible, rather than copying all of the
     * underlying data, thus gaining a significant performance benefit.
     *
     * @param fileName the name of the file to which the stream will be
     * written. The file is created relative to the location as
     * specified in the MultipartConfig
     *
     * @throws IOException if an error occurs.
     */
    public void write(String fileName) throws IOException;

    /**
     * Deletes the underlying storage for a file item, including deleting any
     * associated temporary disk file.
     *
     * @throws IOException if an error occurs.
     */
    public void delete() throws IOException;

    /**
     *
     * Returns the value of the specified mime header
     * as a <code>String</code>. If the Part did not include a header
     * of the specified name, this method returns <code>null</code>.
     * If there are multiple headers with the same name, this method
     * returns the first header in the part.
     * The header name is case insensitive. You can use
     * this method with any request header.
     *
     * @param name      a <code>String</code> specifying the
     *              header name
     *
     * @return          a <code>String</code> containing the
     *              value of the requested
     *              header, or <code>null</code>
     *              if the part does not
     *              have a header of that name
     */
    public String getHeader(String name);

    /**
     * Gets the values of the Part header with the given name.
     *
     * <p>Any changes to the returned <code>Collection</code> must not 
     * affect this <code>Part</code>.
     *
     * <p>Part header names are case insensitive.
     *
     * @param name the header name whose values to return
     *
     * @return a (possibly empty) <code>Collection</code> of the values of
     * the header with the given name
     */
    public Collection<String> getHeaders(String name);

    /**
     * Gets the header names of this Part.
     *
     * <p>Some servlet containers do not allow
     * servlets to access headers using this method, in
     * which case this method returns <code>null</code>
     *
     * <p>Any changes to the returned <code>Collection</code> must not 
     * affect this <code>Part</code>.
     *
     * @return a (possibly empty) <code>Collection</code> of the header
     * names of this Part
     */
    public Collection<String> getHeaderNames();

}

可以看到,Part方法的名称和MultipartFile方法的名称是差不多的,有些比较类似,例如getSubmittedFileName()对应getOriginalName(),write()类似于transferTo()等等。

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

3、处理异常

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

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

3.1、将异常映射为HTTP状态码

在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码,下表中列出了这些映射关系:

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

上表中的异常一般会由Spring自身抛出,作为DispatcherServlet处理过程中或执行校验时出现问题的结果。例如,如果DispatcherServlet无法找到适合处理请求的控制器方法,那么将会抛出NoSuchRequestHandlingMethodException异常,最终的结果就是产生404状态码的响应。尽管这些内置映射很有用,但是对于应用抛出的异常它们就无法处理了,Spring提供了一种机制,能够通过@ResponseStatus注解将异常映射为HTTP状态码。

3.2、将异常映射为HTTP状态码

我们通过一个案例来讲解如何使用@ResponseStatus注解将异常映射为HTTP状态码,假设我们使用路径参数查询学生信息,如果查询不到相关的学生信息,则抛出一个无此学生的异常StudentNotExistsException:

@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable("id") int id, Model model) {
    Student s = new Student().getStudentById(id);

    if(null != s) {
        model.addAttribute("student", s);
    } else {
        throw new StudentNotExistsException();
    }

    return "student";
}

我们再定义一下StudentNotExistsException:

public class StudentNotExistsException extends RuntimeException {
}

假设我们现在有如下四个学生:

Student s1 = new Student(1, "张三", "男");
Student s2 = new Student(2, "李四", "女");
Student s3 = new Student(3, "王五", "男");
Student s4 = new Student(4, "测试", "女");

当我们查询id为5的学生时,页面会抛出StudentNotExistsException异常并返回500的HTTP的状态码:

500状态码

这是因为在Spring中,如果出现任何没有映射的异常,响应都会带有500状态码。但是在这个功能中,如果查不到相应的学生信息,我们想返回404的状态码,用于表示无法找到相应的资源,这样地描述更为准确,所以我们需要在StudentNotExistsException定义上添加@ResponseStatus注解:

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Student Not Exists")
public class StudentNotExistsException extends RuntimeException {
}

这样,如果控制器方法抛出StudentNotExistsException异常的话,它会返回404状态码,并且失败原因为:Student Not Exists。

404状态码

3.3、编写异常处理的方法

如果我们想在响应中不仅包括状态码,还想包含所产生的错误,这样,我们就不能将异常视为HTTP错误了,而是要按照处理请求的方式来处理异常,我们可以修改一下上述代码:

@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable("id") int id, Model model) {
    try {
        Student s = new Student().getStudentById(id);

        if(null != s) {
            model.addAttribute("student", s);
        } else {
            throw new StudentNotExistsException();
        }

        return "student";
    } catch (StudentNotExistsException e) {
        return "studentNotExists";
    }
}

我们在代码中添加了异常处理机制,如果抛出StudentNotExistsException异常,那么系统会跳转到学生信息不存在的页面。我们也可以不修改原有的代码来实现与上述代码相同的效果,我们只需要在控制器中添加一个新的方法,并在方法上添加@ExceptionHandler注解即可:

@ExceptionHandler(value=StudentNotExistsException.class)
public String handleStudentNotExists() {
    return "studentNotExists";
}

这样如果系统抛出StudentNotExistsException异常,将会委托该方法来处理,它返回的是一个String,指定了要渲染的视图名,这与处理请求的方法是一致的。还有一点需要我们注意的是,尽管我们是从queryStudentInfo()方法中抽出这个异常处理方法的,但是,这个方法能够处理同一个控制器中所有处理器方法所抛出的StudentNotExistsException异常,我们不用在每一个可能抛出StudentNotExistsException异常的方法中添加异常处理代码。现在,我们就需要考虑有没有一种方式能够处理所有控制器中处理器方法所抛出的异常呢?从Spring 3.2开始,我们只需要将其定义到控制器通知类中即可。

4、控制器通知

控制器通知是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:

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

在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。例如,我们上节介绍的,如果我们想为应用中所有的控制器的处理器方法都添加StudentNotExistsException异常处理方法,我们可以这样写:

@ControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(value=StudentNotExistsException.class)
    public String handleStudentNotExists() {
        return "studentNotExists";
    }
}

这样的话,如果任意的控制器抛出StudentNotExistsException异常,不管这个方法位于哪个控制器中,都会调用handleStudentNotExists()方法来处理异常。

5、跨重定向请求传递数据

在JSP中我们就知道,重定向是由客户端向服务端重新发起一个新的请求,这就意味着原来存储在旧请求中的属性随着旧请求一起消亡了,新的请求中将不包含旧请求中的数据。为了能够从发起重定向的方法传递数据给处理重定向方法中,Spring提供了两种方式:

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

5.1、通过URL模板进行重定向

通过路径参数或者查询参数的方式在重定向中传递参数是很简单的,例如,我们想要重定向到查询学生信息页面并查询id为1的学生信息,因为我们已经有了通过学生id查询学生信息的处理器方法了,所以我们只需要在重定向的时候加上路径参数即可:return “redirect:/queryStudentInfo/1”,我们想查询哪个学生信息只需要修改URL的路径参数即可,但是这样直接拼接路径参数是不太合适的,如果要构建SQL查询语句的话,这样拼接String是很危险的,所以Spring还提供了使用模板方法的方式,例如:

@RequestMapping(value="/studentOne",method=RequestMethod.GET)
public String studentOne(Model model) {

    model.addAttribute("id", 1);
    return "redirect:/queryStudentInfo/{id}";
}

这样id作为占位符填充到URL中,而不是直接拼接到重定向String中,id中的不安全字符会进行转义,这里id可以允许用户输入任意的值,并将其附加到路径上,这里我们是为了示意才设置id为1。

除此之外,模型中所有的其他原始类型都可以添加到URL中作为查询参数,例如:

@RequestMapping(value="/studentOne",method=RequestMethod.GET)
public String studentOne(Model model) {

    model.addAttribute("id", 1);
    model.addAttribute("name", "Tom");
    return "redirect:/queryStudentInfo/{id}";
}

在这里,因为模型中的name属性没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL中,所以最终得到的URL为:http://localhost:8080/spring_mvc1/queryStudentInfo/1?name=Tom

通过路径变量和查询参数的形式跨重定向传递数据是很简单直接的方式,但它也有一定的限制,它只能用来发送简单的值,如String和数字的值,在URL中,并没有办法发送更复杂的值,这时候我们需要就使用flash属性了。

5.2、使用flash属性

假设我们想将一个学生对象通过重定向传递给重定向的处理方法,我们可以通过session来实现,在重定向之前,将学生对象放入session中,在重定向之后,将学生对象从session中取出,然后将其从session中清除。其实,Spring提供了一种类似的方式,我们无需管理这些对象,只需传递、获取数据即可,这就是flash属性。

Spring提供了通过RedirectAttributes设置flash属性的方法,这是Spring 3.1引入的Model的一个子接口,RedirectAttributes提供了Model的所有功能,除此之外,还有几个方法是用来设置flash属性的,flash属性会一直携带这些数据直到下一次请求,然后才会消失。下面这个方法我们就是通过flash属性将Student对象添加到模型中:

@RequestMapping(value="/studentOne",method=RequestMethod.GET)
public String flashAttr(RedirectAttributes model) { 
    model.addFlashAttribute("student", new Student().getStudentById(1));

    return "redirect:/queryStudentOne";
}

在执行重定向之前,所有的flash属性都会复制到session中,在重定向后,存在session中的flash属性会被取出,并从session转移到模型中,处理重定向的方法就能从模型中访问Student对象了,就像获取其他的模型对象一样,所以下面这个方法无需任何设置可以直接渲染学生信息页面:

@RequestMapping(value="/queryStudentOne",method=RequestMethod.GET)
public String queryStudentOne(Model model) {

    return "student";
}
展开阅读全文

没有更多推荐了,返回首页