ConstraintValidator中调用addMessageParameter方法

我目前的这套方案可以说是以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,然后用在我们的项目中。

参考资料

  1. Setting Custom Field Name and Code

    Spring Validation custom messages - field name

    印证了我的一些思想,同时提到了Validator的开发,我目前没有相关的技术需求。

  2. spikefin-goby/spring-boot-validation-sample

    讲到了message.properties技术的应用,并提供了相关的案例代码。