本篇主要介绍利用Spring MVC处理文件上传,异常处理,为控制器添加通知以及跨重定向请求传递数据。
在有文件上传的表单中,我们需要使用multipart格式的数据来上传,multipart格式的数据会将一个表单拆分成多个部分(part),每个部分对应一个输入域。在一半的表单输入域中,它所对应的部分会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制。下面是前段页面表单部分:
<form action="${ctx}/register" enctype="multipart/form-data" method="post">
<label>用户名:label>
<input type="text" name="username"><br/>
<label>密码:label>
<input type="password" name="password"><br/>
<label>邮箱:label>
<input type="text" name="email"><br/>
<label>头像:label>
<input type="file" name="image" accept="image/jpeg,image/png,image/gif"><br/>
<input type="submit" value="注册">
form>
form表单现在将enctype属性设置为multipart/form-data,这会告诉浏览器以multipart数据的形式提交表单,而不是以表单数据的形式进行提交。在multipart中,每个输入域都会对应一个part。而文件上传输入域中的accept属性用来将文件类型限制为JPEG、PNG和GIF图片。
DispatcherServlet将解析multipart请求数据的任务委托给了Spring中MultipartResolver策略接口的实现。Spring内置了两个MultipartResolver的实现供我们选择:
使用Servlet 3.0解析multipart请求:
在Spring MVC上下文中将其声明为bean:
@Configuration
@EnableWebMvc //启用Spring MVC
@ComponentScan("spittr.web") //启用组件扫描
public class WebConfig
extends WebMvcConfigurerAdapter{
/**
* 配置multipart解析器
* @return
*/
@Bean
public MultipartResolver multipartResolver(){
return new StandardServletMultipartResolver();
}
}
StandardServletMultipartResolver的配置是在Servlet中通过传入一个MultipartConfigElement实例来指定:
如果是自定义的Servlet,即(自己实现WebApplicationInitializer),这样做:
public class myServletIntializer implements WebApplicationInitializer{
public void onStartup(ServletContext servletContext) throws ServletException {
MyServlet myServlet = new MyServlet();
Dynamic dynamic = servletContext.addServlet("myServlet", myServlet);
dynamic.addMapping("/");
dynamic.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads"));
}
}
如果是通过继承AbstractAnnotationConfigDispatcherServletInitializer的到的DispatcherServlet的话,将不会有对Dynamic的引用,还记得上一篇我们提到的么,通过重载customizeRegistration会提供一个Dynamic参数,so:
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads"));
}
这里,我们都是通过构造器来对MultipartConfigElement进行配置的。new MultipartConfigElement(location, maxFileSize, maxRequestSize, fileSizeThreshold)
这是其所有的构造器。
下面是用web.xml来配置MultipartConfigElement,使用
的
元素:
<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>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
</servlet>
限制文件的大小为2MB,整个请求不超过4MB,而且所有的文件都写到磁盘里。
使用使用Jakarta Commons FileUpload解析multipart请求
声明为bean:
@Bean
public MultipartResolver multipartResolver(){
//return new StandardServletMultipartResolver();
return new CommonsMultipartResolver();
}
CommonsMultipartResolver的配置不是在Servlet指定的,而是直接配置在实例中,而且CommonsMultipartResolver不会轻质要求设置临时文件路径,默认情况下,这个路径就是Servlet容器的临时目录。下面是配置一个等价于上面我们对MultipartConfigElement的配置:
@Bean
public MultipartResolver multipartResolver() throws IOException{
//return new StandardServletMultipartResolver();
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setUploadTempDir(
new FileSystemResource("/tmp/spittr/uploads"));//对应于MultipartConfigElement的location
multipartResolver.setMaxUploadSize(2097152);//对应于MultipartConfigElement的maxFileSize
multipartResolver.setMaxInMemorySize(0);//对应于MultipartConfigElement的fileSizeThreshold
return multipartResolver;
}
CommonsMultipartResolver 无法设定multipart请求整体的最大容量。
1.3.1用byte[]数组来接受上传文件的二进制数据
代码:
@RequestMapping("/register")
public void spittle(
@RequestPart("image") byte[] image,
User user){
System.out.println(image.length);
}
1.3.2接受MultipartFile:
@RequestMapping("/register")
public void spittle(
@RequestPart("image") MultipartFile image,
User user,HttpServletRequest request) throws IllegalStateException, IOException{
System.out.println(image.getContentType());
System.out.println(image.getName());
System.out.println(image.getOriginalFilename());
System.out.println(image.getSize());
image.transferTo(
new File("/uploads/"+image.getOriginalFilename()));//将上传文件写入到文件系统中
}
需要注意的是这里File的位置是以你配置的临时文件路径为前提的,比如说你的临时文件路径为F:\Workspace\spring,那么上面的文件就是在F:\Workspace\spring\uploads\下
Servlet请求的输出都是一个Servlet响应。如果在请求处理的时候,出现了异常,那它放入输出依然会使Servlet响应。异常必须要以某种方法转换为响应。
Spring提供了多种方式将异常转换为响应:
Spring的一些异常会默认映射为HTTP状态码
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 |
下面介绍将异常映射为特定的状态码
首先我们创建一个异常,在其上面使用@ResponseStatus来将异常映射为特定的状态码。
@ResponseStatus(value=HttpStatus.NOT_FOUND,
reason="Param Not Found")//将异常映射为HTTP状态404
public class ParamNotFountException extends RuntimeException{
private static final long serialVersionUID = 1L;
}
在控制器如果接受到的username为空则抛出这个异常:
@RequestMapping("/register")
public String spittle(
@RequestPart("image") MultipartFile image,
User user,HttpServletRequest request){
//如果用户名为空则抛出异常
if(user.getUsername().isEmpty()){
throw new ParamNotFountException();
}
return "register";
}
继续上面的方法,如果我们不想显示错误页面,想要捕获异常的话,平时我们会使用catch进行捕获,但catch只能捕获当前语句块的异常并处理。下面我们来使用@ExceptionHandler标注的方法,它可以出来同一个控制器中所有处理器方法抛出的异常。接着上面的代码,我们在控制器方法下面写这样一个方法:
@RequestMapping("/register")
public String spittle(
@RequestPart("image") MultipartFile image,
User user,HttpServletRequest request){
//如果用户名为空则抛出异常
if(user.getUsername().isEmpty()){
throw new ParamNotFountException();
}
return "register";
}
//捕获这个控制器抛出的ParamNotFountException异常,并处理
@ExceptionHandler(value=ParamNotFountException.class)
public String HandleException(){
System.out.println("在这里处理异常");
return "error";
}
这一次,返回的不是404页面,而是error页面,而且控制台输出:“在这里处理异常”字段。
控制器通知(controller advice)是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:
在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。@ControllerAdvice注解本身自带扫描特性,即使用了@Component。
还是上面的代码,我们把控制器中的HandleException()方法删掉,然后新建一个类,运行效果是一样的:
@ControllerAdvice
public class AppWideException {
//捕获所有控制器抛出的ParamNotFountException异常,并处理
@ExceptionHandler(value=ParamNotFountException.class)
public String HandleException(){
System.out.println("在这里处理异常");
return "error";
}
}
当一个处理器方法完成之后,该方法所指定的模型数据将会复制到请求中,并作为请求的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发的过程中,请求属性能够得以保存。当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的get请求。原始请求中所带有的模型数据也就随之请求一起消亡了。
有两种方法能够从发起重定向的方法传递数据给处理重定向方法中:
使用URL中的占位符进行传递:
@RequestMapping("/register")
public String spittle(User user,Model model){
model.addAttribute("username", "xuexiaoqiang");
model.addAttribute("email", "[email protected]");
//重定向
return "redirect:/test/{username}";
}
@RequestMapping("/test/{username}")
public String test(
@PathVariable(value="username") String username,
HttpServletRequest request){
String email = request.getParameter("email");
System.out.println("username:"+username);
System.out.println("email:"+email);
return "register";
}
输出:
username:xuexiaoqiang
email:test@tom.com
这里,因为模型中的email没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上。这里最后的重定向路径是“/test/[email protected]”。
如果我们想要发生一个对象,那么URL就不能实现,Spring提供了将数据发生为flash属性的功能。按照定义,flash属性会一直携带这些数据知道下一次请求,然后才会消失。
RedirectAttributes是Model的一个子接口,提供了Model的所有功能,除此之外,还有几个方法是用来设置flash属性的。
@RequestMapping("/register")
public String spittle(User user,RedirectAttributes model){
user.setEmail("[email protected]");
user.setUsername("xuexiaoqiang");
model.addFlashAttribute("user", user);
//重定向
return "redirect:/test";
}
@RequestMapping("/test")
public String test(Model model){
if(model.containsAttribute("user")){
System.out.println("存在");
}
return "testdata";
}
testdata.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title heretitle>
head>
<body>
<h1>用户名:${user.username }h1>
<h1>邮箱:${user.email }h1>
body>
html>
在重定向执行之前,所有flash属性都会复制到会话中。在重定向后,存在会话中的flash属性会被取出,并从会话转移到模型之中。处理重定向的方法就能从模型中访问User对象了,就像获取其他的模型对象一样。