17、Spring MVC 的高级技术(1)(spring笔记)

一、Spring MVC 配置的替代方案

在之前我们通过扩展AbstractAnnotationConfigDispatcherServletInitializer快速搭建了Spring MVC环境,这个便利的基础类会帮我们创建需要的DispatcherServlet和ContextLoaderListener对象。但是在实际应用中,除了DispatcherServlet之外,可能还需要额外的ServletFilter;或者还需要对DispatcherServlet本身做一些额外的配置;又或者需要将应用部署到Servlet3.0之前的容器中,此时则需要将DispatcherServlet配置到传统的web.xml中。

1.1 自定义 DispatcherServlet配置

在之前使用Java配置方式中,我们编写SpittrWebApplicationInitializer配置类继承AbstractAnnotationConfigDispatcherServletInitializer,对其中的三个方法进行了重载,但是这三个方法只是必须要重载的抽象方法,实际上还有更多方法可以重载。比如customizeRegistration()方法,在AbstractAnnotationConfigDispatcherServletInitializerDispatcherServlet注册到Servlet容器之后,就会调用此方法,并将Servlet注册后得到的Registration.Dynamic对象传递进来,我们可以通过此对象对DispatcherServlet进行一些额外的配置。

1.2 添加其他的 Servlet和 Filter

如果应用中除了DispatcherServlet这个Servlet需要注册外,还有其他的ServletFilter的话,怎样将其注册到Servlet容器中?

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

public class MyServletInitializer implements WebApplicationInitializer{

    public void onStartup(ServletContext servletContext) throws ServletException{
        javax.servlet.ServletRegistration.Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
        myServlet.addMapping("/custom/**");
    }
}

说明:这个类中MyServlet类就是我们自定义的Servlet类,同时配置了其映射路径,当然我们也可以这样手动配置DispatcherServlet类,但是没有必要。同理,还可以使用这种方式添加其他的FilterListener类:

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

说明:这里的Dynamic类和之前的不一样。如果要将应用部署到支持Servlet3.0的容器中,那么这是一种通用的方式。而如果只是注册Filter,并且该Filter只会映射到DispatcherServlet上的话,那么AbstractAnnotationConfigDispatcherServletInitializer中还有一种快捷方式。只需要重载其getServletFilters()(其实是其父类中的方法)方法。

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

说明:这个方法返回一个javax.servlet.Filter的数组,在这里没有必要声明它的映射路径,此方法返回的所有Filter都会映射到DispatcherServlet上。

以上介绍的方式都是将应用部署到Servlet3.0容器中的情况,这种方式下不用创建web.xml文件。但是如果要部署到Servlet3.0之前的容器中,则需要配置web.xml文件了。

1.3 在web.xml 中声明 DispatcherServlet

这种方式配置下,我们需要自己来注册DispatcherServletContextLoaderListener。如下:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <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>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

说明:上下文contextConfigLoaction中指定了初始化文件地址,这个文件定义了根应用上下文,会被ContextLoaderListener加载。而DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文。这里Servlet名字是appServlet,因此对应的文件为“/WEB-INF/appServlet-context.xml”,当然我们也可以显示指定其路径:

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/appServlet-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>

说明:以上的配置中DispatcherServletContextLoaderListener要加载的配置文件都是XML文件,我们也可以使用Java配置。

<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>spittr.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>
        <!--指明配置类路径-->
        <param-value>spittr.config.WebConfig</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>

说明:这里的两个配置类在之前的文章中已经给出,这里只需要配置使用Java配置,在初始化的时候就会从带有@Configuration注解的类上加载配置,所以RootConfig.javaWebConfig.java中都需要使用这个注解。

二、处理 multipart 形式的数据

一般表单提交所形成的请求结果是很简单的,如GET方式中,就是以多个“&”符号分割多个name-value对。但是对于传送二进制文件,如图片上传,就不行了。而multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般地表单输入域中,它所对应的部分中会放置文本型数据,但是如果是上传文件的话,它所对应的可以是二进制,如下:

------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="firstName"

Charles
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="lastName"

Xavier
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="email"

charles@xmen.com
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="username"

professorx
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="password"

letmein01
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="profilePicture" filename="me.jpg"
Content-Type: image/jpeg

    [[ Binary image data goes here]]
------WebKitFormBoundaryqgkaBn8IHJCuNmiW------

说明:这个multipart请求中,可以看到profilePicture部分与其他部分明显不同,指明自己的Content-Type头为一个JPEG的图片。此时请求的就是二进制数据了。虽然看起来很复杂,但是在Spring MVC中处理却很简单,只需要配置一个multipart解析器即可,通过它来告诉DispatcherServlet该如何读取这种请求。

2.1 配置multipart 解析器

DispatcherServlet并没有实现任何解析multipart请求数据的功能,而是将其委托给了Spring中的MultipartResolver策略接口的实例。从Spring3.1开始,Spring内置了两个MultipartResolver实现类供我们选择:

  • CommonsMultipartResolver:使用Jakarta Commons FileUpload(即Apache的一个项目)解析multipart请求。
  • StandardServletMultipartResolver:依赖于Servlet3.0multipart请求的支持(始于Spring3.1)。

一般来讲,后者可能会是优选方案,但是要注意各自使用的范围。

2.1.1 使用 Servlet3.0 解析 multipart 请求

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

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

说明:将其配置为bean很简单,那如何限制上传文件的大小,如何指定文件上传时临时写入的目录?其实是有办法配置相关限制条件的,只是不是在Spring中配置,而是要在Servlet中指定multipart的配置。这里必须要指定临时写入的目录,否则无法正常工作。具体来讲,必须要在web.xmlServlet初始化类中,将multipart的具体限制条件作为DispatcherServlet配置的一部分。之前讲过,可以对DispatcherServlet进行一些额外的配置,所以这里可以这样配置:

DispatcherServlet ds = new DispatcherServlet();
javax.servlet.ServletRegistration.Dynamic registration = servletContext.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfigElement(new MultipartConfigElement("/tmp/spittr/uploads"));

说明:这里将临时文件设置为了"/tmp/spittr/uploads",这是一种很基本的方式,如果我们配置DispatcherServlet的自定义Servlet继承了AbstractAnnotationConfigDispatcherServletInitializer或其直接父类的话,可以直接重载customizeRegistration()方法进行配置:

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

但目前为止,使用的是只有一个参数的MultipartConfigElement构造器,这个参数指定的是文件系统中的一个绝对目录,上传文件将会写入临时写入该目录中。但是还可以通过其他构造器来限制上传文件的大小。其他参数有:

  • 上传文件的最大容量(即每个文件的大小最大值,以字节为单位)。默认是没有限制的。
  • 整个multipart请求的最大容量(字节),不会关心有多少个part以及每个part的大小。默认是没有限制的。
  • 在上传的过程中,如果文件大小达到了一个指定最大容量(字节),将会写入到临时文件路径中。默认值为零,也就是所有上传文件都会写入到磁盘。是不是可以这样理解,默认就是直接写入到磁盘,而不是使用内存缓存一定大小之后再写入到磁盘?

例如,我们想限制文件的大小不超过2MB,整个请求不超过4MB,而且所有的文件都要写到磁盘中,配置如下:

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

当然也可以在web.xml中配置:

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

注意:必须要配置路径。

2.1.2 配置Jakarta Commons FileUpload multipart 解析器

如果我们需要将应用部署到Servlet3.0之前的容器中,则可以使用Jakarta Commons FileUpload multipart来替代,当然也可以自己编写MultipartResolver实现,但是没哟必要。Spring内置了CommonsMultipartResolver,可以作为StandardServletMultipartResolver的替代方案。

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

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

说明:StandardServletMultipartResolver不同,CommonsMultipartResolver不会强制要求设置临时文件路径。默认情况下,这个路径就是Servlet容器的临时目录。当然也可以手动指定目录。

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

说明:实际上,还可以设置更多细节,如文件大小之类的。

@Bean
public MulitipartResolver 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的第二个和第四个构造器参数。无法设定总共上传文件大小之和。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容