Spring+JSR的数据校验
起因
今天在写一个简单的接口的时候,想对参数进行校验
1
2
3
4
5
6
7
8
9
10
11
public class UserController {
private final UserService userService;
public User login( { String username, String password)
return userService.login(username, password);
}
}在调用的过程中,发现校验没有生效
搞不懂为什么会失败??
正确写法
在书写业务逻辑的时候,是否会经常书写大量的判空校验。比如
Service
层或者Dao
层的方法入参、入参对象、出参中,比如有些字段必传,有的非必传;返回值中有些字段必须有值,有的非必须等等~如上描述的校验逻辑,窥探一下你的代码,估摸里面有大量的
if else
吧。此部分逻辑简单(因为和业务关系不大)却看起来眼花缭乱。这部分在我们眼中就是垃圾代码。Bean Validation
校验其实是基于DDD
思想设计的,我们虽然可以不完全的遵从这种思考方式编程,但是其优雅的优点还是可取的,本文将介绍Spring
为此提供的解决方案~
Spring
+JSR
相关约束体验
首先就来体验一把吧~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface UserService {
User login(; String username, String password)
}
// 实现类如下
public class UserServiceImpl implements UserService {
private final UserDao userDao;
public User login(String username, String password) {
User user = userDao.getOne(new QueryWrapper<User>().lambda().eq(User::getUsername, username));
if (user != null && password.equals(user.getPassword())) {
return user;
}
return null;
}
}向容器里注册一个处理器:
1
2
3
4
5
6
7
public class RootConfig {
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}测试:
1
2
3
4
5
6
7
8
9
10
11
class TestSpringBean {
private UserService userService;
public void test1() {
System.out.println(userService.getClass());
userService.login(null, null);
}
}结果完美的校验住了方法入参。
若需要校验方法返回值,改写如下:
1
2
3
4
5
public interface UserService {
User login(; String username, String password)
}测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TestSpringBean {
private UserService userService;
public void test1() {
System.out.println(userService.getClass());
userService.login("admin", "admin");
}
}
javax.validation.ConstraintViolationException: login.<return value>: must not be null校验完成。就这样借助Spring+JSR相关约束注解,就非常简单明了,语义清晰的优雅的完成了方法级别(入参校验、返回值校验)的校验。
校验不通过的错误信息,再来个全局统一的异常处理,就能让整个工程都能尽显完美之势。
- 错误消息可以从异常ConstraintViolationException的getConstraintViolations()方法里获得的~
显示方法参数名称
注意此处的一个小细节: 我们显示的参数名称可能是
login.args0
等,如果想是形参名。可以使用ava8的编译参数:-parameters
,让我们在运行期获取方法参数名称。普通java工程在 File->Settings->Build,Execution,Deployment->Compiler->Java Compiler-> Additional command line parameters: 后面填上 -parameters,如下图
此处说一点: 若你的逻辑中强依赖于此参数,务必在你的maven中加入编译插件并且配置好此编译参数
maven设置编译参数,版本3.6.2之前
1
2
3
4
5
6
7
8<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<compilerArgs>-parameters</compilerArgs>
</configuration>
</plugin>版本3.6.2及之后
1
2
3
4
5
6
7
8<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
SpringBoot项目如果继承自spring-boot-starter-parent,会默认开启。
MethodValidationPostProcessor
它是Spring提供的来实现基于方法Method的JSR校验的核心处理器~它能让约束作用在方法入参、返回值上,如:
1
2
3
4public interface UserService {
User login(; String username, String password)
}官方说明: 方法里写有JSR校验注解要想其生效的话,要求类型级别上必须使用@Validated标注(还能指定验证的Group)
@Validated(Default.class)
另外提示一点: 这个处理器同处理@Async的处理器AsyncAnnotationBeanPostProcessor非常相似,都是继承自AbstractBeanFactoryAwareAdvisingPostProcessor的
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
40
41
42
43
44
45
46
47
48
49// @since 3.1
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
// 备注: 此处你标注@Valid是无用的~~~Spring可不提供识别
// 当然你也可以自定义注解(下面提供了set方法~~~)
// 但是注意: 若自定义注解的话,此注解只决定了是否要代理,并不能指定分组哦 so,没啥事别给自己找麻烦吧
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
// 这个是javax.validation.Validator
private Validator validator;
// 可以自定义生效的注解
public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
this.validatedAnnotationType = validatedAnnotationType;
}
// 这个方法注意了: 你可以自己传入一个Validator,并且可以是定制化的LocalValidatorFactoryBean哦~(推荐)
public void setValidator(Validator validator) {
// 建议传入LocalValidatorFactoryBean功能强大,从它里面生成一个验证器出来靠谱
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
} else if (validator instanceof SpringValidatorAdapter) {
this.validator = validator.unwrap(Validator.class);
} else {
this.validator = validator;
}
}
// 当然,你也可以简单粗暴的直接提供一个ValidatorFactory即可~
public void setValidatorFactory(ValidatorFactory validatorFactory) {
this.validator = validatorFactory.getValidator();
}
// 毫无疑问,Pointcut使用AnnotationMatchingPointcut,并且支持内部类哦~
// 说明@Aysnc使用的也是AnnotationMatchingPointcut,只不过因为它支持标注在类上和方法上,所以最终是组合的ComposablePointcut
// 至于Advice通知,此处一样的是个`MethodValidationInterceptor`~~~~
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
// 这个advice就是给@Validation的类进行增强的~ 说明: 子类可以覆盖哦~
// @since 4.2
protected Advice createMethodValidationAdvice( { Validator validator)
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}它是个普通的BeanPostProcessor,为Bean创建的代理的时机是postProcessAfterInitialization(),也就是在Bean完成初始化后有必要的话用一个代理对象返回进而交给Spring容器管理~(同@Aysnc)
容易想到,关于校验方面的逻辑不在于它,而在于切面的通知: MethodValidationInterceptor
MethodValidationInterceptor
它是AOP联盟类型的通知,此处专门用于处理方法级别的数据校验。
注意理解方法级别: 方法级别的入参有可能是各种平铺的参数、也可能是一个或者多个对象
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66// @since 3.1 因为它校验Method 所以它使用的是javax.validation.executable.ExecutableValidator
public class MethodValidationInterceptor implements MethodInterceptor {
// javax.validation.Validator
private final Validator validator;
// 如果没有指定校验器,那使用的就是默认的校验器
public MethodValidationInterceptor() {
this(Validation.buildDefaultValidatorFactory());
}
public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
this(validatorFactory.getValidator());
}
public MethodValidationInterceptor(Validator validator) {
this.validator = validator;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
// 如果是FactoryBean.getObject() 方法 就不要去校验了~
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
Class<?>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API ExecutableValidator是1.1提供的
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result; // 错误消息result 若存在最终都会ConstraintViolationException异常形式抛出
try {
// 先校验方法入参
result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
} catch (IllegalArgumentException ex) {
// 此处回退了异步: 找到bridged method方法再来一次
methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) { // 有错误就抛异常抛出去
throw new ConstraintViolationException(result);
}
// 执行目标方法 拿到返回值后 再去校验这个返回值
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
// 找到这个方法上面是否有标注@Validated注解 从里面拿到分组信息
// 备注: 虽然代理只能标注在类上,但是分组可以标注在类上和方法上哦~~~~
protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
if (validatedAnn == null) {
validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
}
return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
}
}这个Advice的实现,简单到不能再简单了,稍微有点基础的应该都能很容易看懂吧(据我不完全估计这个应该是最简单的)。
使用细节(重要)
使用
@Validated
去校验方法Method
,不管从使用上还是原理上,都是非常简单和简约的,建议大家在企业应用中多多使用。
- 文首虽然已经给了一个使用示例,但是那毕竟只是局部。在实际生产使用中,比如上面理论更重要的是一些使用细节(细节往往是区分你是不是高手的地方),这里从我使用的经验中,总结如下几点供给大家参考(基本算是分享我躺过的坑):
约束注解(如@NotNull)不能放在实体类上
一般情况下,我们对于Service层验证(Controller层一般都不给接口),大都是面向接口编程和使用,那么这种@NotNull放置的位置应该怎么放置呢?
看这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13public interface HelloService {
Object hello( Integer id, String name);
}
public class HelloServiceImpl implements HelloService {
public Object hello( { Integer id, String name)
return null;
}
}约束条件都写在实现类上,按照我们所谓的经验,应该是不成问题的。但运行:
1
2
3javax.validation.ConstraintDeclarationException: HV000151:
A method overriding another method must not redefine the parameter constraint(约束) configuration,
but method UserServiceImpl#login(String, String) redefines the configuration of UserService#login(String, String).
请务必注意请务必注意请务必注意这个异常是javax.validation.
ConstraintDeclarationException
(约束声明异常),而不是错误校验错误异常javax.validation.ConstraintViolationException
(约束冲突异常)。请在做全局异常捕获的时候一定要区分开来~
异常信息是说
parameter constraint configuration
在校验方法入参的约束时,若是@Override
父类/接口的方法,那么这个入参约束只能写在父类/接口上面~~~至于为什么只能写在接口处,这个具体原因其实是和
Bean Validation
的实现产品有关的,比如使用的Hibernate校验,原因可参考它的此类:OverridingMethodMustNotAlterParameterConstraints
还需注意一点: 若实现类写的约束和接口一模一样,那也是没问题的。比如上面若接口这么写是没有问题能够完成正常校验的:
@Validated(Default.class) public interface UserService { @NotNull User login(@NotBlank String username, @NotBlank String password); }
虽然能正常work完成校验,但需要深刻理解一模一样这四个字。简单的说把@min(0)改成@min(1)都会报ConstraintDeclarationException异常,更别谈移除某个注解了(不管多少字段多少注解,但凡只要写了一个就必须保证一模一样)。
关于@Override方法校验返回值方面: 即使写在实现类里也不会抛ConstraintDeclarationException
另外@Validated注解它写在实现类/接口上均可~
最后你应该自己领悟到: 若入参校验失败了,方法体是不会执行的。但倘若是返回值校验执行了(即使是失败了),方法体也肯定被执行了~~
@NotEmpty/@NotBlank只能哪些类型上?
提出这个细节的目的是: 约束注解并不是能用在所有类型上的。比如若你把@NotEmpty让它去验证Object类型,它会报错如下:
1
2
3javax.validation.UnexpectedTypeException: HV000030:
No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Object'.
Check configuration for 'hello.<return value>'需要强调的是: 若标注在方法上是验证返回值的,这个时候方法体是已经执行了的,这个和ConstraintDeclarationException不一样~
对这两个注解依照官方文档做如下简要说明。@NotEmpty只能标注在如下类型
1
2
3
4CharSequence
Collection
Map
Array注意:
"“
它是空的,但是” "
(空格字符串)就不是了@NotBlank只能使用在CharSequence上,它是Bean Validation 2.0新增的注解~
接口和实现类上都有注解,以谁为准?
这个问题有个隐含条件: 只有校验方法返回值时才有这种可能性。
1
2
3
4
5
6
7
8
9
10
11
12
13public interface HelloService {
hello(; Integer id, String name) String
}
public class HelloServiceImpl implements HelloService {
public String hello(Integer id, String name) {
return "";
}
}运行返回结果
1
javax.validation.ConstraintViolationException: hello.<return value>: must not be empty
到这里,可能有小伙伴就会早早下结论: 当同时存在时,以接口的约束为准。
那么,我只把返回值稍稍修改,你再看一下呢???
1
2
3
4@Override
public @NotNull String hello(Integer id, String name) {
return null; // 返回值改为null
}再运行:
1
javax.validation.ConstraintViolationException: hello.<return value>: must not be empty, hello.<return value>: must not be null
透过打印的信息,结论就自然不必我多。但是有个道理此处可说明: 大胆猜测,小心求证
如何校验级联属性?
在实际开发中,其实大多数情况下我们方法入参是个对象(甚至对象里面有对象),而不是单单平铺的参数,因此就介绍一个级联属性校验的例子:
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
40
41
42
public class Person {
private String name;
// @PositiveOrZero // 参数必须是正整数或0
private Integer age;
// 让InnerChild的属性也参与校验
private InnerChild child;
public static class InnerChild {
private String name;
private Integer age;
}
}
public interface HelloService {
String cascade(; Person father, Person mother)
}
public class HelloServiceImpl implements HelloService {
public String cascade(Person father, Person mother) {
return "hello cascade...";
}
}测试
1
2
3
4
public void test1() {
helloService.cascade(null, null);
}此处说明一点: 若你
father
前面没加@NotNull
,那打印的消息只有:cascade.mother: 不能为null
我把测试用例改造如下,你继续感受一把:
@Test public void test1() { Person father = new Person(); father.setName("fsx"); Person.InnerChild innerChild = new Person.InnerChild(); innerChild.setAge(-1); father.setChild(innerChild); helloService.cascade(father, new Person()); }
错误消息如下(请小伙伴仔细观察和分析缘由):
1
cascade.father.age: 不能为null, cascade.father.child.name: 不能为null, cascade.father.child.age: 必须是正数
思考: 为何mother的相关属性以及子属性为何全都没有校验呢?
循环依赖问题
上面说了Spring对@Validated的处理和对@Aysnc的代理逻辑是差不多的,有了之前的经验,很容易想到它也存在着如题的问题:
比如HelloService的A方法想调用本类的B方法,但是很显然我是希望B方法的方法校验是能生效的,因此其中一个做法就是注入自己,使用自己的代理对象来调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public interface HelloService {
Object hello(; Integer id, String name)
String cascade(; Person father, Person mother)
}
public class HelloServiceImpl implements HelloService {
private HelloService helloService;
public Object hello( { Integer id, String name)
helloService.cascade(null, null); // 调用本类方法
return null;
}
public String cascade(Person father, Person mother) {
return "hello cascade...";
}
}运行测试用例:
1
2
3
4
public void test1() {
helloService.hello(18, "fsx"); // 入口方法校验通过,内部调用cascade方法希望继续得到校验
}运行报错:
1
2Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'helloServiceImpl': Bean with name 'helloServiceImpl' has been injected into other beans [helloServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean这个报错消息不可为不熟悉。关于此现象,之前做过非常非常详细的说明并且提供了多种解决方案,所以此处略过。
1
2
3把allowRawInjectionDespiteWrapping设置为true
使用 或者 解决
不要让 的Bean参与循环依赖提供问题解决后运行的打印输出情况,供给小伙伴调试参考
1
javax.validation.ConstraintViolationException: cascade.mother: 不能为null, cascade.father: 不能为null
Spring中校验总结
Spring提供给我们方法级别校验的能力,在企业应用中使用此种方式完成绝大部分的基本校验工作,能够让我们的代码更加简洁、可控并且可扩展,因此我是推荐使用和扩散的~
有必要强调一点: 关于上面级联属性的校验时使用的@Valid注解你使用@Validated可替代不了,不会有效果的。
稍稍说一下它的弊端: 因为校验失败它最终采用的是抛异常方式来中断,因此效率上有那么一丢丢的损耗。but,你的应用真的需要考虑这种极致性能问题吗?这才是你该思考的~
SpringMCV校验
它在Spring MVC(Controller层)里怎么应用呢?本文为此继续展开讲解Spring MVC中的数据校验~
- 可能小伙伴能立马想到: 这不一样吗?我们使用Controller就是方法级别的,所以它就是直接应用了方法级别的校验而已嘛~
- 对于此疑问我先不解答,而是顺势再抛出两个问题你自己应该就能想明白了:
- 上文有说过,基于方法级别的校验Spring默认是并未开启的,但是为什么你在Spring MVC却可以直接使用@Valid完成校验呢?
- 可能有的小伙伴说他用的是SpringBoot可能默认给开启了,其实不然。哪怕你用的传统Spring MVC你会发现也是直接可用的,不信你就试试
- 类比一下: Spring MVC的HandlerInterceptor是AOP思想的实现,但你有没有发现即使你没有启动@EnableAspectJAutoProxy的支持,它依旧好使~
- 上文有说过,基于方法级别的校验Spring默认是并未开启的,但是为什么你在Spring MVC却可以直接使用@Valid完成校验呢?
- 若你能想明白我提出的这两个问题,下文就非常不难理解了。当然即使你知道了这两个问题的答案,还是建议你读下去。毕竟: 永远相信本文能给你带来意想不到的收获~
使用示例
关于数据校验这一块在Spring MVC中的使用案例,我相信但凡有点经验的Java程序员应该没有不会使用的,并且还不乏熟练的选手。
在此之前我简单“采访”过,绝大多数程序员甚至一度认为Spring中的数据校验就是指的在Controller中使用@Validated校验入参JavaBean这一块~
因此下面这个例子,你应该一点都不陌生:
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
public class Person {
private String name;
private Integer age;
// 让InnerChild的属性也参与校验
private InnerChild child;
public static class InnerChild {
private String name;
private Integer age;
}
}controller层
1
2
3
4
5
6
7
8
9
10
public class HelloController {
public Object helloPost( { Person person, BindingResult result)
System.out.println(result.getErrorCount());
System.out.println(result.getAllErrors());
return person;
}
}发送post请求:
/hello
Content-Type=application/json
,传入的json串如下:1
2
3
4
5
6
7{
"name": "fsx",
"age": "-1",
"child": {
"age": 1
}
}控制台有如下打印:
1
22
[Field error in object 'person' on field 'child.name': rejected value [null]; codes [NotNull.person.child.name,NotNull.child.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.child.name,child.name]; arguments []; default message [child.name]]; default message [不能为null], Field error in object 'person' on field 'age': rejected value [-1]; codes [Positive.person.age,Positive.age,Positive.java.lang.Integer,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.age,age]; arguments []; default message [age]]; default message [必须是正数]]从打印上看: 校验生效(拿着错误消息就可以返回前端展示,或者定位到错误页面都行)。
**此例两个小细节务必注意: **
- @RequestBody注解不能省略,否则传入的json无法完成数据绑定(即使不绑定,校验也是生效的哦)~
- 若方法入参不写BindingResult result这个参数,请求得到的直接是400错误,因为若有校验失败的服务端会抛出异常org.springframework.web.bind.MethodArgumentNotValidException。
- 若写了,那就调用者自己处理喽~
- 据我不完全和不成熟的统计,就这个案例就覆盖了小伙伴们实际使用中的90%以上的真实使用场景,使用起来确实非常的简单、优雅、高效~
但是作为一个有丰富经验的程序员的你,虽然你使用了@Valid优雅的完成了数据校验,但回头是你是否还会发现你的代码里还是存在了大量的if else的基础的校验?什么原因?
- 其实根本原因只有一个: 很多case使用@Valid并不能覆盖,因为它只能校验JavaBean
- 我相信你是有这样那样的使用痛点的,本文先从原理层面分析,进而给出你所遇到的痛点问题的参考解决参考方案~
@Validated和@Valid的区别
如题的问题,我相信是很多小伙伴都很关心的一个对比,若你把这个系列都有喵过,那么这个问题的答案就浮出水面了:
@Valid: 标准JSR-303规范的标记型注解,用来标记验证属性和方法返回值,进行级联和递归校验
@Validated: Spring的注解,是标准JSR-303的一个变种(补充),提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制
- 在Controller中校验方法参数时,使用@Valid和@Validated并无特殊差异(若不需要分组校验的话)
@Validated注解可以用于类级别,用于支持Spring进行方法级别的参数校验。@Valid可以用在属性级别约束,用来表示级联校验。
@Validated只能用在类、方法和参数上,而@Valid可用于方法、字段、构造器和参数上
对平铺参数执行数据校验
- 我们知道
Spring MVC
层是默认可以支持Bean Validation
的 - 但是我在实际使用起来有很多不便之处(相信我的使用痛点也是小伙伴的痛点),就感觉它是个半拉子: 只支持对
JavaBean
的验证,而并不支持对Controller
处理方法的平铺参数的校验。 - 我们Controller控制器方法中入参,其实大部分情况下都是平铺参数而非JavaBean的。然而对于平铺参数我们并不能使用@Validated像校验JavaBean一样去做,并且Spring MVC也并没有提供源生的解决方案(其实提供了,哈哈)。
- 那怎么办?难道真的只能自己书写重复的if else去完成吗?当然不是,那么本文将对此常见的痛点问题(现象)提供两种思路,供给使用者参考~
Controller层平铺参数的校验
- 因为Spring MVC并不天然支持对控制器方法平铺参数的数据校验,但是这种case的却有非常的常见,因此针对这种常见现象提供一些可靠的解决方案,对你的项目的收益是非常高的。
方案一: 借助Spring对方法级别数据校验的能力
首先必须明确一点: 此能力属于Spring框架的,而不是web框架Spring MVC。
Spring对方法级别数据校验的能力非常重要(它能对Service层、Dao层的校验等),前面也重点分析过
Spring方法级别数据校验: @Validated + MethodValidationPostProcessor优雅的完成数据校验动作
使用此种方案来解决问题的步骤比较简单,使用起来也非常方便。下面我写个简单示例作为参考:
1
2
3
4
5
6
7
8
public class WebMvcConfig extends WebMvcConfigurerAdapter {
public MethodValidationPostProcessor mvcMethodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}在
Controller
中 类 上使用@Validated
标注,然后方法上正常使用约束注解标注平铺的属性:1
2
3
4
5
6
7
8
9@RestController
@RequestMapping
@Validated
public class HelloController {
@PutMapping("/hello/id/{id}/status/{status}")
public Object helloGet(@Max(5) @PathVariable Integer id, @Min(5) @PathVariable Integer status) {
return "hello world";
}
}请求:
/hello/id/6/status/4
可看见抛异常由此可见,校验生效了。抛出了javax.validation.ConstraintViolationException异常,这样我们再结合一个全局异常的处理程序,也就能达到我们预定的效果了~
这种方案一样有一个非常值得注意但是很多人都会忽略的地方: 因为我们希望能够代理Controller这个Bean,所以仅仅只在父容器中配置MethodValidationPostProcessor是无效的,必须在子容器(web容器)的配置文件中再配置一个MethodValidationPostProcessor,请务必注意~
- 有小伙伴问我了,为什么它的项目里只配置了一个
MethodValidationPostProcessor
也生效了呢? 我的回答是: 检查一下你是否是用的SpringBoot。
借助HandlerInterceptor做拦截处理(轻量)
- 方案一的使用已经很简单了,但我个人总还觉得怪怪的,因为我一直不喜欢Controller层被代理(可能是洁癖吧)。
- 因此针对这个现象,我自己接下来提供一个自定义拦截器HandlerInterceptor的处理方案来实现,大家不一定要使用,也是供以参考嘛~
- 设计思路: Controller拦截器 + @Validated注解 + 自定义校验器(当然这里面涉及到不少细节的: 比如入参解析、绑定等等内置的API)
准备一个拦截器ValidationInterceptor用于处理校验逻辑:
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100// 注意: 此处只支持@RequesrMapping方式~~~~
public class ValidationInterceptor implements HandlerInterceptor, InitializingBean {
private LocalValidatorFactoryBean validatorFactoryBean;
private RequestMappingHandlerAdapter adapter;
private List<HandlerMethodArgumentResolver> argumentResolvers;
public void afterPropertiesSet() throws Exception {
argumentResolvers = adapter.getArgumentResolvers();
}
// 缓存
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 只处理HandlerMethod方式
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
Validated valid = method.getMethodAnnotation(Validated.class); //
if (valid != null) {
// 根据工厂,拿到一个校验器
ValidatorImpl validatorImpl = (ValidatorImpl) validatorFactoryBean.getValidator();
// 拿到该方法所有的参数们~~~ org.springframework.core.MethodParameter
MethodParameter[] parameters = method.getMethodParameters();
Object[] parameterValues = new Object[parameters.length];
//遍历所有的入参: 给每个参数做赋值和数据绑定
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
// 找到适合解析这个参数的处理器~
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
WebDataBinderFactory webDataBinderFactory = getDataBinderFactory(method);
Object value = resolver.resolveArgument(parameter, mavContainer, new ServletWebRequest(request, response), webDataBinderFactory);
parameterValues[i] = value; // 赋值
}
// 对入参进行统一校验
Set<ConstraintViolation<Object>> violations = validatorImpl.validateParameters(method.getBean(), method.getMethod(), parameterValues, valid.value());
// 若存在错误消息,此处也做抛出异常处理 javax.validation.ConstraintViolationException
if (!violations.isEmpty()) {
System.err.println("方法入参校验失败~~~~~~~");
throw new ConstraintViolationException(violations);
}
}
}
return true;
}
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) {
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
// 支持到@InitBinder注解
methods = MethodIntrospector.selectMethods(handlerType, RequestMappingHandlerAdapter.INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(new InvocableHandlerMethod(bean, method));
}
return new ServletRequestDataBinderFactory(initBinderMethods, adapter.getWebBindingInitializer());
}
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}配置拦截器到
Web
容器里(拦截所有请求),并且自己配置一个LocalValidatorFactoryBean
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class WebMvcConfig extends WebMvcConfigurerAdapter {
// 自己配置校验器的工厂 自己随意定制化哦~
public LocalValidatorFactoryBean localValidatorFactoryBean() {
return new LocalValidatorFactoryBean();
}
// 配置用于校验的拦截器
public ValidationInterceptor validationInterceptor() {
return new ValidationInterceptor();
}
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(validationInterceptor()).addPathPatterns("/**");
}
}Controller
的方法(只需要在方法上标注即可)上标注@Validated
注解:1
2
3
4
5
6// 只需要方法处标注注解即可 非常简便
public Object helloGet( { Integer id, Integer status)
return "hello world";
}
同样的完美完成了我们的校验需求。针对我自己书写的这一套,这里继续有必要再说说两个小细节:
- 本例的@PathVariable(“id”)是指定的value值的,因为在处理@PathVariable过程中我并没有去分析字节码来得到形参名,所以为了简便此处写上value值,当然这里是可以优化的,- 有兴趣的小伙伴可自行定制
- 因为制定了value值,错误信息中也能正确识别出字段名了~
- 在Spring MVC的自动数据封装体系中,value值不是必须的,只要字段名对应上了也是ok的(这里面运用了字节码技术,后文有讲解)。但是在数据校验中,它可并没有用到字节码结束,请注意做出区分~~~
总结
本文介绍了两种方案来处理我们平时遇到Controller中对处理方法平铺类型的数据校验问题,至于具体你选择哪种方案当然是仁者见仁了。(方案一简便,方案二需要你对Spring MVC的处理流程API很熟练,可炫技)
数据校验相关知识介绍至此,不管是Java上的数据校验,还是Spring上的数据校验,都可以统一使用优雅的Bean Validation来完成了。希望这么长时间来讲的内容能对你的项目有实地的作用,真的能让你的工程变得更加的简介,甚至高能。毕竟真正做技术的人都是追求一定的极致性,甚至是存在代码洁癖,甚至是偏执的~