我目前的这套方案可以说是以Validation的思想实现我需要的需求。
已存在的校验技术及我的校验注解需求
搞了大半天的成果,虽然不能很优雅的实现我的需求,但是在研究的过程中增加了我对Validation的了解,所以还是记录一下下吧。
我听说有一种技术(我已经验证过这个技术),可以在@NotBlank的message中定义占位符,如下所示:
@NotBlank(message = "{notBlank}不为空")
然后在一个messages.properties的文件中定义:
notBlank="XXX"
当参数校验错误的时候生成的消息为:XXX不为空。更进一步的,我们可以在message.properties中这么定义:
notBlank="{0} XXX"
理想情况下,{0}将会被替换为@NotBlank标注的字段的名称,我无法成功实验该技术,但是这个技术的的确确是我想要的。我希望通过这个技术定义自己的@NotNull、@NotBlank等注解,这样我们在使用这些注解时只需要加上这些注解,而不需要写上message消息。
一种不是太满意的实现方案
这里需要特殊说明一下,其实我可以通过如下的技术实现在ExceptionHandler中拼接字段信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
public Response exceptionHandler(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
return new Response(0, fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
}
|
这种方案很简单也很好理解,但是我放弃这个方案了,其原因是ExceptionHandler是全局的,而@NotNull等注解并不一定应用于Request中的参数校验,这种方式可能会暴露程序内部的一些设计;其次,修改ExceptionHandler是框架层的东西,我对其的任何调整都会影响已经在线上运行的项目,我觉得这样风险很大。最后,我觉得这并不是一种优雅的方式。
我目前的方案
先分析一下如下代码是如何实现notBlank被替换为我们想要的字符串的。message.properties会被加载到内存中,存在一个叫做messageParams的属性中,当框架发现了message中包含大括号,则其会取到大括号的内容,然后得到该内容在messageParams中映射的值(有兴趣自己读源码吧,我是一点一点断点理解这个问题的)。
1
2
3
|
@NotBlank(message = "{notBlank}不为空")
|
针对我们的代码,正常情况下参数校验完成后的到的message是:{fieldName}:不能为空。因为我们没办法在message.properties中动态的定义fieldName,故messageParams中是找不到fieldName的。
1
2
3
4
|
@JNotBlank(message = "{fieldName}:不能为空")
private String tmpString;
|
我们可以如何处理这个问题呢?如下代码,我们在开发ConstraintValidator时,是可以在isValid方法中获得一个ConstraintValidatorContext的,通过调用该context的addMessageParameters方法,我们就可以将参数加载到messageParams,从而完成映射。
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
|
public class JNotBlankValidator implements ConstraintValidator<JNotBlank, String> {
@Override
public void initialize(JNotBlank constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
ValidationUtils.addMessageParameters(context);
return StringUtils.isNotBlank(value);
}
}
public class ValidationUtils {
/**
* 添加fieldName参数到ConstraintValidatorContext中
* <p>
* 该工具可以在ConstraintValidatorContext添加一个额外的fieldName参数,这样你在
* 使用参数校验的注解时,可以在message中使用{@code fieldName}
* <p>
* 仅当注解基于字段时,该工具有效。
*/
public static void addMessageParameters(ConstraintValidatorContext context) {
if (context instanceof HibernateConstraintValidatorContext) {
try {
Field basePathField = context.getClass().getDeclaredField("basePath");
basePathField.setAccessible(true);
Path basePath = (Path) (basePathField.get(context));
((HibernateConstraintValidatorContext) context).addMessageParameter("fieldName", basePath);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException("Wrong");
}
}
}
}
|
至于如何获得fieldName的名称,我想到的办法只有反射了,我花费了很长时间,都没有找到可以获取fieldName字段的api,遂走反射这个方案。
总结
我其实是无话可说的,这个方案是一点一点的探索出来的。不过这些技术的研究确实可以很好的减少我们工作量,我非常满意。另外,我想把这些技术开发成一个starter,然后用在我们的项目中。
参考资料
-
Setting Custom Field Name and Code
Spring Validation custom messages - field name
印证了我的一些思想,同时提到了Validator的开发,我目前没有相关的技术需求。
-
spikefin-goby/spring-boot-validation-sample
讲到了message.properties技术的应用,并提供了相关的案例代码。