《Spring实战》读书笔记-第7章 Spring MVC的高级技术

要在Spring MVC中使用基于Java的配置,我们需要告诉DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext,这是一个WebApplicationContext的实现类,它会加载Java配置类,而不是使用XML,要实现这种配置,我们可以设置contextClass上下文参数以及DispatcherServlet的初始化参数。如下的程序清单展现了一个新的web.xml,在这个文件中,它所搭建的Spring MVC使用基于Java的Spring配置:

<?xml version="1.0" encoding="UTF-8" ?>

<web-app version=“2.5”

xmlns=“http://java.sun.com/xml/ns/javaee”

xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

contextClass

org.springframework.web.context.support.

AnnotationConfigWebApplicationContext

contextConfigLocation

com.jourwon.spitter.config.RootConfig

org.springframework.web.context.ContextLoaderListener

appServlet

org.springframework.web.servlet.DispatcherServlet

contextClass

org.springframework.web.context.support.

AnnotationConfigWebApplicationContext

contextConfigLocation

com.jourwon.spittr.config.WebConfigConfig

1

appServlet

/

7.2 处理multipart形式的数据


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

对于传送二进制数据,如上传图片,与典型的基于文本的表单提交有所不同,multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分是二进制,下面展示了multipart的请求体:

展示了multipart的请求体:

img

在这个multipart的请求中,我们可以看到profilePicture部分与其他部分明显不同。除了其他内容以外,它还有自己的Content-type头,表明它是一个JPEG图片。尽管不一定那么明显,但profilePicture部分的请求体是二进制数据,而不是简单的文本。

在编写控制器方法之前,我们必须要配置一个multipart解析器,通过它来告诉DispatcherServlet该如何读取multipart请求。

配置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了。

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

@Bean

public MultipartResolver multipartResolver() throws IOException {

return new StrandardServletMultipartResolver();

}

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

如果我们采用Servlet初始化类的方式来配置DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()方法,传入一个MultipartConfigElement实例。我们可以通过重载customizeRegistration()方法来配置multipart的具体细节。

MultipartConfigElement的构造器所能接受的参数如下:

  • 上传文件的临时路径。

  • 上传文件的最大容量(以字节为单位)。默认是没有限制的。

  • 整个multipart请求的最大容量(以字节为单位),不会关心有多少个part以及每个part的大小。默认是没有限制的。

  • 在上传的过程中,如果文件大小达到了一个指定最大容量(以字节单位),将会写入到临时文件路径中。默认值为0,也就是所上传的文件都会写入到磁盘上。

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

@Override

protected void customizeRegistration(Dynamic registration) {

registration.setMultipartConfig(new MultipartConfigElement(“/tmp/spittr/uploads”, 2097152, 4194394, 0));

}

如果我们使用更为传统的web.xml来配置MultipartConfigElement的话,那么可以使用<servlet><multipart-config>元素,如下所示:

appServlet

org.springframework.web.servlet.DispatcherServlet

1

/tmp/spittr/uploads

2097152

4194394

除了StrandardServletMultipartResolver,我们还可以使用Spring内置的CommonsMultipartResolver。声明如下:

@Bean

public MultipartResolver multipartResolver() {

return new CommonsMultipartResulver();

}

CommonsMultipartResulver不会强制要求设置临时文件路径。默认情况下,这个路径就是Servlet容器的临时目录。不过,通过设置uploadTempDir属性,我们可以将其指定为一个不同的位置。实际上,我们还可以按照这种方式指定其他的multipart上传细节,也就是设置CommonsMultipartResolver的属性。例如,如下的配置就等价于我们通过MultipartConfigElement所配置的StrardardServletMultipartResolver:

@Bean

public MultipartResolver multipartResolver() {

CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();

multipartResolver.setUploadTempDir(new FileSystemResource(“/tmp/spittr/uploads”));

multipartResolver.setMaxUploadSize(2097152);

multipartResolver.setMaxInMemorySize(0);

return multipartResolver;

}

在这里,我们将最大的文件容量设置为2MB,最大的内存大小设置为0字节,表明不能上传超过2MB的文件,并不管文件的大小如何,所有的文件都会写到磁盘中。但是与MultipartConfigElement有所不同,我们无法设定multipart请求整体的最大容量。

处理multipart请求

要实现控制器方法来接收上传的文件,最常见的方式就是在某个控制器方法参数上添加@RequestPart注解。

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

<sf:form method=“POST” commandName=“spitter” enctype=“multipart/form-data”>

:

<input type=“file”

name=“profilePicture”

accept=“image/jpeg,image/png,image/gif” />

</sf:form>

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

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

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

@RequestMappting(value=“/register”, method=POST)

public String processRegistration(

@RequestPart(“profilePicture”) byte[] profilePicture,

@Valid Spitter spitter,

Errors errors) {

}

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

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

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;

}

它提供了获取上传文件byte的方式,还能获得原始的文件名、大小以及内容类型。它还提供了一个InputStream,用来将文件数据以流的方式进行读取。

除此之外,MultipartFile还提供了一个便利的transferTo()方法,它能够帮助我们将上传的文件写入到文件系统中。作为样例,我们可以在processRegistration() 方法中添加如下的几行代码,从而将上传的图片文件写入到文件系统中:

@RequestMapping(value=“/register”, method=POST)

public String processRegistration(

@Valid SpitterForm spitterForm, // 校验 Spitter输入

Errors errors) throws IllegalStateException, IOException {

if (errors.hasErrors()) {

return “registerForm”; // 如果校验出现错误,则重新返回表单

}

Spitter spitter = spitterForm.toSpitter();

spitterRepository.save(spitter);

MultipartFile profilePicture = spitterForm.getProfilePicture();

profilePicture.transferTo(new File(“/tmp/spittr/” + profilePicture.getOriginalFilename()));

return “redirect:/spitter/” + spitter.getUsername();

}

其中用到的SpitterForm类,如下所示:

package spittr.web;

import javax.validation.constraints.NotNull;

import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;

import org.springframework.web.multipart.MultipartFile;

import spittr.model.Spitter;

public class SpitterForm {

@NotNull

@Size(min=5, max=16, message=“{username.size}”)

private String username;

@NotNull

@Size(min=5, max=25, message=“{password.size}”)

private String password;

@NotNull

@Size(min=2, max=30, message=“{firstName.size}”)

private String firstName;

@NotNull

@Size(min=2, max=30, message=“{lastName.size}”)

private String lastName;

@NotNull

@Email

private String email;

private MultipartFile profilePicture;

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getFirstName() {

return firstName;

}

public void setFirstName(String firstName) {

this.firstName = firstName;

}

public String getLastName() {

return lastName;

}

public void setLastName(String lastName) {

this.lastName = lastName;

}

public String getEmail() {

return email;

}

public void setEmail(String email) {

this.email = email;

}

public MultipartFile getProfilePicture() {

return profilePicture;

}

public void setProfilePicture(MultipartFile profilePicture) {

this.profilePicture = profilePicture;

}

public Spitter toSpitter() {

return new Spitter(username, password, firstName, lastName, email);

}

}

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

@RequestMappting(value=“/register”, method=POST)

public String processRegistration(

@RequestPart(“profilePicture”) Part profilePicture,

@Valid Spitter spitter,

Errors errors) {

}

那么将上传的文件写入文件系统中的代码为:

profilePicture.write(new File(“/tmp/spittr/” + profilePicture.getSubmittedFileName()));

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

7.3 处理异常


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

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

  • 特定的Spring异常将会自动映射为指定的HTTP状态码;

  • 异常上可以添加@ResponseStatus注解,从而将其映射为某一个HTTP状态码;

  • 在方法上可以添加@ExceptionHandler注解,使其用来处理异常。

将异常映射为HTTP状态码

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

| Spring异常 | HTTP状态码 |

| — | — |

| BindException | 400 - Bad Request |

| ConversionNotSupportedException | 500 - Internal Server Error |

| HttpMediaTypeNotAcceptableException | 406 - Not Acceptable |

| HttpMediaTypeNotSupportedException | 415 - Unsupported Media Type |

| 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状态码的响应(Not Found)。

对于应用所抛出的异常,这些内置的映射就无能为力了。幸好,Spring提供了一种机制,能够通过@RequestStatus注解将异常映射为HTTP状态码。

为了阐述这项功能,请参考SpittleController中如下的请求处理方法,它可能会产生HTTP 404状态(但目前还没有实现):

@RequestMapping(value=“/{spittleId}” , method=RequestMethod.GET)

public String spittle(

@PathVariable(“spittleId”) long spittleId,

Model model) {

Spittle spittle = spittleRepository.findOne(spittleId);

if (spittle == null) {

throw new SpittleNotFoundException();

}

model.addAttribute(spittle);

return “spittle”;

}

现在SpittleNotFoundException就是一个简单的非检查型异常,如下所示:

package spittr.web;

public class SpittleNotFoundException extends RuntimeException {

}

SpittleNotFoundException默认会产生500状态码(Internal Server Error)的响应。实际上,如果出现任何没有映射的异常,响应都会带有500状态码,但是,我们可以通过映射SpittleNotFoundException对这种默认行为进行变更。

当抛出SpittleNotFoundException异常时,这是一种请求资源没有找到的场景。我们要使用@ResponseStatus注解将SpittleNotFoundException映射为HTTP状态码404。

package spittr.web;

import org.springframework.http.HttpStatus;

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

@ResponseStatus(value = HttpStatus.NOT_FOUND,

reason = “Spittle Not Found”)

public class SpittleNotFoundException extends RuntimeException {

}

编写异常处理的方法

作为样例,假设用户试图创建的Spittle与已创建的Spittle文本完全相同,那么SpittleRepository的save()方法将会抛出DuplicateSpittleException异常。这意味着SpittleController的saveSpittle()方法可能需要处理这个异常。如下面的程序所示,saveSpittle()方法可以只关注成功保存Spittle的情况。

@RequestMapping(method=RequestMethod.POST)

public String saveSpittle(SpittleForm form, Model model) throws Exception {

spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),

form.getLongitude(), form.getLatitude()));

return “redirect:/spittles”;

}

然后,我们为SpittleController添加一个新的方法,它会处理抛出DuplicateSpittleException的情况:

@ExceptionHandler(DuplicateSpittleException.class)

public String handleDuplicateSpittle(){

return “error/duplicate”;

}

对于@ExceptionHandler注解标注的方法来说,比较有意思的一点在于它能处理同一个控制器中所有处理器方法所抛出的异常。所有,我们不用在每一个可能抛出DuplicateSpittleException的方法中添加异常处理代码,这一个方法就涵盖了所有的功能。

7.4 为控制器添加通知


如果控制器类的特定切面能够运用到整个应用程序的所有控制器中,那么这将会便利很多。举例说明,如果要在多个控制器中处理异常,那@ExceptionHandler注解所标注的方法是很有用的。不过,如果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在所有的控制器方法中重复相同的@ExceptionHandler方法。或者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类要扩展这个类,从而继承通用的@ExceptionHandler方法。

Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。

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

  • @ExceptionHandler注解标注的方法;

  • @InitBinder注解标注的方法;

  • @ModelAttribute注解标注的方法;

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

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

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

package spittr.web;

import org.springframework.web.bind.annotation.ControllerAdvice;

import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice

public class AppWideExceptionHandler {

@ExceptionHandler(DuplicateSpittleException.class)

public String duplicateSpittleHandler(){

return “error/duplicate”;

}

}

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

7.5 跨重定向请求传递数据


在处理完POST请求后,通常来讲一个最佳实践就是执行一下重定向。除了其他的一些因素外,这样做能够防止用户点击浏览器的刷新按钮或后退箭头时,客户端重新执行危险的POST请求。

“redirect:”前缀能够让重定向功能变得非常简单。Spring为重定向功能还提供了一些其他的辅助功能。

一般来讲,当一个处理器方法完成之后,该方法所指定的模型数据将会复制到请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发的过程中,请求属性能够得以保存。

但是,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。

显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一些其他的方案,能够从发起重定向的方法传递数据给处理重定向方法中:

  • 使用URL模板以路径变量和/或查询参数的形式传递数据;

  • 通过flash属性发送数据。

通过URL模板进行重定向

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

return “redirect:/spitter/” + spitter.getUsername();

这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的。

除了连接String的方式来构建重定向URL,Spring还提供了使用模板的方式来定义重定向URL。例如

@RequestMapping(value=“/register”, method=POST)

public String processRegistration(

Spitter spitter, Model model) {

spitterRepository.save(spitter);

model.addAttribute(“username”, spitter.getUsername());

return “redirect:/spitter/{username}”;

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
“redirect:/spitter/” + spitter.getUsername();

这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的。

除了连接String的方式来构建重定向URL,Spring还提供了使用模板的方式来定义重定向URL。例如

@RequestMapping(value=“/register”, method=POST)

public String processRegistration(

Spitter spitter, Model model) {

spitterRepository.save(spitter);

model.addAttribute(“username”, spitter.getUsername());

return “redirect:/spitter/{username}”;

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-379yIqLh-1715109777807)]

[外链图片转存中…(img-sobJteJq-1715109777807)]

[外链图片转存中…(img-JR83ADGb-1715109777807)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值