美文网首页
Spring 学习笔记(十一)Spring MVC的高级技术

Spring 学习笔记(十一)Spring MVC的高级技术

作者: Theodore的技术站 | 来源:发表于2019-01-03 13:38 被阅读12次

    Spring MVC 配置的替代方案

    自定义 DispatcherServlet 配置

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

    虽然从代码外观上不一定能够看得出来,但是 Abstract-AnnotationConfigDispatcherServletInitializer 所完成的事情其实比看上去要多。在 SpittrWebAppInitializer 中我们所编写的三个方法仅仅是必须要重载的abstract方法。但实际上还有更多的方法可以进行重载,从而实现额外的配置。

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

    在稍后的内容中,我们将会看到如何在 SpringMVC 中处理 multipart 请求和文件上传。如果计划使用 Servlet 3.0 对 multipart 配置的支持,那么需要使用 DispatcherServlet 的 registration 来启用 multipart 请求。我们可以重载 customizeRegistration() 方法来设置 MultipartConfigElement,如下所示:

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

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

    添加其他的Servlet和Filter

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

    类似地,我们还可以创建新的 WebApplicationInitializer 实现来注册 Listener 和 Filter。例如,如下的程序清单展现了如何注册 Filter。

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException{
        javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter",MyFilter.class);
        filter.addMappingForUrlPatterns(null,false,"/custom/**");
    }
    

    如果要将应用部署到支持 Servlet 3.0 的容器中,那么 WebApplicationInitializer 提供了一种通用的方式,实现在Java中注册 Servlet、Filter 和 Listener。不过,如果你只是注册 Filter,并且该 Filter 只会映射到 DispatcherServlet 上的话,那么在 AbstractAnnotationConfigDispatcherServletInitializer 中还有一种快捷方式。

    为了注册 Filter 并将其映射到 DispatcherServlet,所需要做的仅仅是重载 AbstractAnnotationConfigDispatcherServletInitializer 的 getServlet-Filters() 方法。例如,在如下的代码中,重载了 AbstractAnnotationConfig-DispatcherServletInitializer 的 getServletFilters() 方法以注册 Filter:

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

    我们可以看到,这个方法返回的是一个 javax.servlet.Filter 的数组。在这里它只返回了一个 Filter,但它实际上可以返回任意数量的 Filter。在这里没有必要声明它的映射路径,getServletFilters() 方法返回的所有 Filter 都会映射到 DispatcherServlet 上。

    处理 multipart 形式的数据

    在 Web 应用中,允许用户上传内容是很常见的需求。Spittr 应用在两个地方需要文件上传。当新用户注册应用的时候,我们希望他们能够上传一张图片,从而与他们的个人信息相关联。当用户提交新的 Spittle 时,除了文本消息以外,他们可能还会上传一张照片。

    一般表单提交所形成的请求结果是很简单的,就是以“&”符分割的多
    个name-value对。例如,当在Spittr应用中提交注册表单时,请求会如
    下所示:

    firstName=Charles&lastName=Xavier&email=porfessorx%40xmen.org
    &username=professorx&password=letmein01
    

    尽管这种编码形式很简单,并且对于典型的基于文本的表单提交也足够满足要求,但是对于传送二进制数据,如上传图片,就显得力不从心了。与之不同的是,multipart 格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制,下面展现了 multipart 的请求体:

    image.png

    配置 multipart 解析器

    DispatcherServlet 并没有实现任何解析 multipart 请求数据的功能。它将该任务委托给了 Spring 中 MultipartResolver 策略接口的实现,通过这个实现类来解析 multipart 请求中的内容。从 Spring 3.1 开始,Spring 内置了两个 MultipartResolver 的实现供我们选择:
    -CommonsMultipartResolver:使用 Jakarta CommonsFileUpload 解析 multipart 请求;
    -StandardServletMultipartResolver:依赖于 Servlet 3.0 对 multipart 请求的支持(始于 Spring 3.1)。

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

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

    既然这个 @Bean 方法如此简单,你可能就会怀疑我们到底该如何限制
    StandardServletMultipartResolver 的工作方式呢。如果我们想要限制用户上传文件的大小,该怎么实现?如果我们想要指定文件在上传时,临时写入目录在什么位置的话,该如何实现?因为没有属性和构造器参数,StandardServletMultipartResolver 的功能看起来似乎有些受限。

    其实并不是这样,我们是有办法配置 StandardServletMultipartResolver 的限制条件的。只不过不是在 Spring 中配置 StandardServletMultipartResolver,而是要在 Servlet 中指定 multipart 的配置。至少,我们必须要指定在文件上传的过程中,所写入的临时文件路径。如果不设定这个最基本配置的话,StandardServlet-MultipartResolver 就无法正常工作。具体来讲,我们必须要在 web.xml 或 Servlet 初始化类中,将 multipart 的具体细节作为 DispatcherServlet 配置的一部分。

    如果我们采用 Servlet 初始化类的方式来配置 DispatcherServlet 的话,这个初始化类应该已经实现了 WebApplicationInitializer,那我们可以在 Servlet registration 上调用 setMultipartConfig() 方法,传入一个 MultipartConfig-Element 实例。如下是最基本的 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 初始化类继承了Abstract AnnotationConfigDispatcherServletInitializer 或 AbstractDispatcher-ServletInitializer 的话,那么我们不会直接创建 DispatcherServlet 实例并将其注册到 Servlet 上下文中。这样的话,将不会有对 Dynamic Servlet registration 的引用供我们使用了。但是,我们可以通过重载 customizeRegistration() 方法(它会得到一个 Dynamic 作为参数)来配置 multipart 的具体细节:

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

    到目前为止,我们所使用是只有一个参数的 MultipartConfigElement 构造器,这个参数指定的是文件系统中的一个绝对目录,上传文件将会临时写入该目录中。但是,我们还可以通过其他的构造器来限制上传文件的大小。除了临时路径的位置,其他的构造器3所能接受的参数如下:
    -上传文件的最大容量(以字节为单位)。默认是没有限制的。
    -整个 multipart 请求的最大容量(以字节为单位),不会关心有多少个 part 以及每个 part 的大小。默认是没有限制的。
    -在上传的过程中,如果文件大小达到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为 0,也就是所有上传的文件都会写入到磁盘上。

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

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

    配置 Jakarta Commons FileUpload multipart 解析器

    通常来讲,StandardServletMultipartResolver 会是最佳的选择,但是如果我们需要将应用部署到非 Servlet 3.0 的容器中,那么就得需要替代的方案。如果喜欢的话,我们可以编写自己的 MultipartResolver 实现。不过,除非想要在处理 multipart 请求的时候执行特定的逻辑,否则的话,没有必要这样做。Spring 内置了 CommonsMultipartResolver,可以作为 StandardServletMultipartResolver 的替代方案
    将 CommonsMultipartResolver 声明为 Spring bean 的最简单方式如下:

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

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

    @Bean
    public MultipartResolver multipartResolver() throws IOException{
        CommonMultipartResolver multipartResolver = new CommonMultipartResolver();
        multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
        return multipartResolver;
    }
    

    实际上,我们可以按照相同的方式指定其他的 multipart 上传细节,也就是设置 CommonsMultipartResolver 的属性。例如,如下的配置就等价于我们在前文通过 MultipartConfigElement 所配置的 StandardServletMultipartResolver:

    @Bean
        public MultipartResolver multipartResolver() throws IOException{
            CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
            multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
            multipartResolver.setMaxUploadSize(2097152);
            multipartResolver.setMaxInMemorySize(0);
            return multipartResolver;
        }
    

    在这里,我们将最大的文件容量设置为 2MB,最大的内存大小设置为0字节。这两个属性直接对应于 MultipartConfigElement 的第二个和第四个构造器参数,表明不能上传超过 2MB 的文件,并且不管文件的大小如何,所有的文件都会写到磁盘中。但是与 MultipartConfigElement 有所不同,我们无法设定 multipart 请求整体的最大容量。

    处理multipart请求

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

    假设我们允许用户在注册 Spittr 应用的时候上传一张图片,那么我们需要修改表单,以允许用户选择要上传的图片,同时还需要修改 SpitterController 中的 processRegistration() 方法来接收上传的图片。如下的代码片段来源于 Thymeleaf 注册表单视图(registrationForm.html),着重强调了表单所需的修改:

    <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>
    

    <form> 标签现在将 enctype 属性设置为 multipart/formdata,这会告诉浏览器以 multipart 数据的形式提交表单,而不是以表单数据的形式进行提交。在 multipart 中,每个输入域都会对应一个 part。

    除了注册表单中已有的输入域,我们还添加了一个新的 <input> 域,其 type 为 file。这能够让用户选择要上传的图片文件。accept 属性用来将文件类型限制为 JPEG、PNG 以及 GIF 图片。根据其 name 属性,图片数据将会发送到 multipart 请求中的 profilePicture part 之中。

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

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

    当注册表单提交的时候,profilePicture 属性将会给定一个 byte 数组,这个数组中包含了请求中对应part的数据(通过 @RequestPart 指定)。如果用户提交表单的时候没有选择文件,那么这个数组会是空(而不是null)。获取到图片数据后,processRegistration() 方法剩下的任务就是将文件保存到某个位置。

    接受MultipartFile

    使用上传文件的原始 byte 比较简单但是功能有限。因此,Spring 还提供了 MultipartFile 接口,它为处理 multipart 数据提供了内容更为丰富的对象。如下的程序清单展现了 MultipartFile 接口的概况。
    程序清单7.5 Spring 所提供的 MultipartFile 接口,用来处理上传的文件

    import java.io.File;
    import java.io.IOException;
    import java.io.InputStream;
    
    public interface MultipartFile {
        String geName();
        String getOriginalFilename();
        String getContentType();
        boolean isEmpty();
        long getSize();
        byte[] getByte() throws IOException;
        InputStream getInputStream() throws IOException;
        void transferTo(File dest) throws IOException;
    }
    

    我们可以看到,MultipartFile 提供了获取上传文件 byte 的方式,但是它所提供的功能并不仅限于此,还能获得原始的文件名、大小以及内容类型。它还提供了一个 InputStream,用来将文件数据以流的方式进行读取
    除此之外,MultipartFile 还提供了一个便利的 transferTo() 方法,它能够帮助我们将上传的文件写入到文件系统中。作为样例,我们可以在 process-Registration() 方法中添加如下的几行代码,从而将上传的图片文件写入到文件系统中:

    profilePicture.transferTo(new File("/data/spittr/" + profilePicture.getOriginalFilename()));
    

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

    如果你需要将应用部署到 Servlet 3.0 的容器中,那么会有 MultipartFile 的一个替代方案。Spring MVC 也能接受 javax.servlet.http.Part 作为控制器方法的参数。如果使用 Part 来替换 MultipartFile 的话,那么 processRegistration() 的方法签名将会变成如下的形式:

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

    Part接口:Spring 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();
        long getSize();
        void write(String var1) throws IOException;
        void delete() throws IOException;
        String getHeader(String var1);
        Collection<String> getHeaders(String var1);
        Collection<String> getHeaderNames();
    }
    

    相关文章

      网友评论

          本文标题:Spring 学习笔记(十一)Spring MVC的高级技术

          本文链接:https://www.haomeiwen.com/subject/meozlqtx.html