Fork me on GitHub
ookamiAntD's Blog

Spring Boot学习之MVC与Validation

Preface

此篇大部分是对Spring MVC的一个回顾以及JSR303中bean validation规范的学习

Spring MVC 相关

Spring MVC 流程

1、 用户发送请求至前端控制器DispatcherServlet

2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet

4、 DispatcherServlet调用HandlerAdapter处理器适配器。

5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

6、 Controller执行完成返回ModelAndView

7、 HandlerAdaptercontroller执行结果ModelAndView返回给DispatcherServlet

8、 DispatcherServletModelAndView传给ViewReslover视图解析器。

9、 ViewReslover解析后返回具体View

10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

11、 DispatcherServlet响应用户。

Spring MVC集成fastjson

https://github.com/alibaba/fastjson/wiki/%E5%9C%A8-Spring-%E4%B8%AD%E9%9B%86%E6%88%90-Fastjson

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>

两种方式:

方式一、实现WebMvcConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class WebMvcMessageConvertConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();

SerializeConfig serializeConfig = SerializeConfig.globalInstance;
serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
serializeConfig.put(Long.class, ToStringSerializer.instance);
serializeConfig.put(Long.TYPE, ToStringSerializer.instance);

FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.forName("UTF-8"));
fastJsonConfig.setSerializeConfig(serializeConfig);
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");

fastConverter.setFastJsonConfig(fastJsonConfig);

converters.add(fastConverter);
}
}

方式二、通过@Bean方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class WebMvcMessageConvertConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverter() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();

SerializeConfig serializeConfig = SerializeConfig.globalInstance;
serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
serializeConfig.put(Long.class, ToStringSerializer.instance);
serializeConfig.put(Long.TYPE, ToStringSerializer.instance);

FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.forName(Constant.CHARSET));
fastJsonConfig.setSerializeConfig(serializeConfig);
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat(Constant.DATE_FORMAT);

fastConverter.setFastJsonConfig(fastJsonConfig);
return new HttpMessageConverters((HttpMessageConverter<?>) fastConverter);
}
}

WebFlux

上面针对的是Web MVC,对于Webflux目前不支持这种方式,可参考以下方式

Spring Boot JSON (Date类型入参、格式化,以及如何处理null)

1
2
3
4
5
spring:
jackson:
default-property-inclusion: non_null # 忽略 json 中值为null的属性
date-format: "yyyy-MM-dd HH:mm:ss" # 设置 pattern
time-zone: GMT+8 # 修正时区
  • 时间格式可以在实体上使用该注解:@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
  • 忽略null属性可以在实体上使用:@JsonInclude(JsonInclude.Include.NON_NULL)

Spring Boot MVC特性

Spring boot 在spring默认基础上,自动配置添加了以下特性

  • 包含了ContentNegotiatingViewResolverBeanNameViewResolver beans。
  • 对静态资源的支持,包括对WebJars的支持。
  • 自动注册ConverterGenericConverterFormatter beans。
  • HttpMessageConverters的支持。
  • 自动注册MessageCodeResolver
  • 对静态index.html的支持。
  • 对自定义Favicon的支持。
  • 主动使用ConfigurableWebBindingInitializer bean

模板引擎的选择

  • FreeMarker
  • Thymeleaf
  • Velocity (1.4版本之后弃用,Spring Framework 4.3版本之后弃用)
  • Groovy
  • Mustache

注:jsp应该尽量避免使用,原因如下:

  • jsp只能打包为:war格式,不支持jar格式,只能在标准的容器里面跑(tomcat,jetty都可以)
  • 内嵌的Jetty目前不支持JSP
  • Undertow不支持jsp
  • jsp自定义错误页面不能覆盖spring boot 默认的错误页面

开启GZIP算法压缩响应流

1
2
3
4
server:
compression:
enabled: true # 启用压缩
min-response-size: 2048 # 对应Content-Length,超过这个值才会压缩

全局异常处理

方式一:添加自定义的错误页面

  • html静态页面:在resources/public/error/ 下定义. 如添加404页面: resources/public/error/404.html页面,中文注意页面编码
  • 模板引擎页面:在templates/error/下定义. 如添加5xx页面: templates/error/5xx.ftl

注:templates/error/ 这个的优先级比较resources/public/error/

方式二:通过@ControllerAdvice

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
@Slf4j
@ControllerAdvice
//@RestControllerAdvice
public class ErrorExceptionHandler {

@ExceptionHandler({ RuntimeException.class })
@ResponseStatus(HttpStatus.OK)
public ModelAndView processException(RuntimeException exception) {
log.info("自定义异常处理-RuntimeException");
ModelAndView m = new ModelAndView();
m.addObject("roncooException", exception.getMessage());
m.setViewName("error/500");
return m;
}

@ExceptionHandler({ Exception.class })
@ResponseStatus(HttpStatus.OK)
public ModelAndView processException(Exception exception) {
log.info("自定义异常处理-Exception");
ModelAndView m = new ModelAndView();
m.addObject("roncooException", exception.getMessage());
m.setViewName("error/500");
return m;
}
}

或者继承ResponseEntityExceptionHandler更灵活地控制状态码、Header等信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

// @ResponseStatus(HttpStatus.OK)
@ExceptionHandler(value = { Exception.class })
@Nullable
protected ResponseEntity<Object> handleConflict(Exception ex, WebRequest request) {
String bodyOfResponse = ex.getMessage();
HttpHeaders headers = new HttpHeaders();
headers.set(CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}

更多方式请看:http://www.baeldung.com/exception-handling-for-rest-with-spring

静态资源

设置静态资源放到指定路径下

1
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/static/

自定义消息转化器

1
2
3
4
5
@Bean
public StringHttpMessageConverter stringHttpMessageConverter() {
StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
return converter;
}

自定义SpringMVC的拦截器

有些时候我们需要自己配置SpringMVC而不是采用默认,比如增加一个拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyInterceptor implements HandlerInterceptor {

@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
System.out.println("拦截器MyInterceptor------->3、请求结束之后被调用,主要用于清理工作。");

}

@Override
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
throws Exception {
System.out.println("拦截器MyInterceptor------->2、请求之后调用,在视图渲染之前,也就是Controller方法调用之后");

}

@Override
public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception {
System.out.println("拦截器MyInterceptor------->1、请求之前调用,也就是Controller方法调用之前。");
return true;//返回true则继续向下执行,返回false则取消当前请求
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class InterceptorConfigurerAdapter extends WebMvcConfigurer {
/**
* 该方法用于注册拦截器
* 可注册多个拦截器,多个拦截器组成一个拦截器链
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns 添加路径
// excludePathPatterns 排除路径
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/*.*");
super.addInterceptors(registry);
}
}

或者可以使用继承HandlerInterceptorAdapter的方式,这种方式可以按需覆盖父类方法

创建 Servlet、 Filter、Listener

注解方式

直接通过@WebServlet@WebFilter@WebListener 注解自动注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebFilter(filterName = "customFilter", urlPatterns = "/*")
public class CustomFilter implements Filter {
...
}

@WebListener
public class CustomListener implements ServletContextListener {
...
}

@WebServlet(name = "customServlet", urlPatterns = "/roncoo")
public class CustomServlet extends HttpServlet {
...
}

然后需要在**Application.java 加上@ServletComponentScan注解,否则不会生效。

注意:如果同时添加了@WebFilter以及@Component,那么会初始化两次Filter,并且会过滤所有路径+自己指定的路径 ,便会出现对没有指定的URL也会进行过滤

通过编码注册

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
@Configuration
public class WebConfig {

@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
MyFilter filter = new MyFilter();
registrationBean.setFilter(filter);

List<String> urlPatterns = new ArrayList<>();
urlPatterns.add("/*");
registrationBean.setUrlPatterns(urlPatterns);
registrationBean.setOrder(1);

return registrationBean;
}

@Bean
public ServletRegistrationBean myServlet() {
MyServlet demoServlet = new MyServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(demoServlet);
List<String> urlMappings = new ArrayList<String>();
urlMappings.add("/myServlet");////访问,可以添加多个
registrationBean.setUrlMappings(urlMappings);
registrationBean.setLoadOnStartup(1);
return registrationBean;
}

@Bean
public ServletListenerRegistrationBean myListener() {
ServletListenerRegistrationBean registrationBean
= new ServletListenerRegistrationBean<>();
registrationBean.setListener(new MyListener());
registrationBean.setOrder(1);
return registrationBean;
}
}

Spring Interceptor与Servlet Filter的区别

  • Filter是基于函数回调的,而Interceptor则是基于Java反射的。
  • Filter依赖于Servlet容器,而Interceptor不依赖于Servlet容器。
  • Filter对几乎所有的请求起作用,而Interceptor只能对action请求起作用。
  • Interceptor可以访问Action的上下文,值栈里的对象,而Filter不能。
  • action的生命周期里,Interceptor可以被多次调用,而Filter只能在容器初始化时调用一次。

RequestBodyAdvice和ResponseBodyAdvice

应用场景

  • 对Request请求参数解密,对Response返回参数进行加密
  • 自定义返回信息(业务无关性的)

使用

先看一下ResponseBodyAdvice

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
public interface ResponseBodyAdvice<T> {

/**
* Whether this component supports the given controller method return type
* and the selected {@code HttpMessageConverter} type.
* @param returnType the return type
* @param converterType the selected converter type
* @return {@code true} if {@link #beforeBodyWrite} should be invoked;
* {@code false} otherwise
*/
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

/**
* Invoked after an {@code HttpMessageConverter} is selected and just before
* its write method is invoked.
* @param body the body to be written
* @param returnType the return type of the controller method
* @param selectedContentType the content type selected through content negotiation
* @param selectedConverterType the converter type selected to write to the response
* @param request the current request
* @param response the current response
* @return the body that was passed in or a modified (possibly new) instance
*/
T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);

}

其中supports方法指定是否需要执行beforeBodyWrite,其中参数returnType可以拿到Controller对应方法中的方法注解以及参数注解:returnType.getMethodAnnotation(XXXAnnotation.class)returnType.getParameterAnnotation(XXXAnnotation.class)

beforeBodyWrite可以对返回的body进行包装或加密:

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
@RestControllerAdvice(annotations = Rest.class)
public class GlobalControllerAdvisor implements ResponseBodyAdvice {
private static final String VOID = "void";
private static final String RESOURCE_NOT_FOUND = "Resource not found!";
private static final String SUCCESS = "SUCCESS";

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return Boolean.TRUE;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
Object result;
if (isVoidMethod(returnType)) {
result = genSuccessResult(null, SUCCESS);
} else if (body instanceof Result) {
result = body;
} else if (nonNull(body)) {
result = genSuccessResult(body);
} else {
result = genBadReqResult(NOT_FOUND, RESOURCE_NOT_FOUND);
}
return result;
}

private boolean isVoidMethod(MethodParameter returnType) {
return VOID.equals(returnType.getMethod().getReturnType().getName());
}
}
  • 需要在类上面添加@ControllerAdvice@RestControllerAdvice才能生效

RequestBodyAdvicebeforeBodyRead在拦截器之后执行,所以可以在拦截器做签名检验,然后在RequestBodyAdvice中解密请求参数

Validation

常用注解(大部分JSR中已有)

注解 类型 说明
@AssertFalse Boolean,boolean 验证注解的元素值是false
@AssertTrue Boolean,boolean 验证注解的元素值是true
@NotNull 任意类型 验证注解的元素值不是null
@Null 任意类型 验证注解的元素值是null
@Min(value=值) BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 验证注解的元素值大于等于@Min指定的value值
@Max(value=值) 和@Min要求一样 验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值) 和@Min要求一样 验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值) 和@Min要求一样 验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数) 和@Min要求一样 验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限) 字符串、Collection、Map、数组等 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Past java.util.Date,java.util.Calendar;Joda Time类库的日期类型 验证注解的元素值(日期类型)比当前时间早
@Future 与@Past要求一样 验证注解的元素值(日期类型)比当前时间晚
@NotBlank CharSequence子类型 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限) CharSequence子类型 验证注解的元素值长度在min和max区间内
@NotEmpty CharSequence子类型、Collection、Map、数组 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值) BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式) CharSequence子类型(如String) 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式) String,任何CharSequence的子类型 验证注解的元素值与指定的正则表达式匹配
@Valid 任何非原子类型 指定递归验证关联的对象;如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

简单使用

实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class Foo {
@NotBlank
private String name;

@Min(18)
private Integer age;

@Pattern(regexp = "^1([34578])\\d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;

@Email(message = "邮箱格式错误")
private String email;
}

Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@Slf4j
public class FooController {

@PostMapping("/foo")
public String foo(@Validated Foo foo, BindingResult bindingResult) {
log.info("foo: {}", foo);
if (bindingResult.hasErrors()) {
for (FieldError fieldError : bindingResult.getFieldErrors()) {
log.error("valid fail: field = {}, message = {}", fieldError.getField(), fieldError.getDefaultMessage());
}
return "fail";
}
return "success";
}
}

快速失效

一般情况下,Validator并不会应为第一个校验失败为停止,而是一直校验完所有参数。我们可以通过设置快速失效:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
// .addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}

这样在遇到第一个校验失败的时候就会停止对之后的参数校验。

分组校验

如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。未成年人是不能喝酒的,而在其他场景下我们不做特殊的限制,这个需求如何体现同一个实体,不同的校验规则呢?

添加分组:

1
2
3
4
5
6
7
8
Class Foo{
@Min(value = 18,groups = {Adult.class})
private Integer age;

public interface Adult{}

public interface Minor{}
}

Controller

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/drink")
public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
if(bindingResult.hasErrors()){
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//...
}
return "fail";
}
return "success";
}

自定义校验

业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。自定义spring validation非常简单,主要分为两步。

1 自定义校验注解
我们尝试添加一个“字符串不能包含空格”的限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CannotHaveBlankValidator.class})<1>
public @interface CannotHaveBlank {

//默认错误消息
String message() default "不能包含空格";

//分组
Class<?>[] groups() default {};

//负载
Class<? extends Payload>[] payload() default {};

//指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CannotHaveBlank[] value();
}

}

我们不需要关注太多东西,使用spring validation的原则便是便捷我们的开发,例如payload,List ,groups,都可以忽略。

<1> 自定义注解中指定了这个注解真正的验证者类。

2 编写真正的校验者类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> {

@Override
public void initialize(CannotHaveBlank constraintAnnotation) {
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context <2>) {
//null时不进行校验
if (value != null && value.contains(" ")) {
<3>
//获取默认提示信息
String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultConstraintMessageTemplate);
//禁用默认提示信息
context.disableDefaultConstraintViolation();
//设置提示语
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
return false;
}
return true;
}
}

<1> 所有的验证者都需要实现ConstraintValidator接口,它的接口也很形象,包含一个初始化事件方法,和一个判断是否合法的方法

1
2
3
4
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A constraintAnnotation);
boolean isValid(T value, ConstraintValidatorContext context);
}

<2> ConstraintValidatorContext 这个上下文包含了认证中所有的信息,我们可以利用这个上下文实现获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作。

<3> 一些典型校验操作,或许可以对你产生启示作用。

值得注意的一点是,自定义注解可以用在METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER之上,ConstraintValidator的第二个泛型参数T,是需要被校验的类型。

手动校验

可能在某些场景下需要我们手动校验,即使用校验器对需要被校验的实体发起validate,同步获得校验结果。理论上我们既可以使用Hibernate Validation提供Validator,也可以使用Spring对其的封装。在spring构建的项目中,提倡使用经过spring封装过后的方法,这里两种方法都介绍下:

Hibernate Validation

1
2
3
4
5
6
7
8
9
Foo foo = new Foo();
foo.setAge(22);
foo.setEmail("000");
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<Foo>> set = validator.validate(foo);
for (ConstraintViolation<Foo> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}

由于依赖了Hibernate Validation框架,我们需要调用Hibernate相关的工厂方法来获取validator实例,从而校验。

在spring framework文档的Validation相关章节,可以看到如下的描述:

Spring provides full support for the Bean Validation API. This includes convenient support for bootstrapping a JSR-303/JSR-349 Bean Validation provider as a Spring bean. This allows for a javax.validation.ValidatorFactory or javax.validation.Validator to be injected wherever validation is needed in your application. Use the LocalValidatorFactoryBean to configure a default Validator as a Spring bean:

bean id=”validator” class=”org.springframework.validation.beanvalidation.LocalValidatorFactoryBean”

The basic configuration above will trigger Bean Validation to initialize using its default bootstrap mechanism. A JSR-303/JSR-349 provider, such as Hibernate Validator, is expected to be present in the classpath and will be detected automatically.

上面这段话主要描述了spring对validation全面支持JSR-303、JSR-349的标准,并且封装了LocalValidatorFactoryBean作为validator的实现。值得一提的是,这个类的责任其实是非常重大的,他兼容了spring的validation体系和hibernate的validation体系,也可以被开发者直接调用,代替上述的从工厂方法中获取的hibernate validator。由于我们使用了springboot,会触发web模块的自动配置,LocalValidatorFactoryBean已经成为了Validator的默认实现,使用时只需要自动注入即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
Validator globalValidator; <1>

@RequestMapping("/validate")
public String validate() {
Foo foo = new Foo();
foo.setAge(22);
foo.setEmail("000");

Set<ConstraintViolation<Foo>> set = globalValidator.validate(foo);<2>
for (ConstraintViolation<Foo> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}

return "success";
}

<1> 真正使用过Validator接口的读者会发现有两个接口,一个是位于javax.validation包下,另一个位于org.springframework.validation包下,注意我们这里使用的是前者javax.validation,后者是spring自己内置的校验接口,LocalValidatorFactoryBean同时实现了这两个接口。

<2> 此处校验接口最终的实现类便是LocalValidatorFactoryBean

基于方法校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@Validated <1>
public class BarController {

@RequestMapping("/bar")
public @NotBlank <2> String bar(@Min(18) Integer age <3>) {
System.out.println("age : " + age);
return "";
}

@ExceptionHandler(ConstraintViolationException.class)
public Map handleConstraintViolationException(ConstraintViolationException cve){
Set<ConstraintViolation<?>> cves = cve.getConstraintViolations();<4>
for (ConstraintViolation<?> constraintViolation : cves) {
System.out.println(constraintViolation.getMessage());
}
Map map = new HashMap();
map.put("errorCode",500);
return map;
}

}

<1> 为类添加@Validated注解

<2> <3> 校验方法的返回值和入参

<4> 添加一个异常处理器,可以获得没有通过校验的属性相关信息

基于方法的校验,个人不推荐使用,感觉和项目结合的不是很好。

统一处理验证异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ControllerAdvice
@Component
public class GlobalExceptionHandler {

@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ValidationException exception) {
if(exception instanceof ConstraintViolationException){
ConstraintViolationException exs = (ConstraintViolationException) exception;

Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
          /**打印验证不通过的信息*/
System.out.println(item.getMessage());
}
}
return "bad request, " ;
}
}

参考:
https://www.cnkirito.moe/2017/08/16/%E4%BD%BF%E7%94%A8spring%20validation%E5%AE%8C%E6%88%90%E6%95%B0%E6%8D%AE%E5%90%8E%E7%AB%AF%E6%A0%A1%E9%AA%8C/

---------------- The End ----------------
ookamiAntD wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
谢谢大爷~

Author:ookamiAntD Yang
Link:http://yangbingdong.com/2018/spring-boot-mvc-validation/
Contact:yangbingdong1994@gmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!

分享到: