在上一篇博客中我们简单介绍了如何搭建一个最简单的Spring MVC,现在我们来学习一下Spring的其他Web功能。为了方便学习,我将自己学习时敲下的源码放到了github上:https://github.com/cap-ljf/Spittr
本文内容对应《Spring实战》第二部分 Web中的Spring 内容:
- JavaConfig配置Spring MVC
- Spring MVC数据绑定
- JSR 303数据校验
- Spring Web视图渲染之Thymeleaf
- JavaConfig配置自定义Servlet和Filter
- 文件上传
- 统一异常处理
1. JavaConfig配置Spring MVC
2. 数据绑定入门
[Spring MVC] - SpringMVC的各种参数绑定方式
SpringMVC数据绑定入门
3. JSR 303数据校验
在了解了数据绑定之后,大部分时候我们都需要校验接受到的数据。有种处理校验的方式非常初级,那就是在controller或service方法中添加代码来检查值的合法性,如果值不合法的话,就将注册表单重新显示给用户。但是这样的校验逻辑会弄乱我们的controller方法。从Spring3.0开始,在SpringMVC中提供了对Java校验API的支持(JSR-303)。
使用JSR 303需要两个jar包依赖。1
2
3
4
5
6
7
8
9
10
11<!-- JSR303校验 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.3.6.Final</version>
</dependency>
Bean Validation中内置的constraint
Hibernate Validator 附加的 constraint
有些时候,你可能需要更复杂的constraint,Bean Validation 提供扩展 constraint 的机制。可以通过两种方法去实现:
组合现有的 constraint 来生成一个更复杂的 constraint
1
2
3
5, max = 10) (min =
private String username;自定义constraint。
定义的IntegerRange
注解。自定义constraint注解,message、groups和payload三个属性是必须定义的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.jifang.spittr.annotation;
import com.jifang.spittr.validator.IntegerRangeValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
(ElementType.FIELD)
(RetentionPolicy.RUNTIME)
(validatedBy = IntegerRangeValidator.class)
public IntegerRange {
String message() default "校验失败";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int min();
int max();
}
定义验证类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package com.jifang.spittr.validator;
import com.jifang.spittr.annotation.IntegerRange;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* author: jifang
* date: 18-4-9 下午11:08
*/
public class IntegerRangeValidator implements ConstraintValidator<IntegerRange, Integer> {
private Integer min;
private Integer max;
public void initialize(IntegerRange constraintAnnotation) {
min = constraintAnnotation.min();
max = constraintAnnotation.max();
}
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
boolean b = integer > min && integer < max ? true : false;
return b;
}
}
ConstraintValidator使用了泛型,有两个类型参数。第一个类型是对应的initialize方法的参数类型(约束注解类型),第二个类型是对应的isValid方法的第一个参数类型。
4.Spring Web视图渲染之Thymeleaf
将控制器中请求处理的逻辑和视图中的渲染实现解耦是Spring MVC的一个重要特性。
Spring自带了13个视图解析器,能够将逻辑视图名转换为物理实现。
在上一篇博文我们介绍了InternalResourceViewResolver
将视图解析为JSP。这一次我们使用新的项目Thymeleaf
来配置实现视图解析。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39/**
* 配置生成模板解析器
*
* @return
*/
public TemplateResolver templateResolver() {
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
/**
* 生成模板引擎并为模板引擎注入模板解析器
*
* @param templateResolver
* @return
*/
public SpringTemplateEngine templateEngine(TemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
/**
* 生成视图解析器并为解析器注入引擎
*
* @param templateEngine
* @return
*/
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
需要注意的是ThymeleafViewResolver bean中注入了一个对SpringTemplate Engine bean的引用。SpringTemplateEngine会在Spring中启用Thymeleaf引擎,用来解析模板,并基于这些模板渲染结果。可以看到,我们为其注入了一个TemplateResolver bean的引用。
Thymeleaf在很大程度上就是HTML文件,与JSP不同,它没有什么特殊的标签或标签库。要使用Thymeleaf的标签库,需要声明Thymeleaf的命名空间1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spittr</title>
<link rel="stylesheet"
type="text/css"
th:href="@{/resources/style.css}"/>
</head>
<body>
<h1>Welcome to Spitter</h1>
<a th:href="@{/spittles}">Spittles</a>
<a th:href="@{/spitter/register}">Register</a>
<br/>
<a th:href="@{/databind}">databind</a>
<a th:href="@{/upload}">upload</a>
</body>
</html>
关于Thymeleaf可以参考
http://www.cnblogs.com/hjwublog/p/5051732.html#autoid-11-0-0
5.JavaConfig配置自定义Servlet和Filter
实现了WebApplicationInitializer接口的类会被自动加载。我们可以在onStartup
中添加自定义的Servlet和Filter。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.jifang.spittr.config;
import com.jifang.spittr.filter.MyFilter;
import com.jifang.spittr.servlet.MyServlet;
import org.springframework.web.WebApplicationInitializer;
import javax.servlet.*;
import java.util.EnumSet;
/**
* 通过实现WebApplicationInitializer来配置额外的Servlet和Filter
* author: jifang
* date: 18-4-9 下午3:39
*/
public class MyServletInitializer implements WebApplicationInitializer {
public void onStartup(ServletContext servletContext) throws ServletException {
//配置自己的Servlet
ServletRegistration.Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
myServlet.setLoadOnStartup(1);
myServlet.addMapping("/myServlet/*");
//配置自己的Filter
FilterRegistration.Dynamic myFilter = servletContext.addFilter("myFilter", MyFilter.class);
myFilter.addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST), true, "myServlet");
}
}
6.文件上传
一般表单提交所形成的请求结果是很简单的,就是以“&”符分割的多个name-value对。尽管这种编码形式很简单,并且对于典型的基于文本的表单提交也足够满足要求,但是对于传送二进制数据,如上传图片,就显得力不从心了。与之不同的,multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制。
6.1 MultiPart形式的数据
Multipart格式数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它对应的部分会放置文本型数据,如果是文件上传形式,它对应的部分可以是二进制。
6.2 Multipart/form-data请求方式
示例:1
2
3
4
5<form action="/spittles/upload.do" method="POST" th:object="${spittr}" enctype="multipart/form-data">
<label>Profile Picture</label>:
<input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif"/><br/>
<input type="submit" value="save"/>
</form>
6.3 SpringMVC处理Multipart数据
xml配置可以参考:https://www.cnblogs.com/maying3010/p/6679974.html
下面介绍JavaConfig配置:
配置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了。
兼容Servlet 3.0的StandardServletMultipartResolver没有构造器参数,也没有要设置的属性。1
2
3
4
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
我们不能在Spring中配置StandardServletMultipartResolver的限制条件,而是要在Servlet中指定multipart的配置。
如果我们采用Servlet初始化类的方式来配置DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在ServletRegistration.Dynamic
上调用setMultipartConfig()
方法,传入MultipartConfigElement实例。如果我们配置DispatcherServlet的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer类的话,那么我们不会直接创建DispatcherServlet实例并将其注册到Servlet上下文中。这样的话,将不会有对Dynamic Servlet registration的引用供我们使用了。但是,我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:1
2
3
4
5
6
7
8
9/**
* 配置multipart的具体细节
*
* @param registration
*/
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/home/jifang/spittr/uploads", 2097152, 4194304, 0));
}
除了临时路径的位置,其他的构造器所能接受的参数如下:
- 上传文件的最大容量(以字节为单位)。默认是没有限制的。
- 整个multipart请求的最大容量(以字节为单位),不会关心有多少个part以及每个part的大小。默认是没有限制的。
- 在上传的过程中,如果文件大小达到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为0,也就是所有上传的文件都会写入到磁盘上。
处理multipart请求
- 使用byte[] 数组
- 使用上传文件的原始byte比较简单但是功能有限。因此,Spring还提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public 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;
}
在controller里保存上传的文件1
2
3
4
5
6
7
8
9"upload.do") (
public String upload(@RequestPart("profilePicture") MultipartFile profilePicture) throws IOException {
File file = new File("/home/jifang/spittr/uploads/");
if (!file.exists() && !file.isDirectory()) {
file.mkdir();
}
profilePicture.transferTo(new File("/home/jifang/spittr/uploads/" + profilePicture.getOriginalFilename()));
return "home";
}
7. 异常处理
不管请求成功或失败,Servlet请求的输出都是一个Servlet相应。异常必须要以某种方式转换为响应。
Spring提供了多种方式将异常转换为响应:
- 特定的Spring异常将会自动映射为指定的HTTP状态码;
- 异常上可以添加@ResponseStatus注解,从而将其映射为某一个HTTP状态码;
- 在方法上可以添加@ExceptionHandler注解,使其用来处理异常。
7.1 将异常对象映射为HTTP状态码(@ResponseStatus)
Spring默认会将自身抛出的异常自动映射到合适的状态码,如下是一些示例:
也可以通过@ResponseStatus注解将自定义异常映射为HTTP状态码。1
2
3"Spittle Not Found") (value = HttpStatus.NOT_FOUND, reason =
public class SpittleNotFoundException extends RuntimeException {
}
在引入@ResponseStatus注解之后,如果控制器方法抛出SpittleNotFoundException异常的话,响应将会具有404状态码,这是因为Spittle Not Found。
7.2 本地处理@ExceptionHandler
@ExceptionHandler(SpittleNotFoundException.class)
,当抛出
SpittleNotFoundException异常的时候,将会委托该方法来处理。1
2
3
4 (SpittleNotFoundException.class)
public String spittleNotFoundHandler() {
return "error/spittleNotFound";
}
这个方法现在只能处理它所属类的方法抛出的异常,如果要处理程序中所有的会抛出该异常的controller,那么可以使用通知(@ControllerAdvice)
7.3 全局处理@ControllerAdvice
在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。
@ControllerAdvice注解本身已经使用了@Component,因此@ControllerAdvice注解所标注的类将会自动被组件扫描获取到,就像带有@Component注解的类一样。1
2
3
4
5
6
7
public class AppWideExceptionHandler {
(SpittleNotFoundException.class)
public String spittleNotFoundHandler() {
return "error/spittleNotFound";
}
}
参考文献
[1] Java数据校验(Bean Validation / JSR303)
[2] 《Spring实战》4th