【问题标题】:How to get query parameter name from ConstraintViolationException如何从 ConstraintViolationException 中获取查询参数名称
【发布时间】:2019-06-05 20:50:27
【问题描述】:

我有一个服务方法:

 @GetMapping(path = "/api/some/path", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getWhatever(@RequestParam(value = "page-number", defaultValue = "0") @Min(0) Integer pageNumber, ...

如果 API 的调用者没有为 page-number 查询参数提交正确的值,则会引发 javax.ConstraintViolationexception。异常的消息会像这样:

getWhatever.pageNumber must be equal or greater than 0

在响应正文中,我希望有此消息:

page-number must be equal or greater than 0

我希望我的消息具有查询参数的名称,而不是参数的名称。恕我直言,包括参数的名称都暴露了实现细节。

问题是,我找不到带有查询参数名称的对象。好像ConstraintViolationException 没有。

我在 spring-boot 中运行我的应用程序。

任何帮助将不胜感激。

P.S.:我去过其他声称可以解决问题的类似线程,但实际上它们都没有。

【问题讨论】:

    标签: java spring-boot hibernate-validator javax.validation


    【解决方案1】:

    这是我在 spring-boot 2.0.3 中的工作方式:

    我必须在 spring-boot 中覆盖和禁用 ValidationAutoConfiguration

    import org.springframework.boot.validation.MessageInterpolatorFactory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.core.env.Environment;
    import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
    import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
    
    import javax.validation.Validator;
    
    @Configuration
    public class ValidationConfiguration {
        public ValidationConfiguration() {
        }
    
        @Bean
        public static LocalValidatorFactoryBean validator() {
            LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
            factoryBean.setParameterNameDiscoverer(new CustomParamNamesDiscoverer());
            MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
            factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
            return factoryBean;
        }
    
        @Bean
        public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator) {
            MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
            boolean proxyTargetClass = (Boolean) environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
            processor.setProxyTargetClass(proxyTargetClass);
            processor.setValidator(validator);
            return processor;
        }
    }
    

    CustomParamNamesDiscoverer 位于同一个包中,它几乎是 DefaultParameterNameDiscoverer 的复制粘贴,spring-boot 的参数名称发现器的默认实现:

    import org.springframework.core.*;
    import org.springframework.util.ClassUtils;
    
    public class CustomParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
        private static final boolean kotlinPresent = ClassUtils.isPresent("kotlin.Unit", CustomParameterNameDiscoverer.class.getClassLoader());
    
        public CustomParameterNameDiscoverer() {
            if (kotlinPresent) {
                this.addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
            }
    
            this.addDiscoverer(new ReqParamNamesDiscoverer());
            this.addDiscoverer(new StandardReflectionParameterNameDiscoverer());
            this.addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
        }
    }
    

    我希望它保持几乎完整(你甚至可以看到那里的 kotlin 检查),唯一的补充是: 我正在将ReqParamNamesDiscoverer 的实例添加到发现者的链接列表中。请注意,添加顺序在这里很重要。

    这里是源代码:

    import com.google.common.base.Strings;
    import org.springframework.core.ParameterNameDiscoverer;
    import org.springframework.lang.Nullable;
    import org.springframework.web.bind.annotation.RequestParam;
    
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Executable;
    import java.lang.reflect.Method;
    import java.lang.reflect.Parameter;
    
    public class ReqParamNamesDiscoverer implements ParameterNameDiscoverer {
    
        public ReqParamNamesDiscoverer() {
        }
    
        @Override
        @Nullable
        public String[] getParameterNames(Method method) {
            return doGetParameterNames(method);
        }
    
        @Override
        @Nullable
        public String[] getParameterNames(Constructor<?> constructor) {
            return doGetParameterNames(constructor);
        }
    
        @Nullable
        private static String[] doGetParameterNames(Executable executable) {
            Parameter[] parameters = executable.getParameters();
            String[] parameterNames = new String[parameters.length];
            for (int i = 0; i < parameters.length; ++i) {
                Parameter param = parameters[i];
                if (!param.isNamePresent()) {
                    return null;
                }
                String paramName = param.getName();
                if (param.isAnnotationPresent(RequestParam.class)) {
                    RequestParam requestParamAnnotation = param.getAnnotation(RequestParam.class);
                    if (!Strings.isNullOrEmpty(requestParamAnnotation.value())) {
                        paramName = requestParamAnnotation.value();
                    }
                }
                parameterNames[i] = paramName;
            }
            return parameterNames;
        }
    }
    

    如果参数带有RequestParam 注释,我将检索value 属性并将其作为参数名称返回。

    接下来的事情是禁用自动验证配置,不知何故,没有它就无法工作。这个注释虽然可以解决问题: @SpringBootApplication(exclude = {ValidationAutoConfiguration.class})

    此外,您需要为您的 ConstraintValidationException 提供一个自定义处理程序:

        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(ConstraintViolationException.class)
        public ErrorDTO handleConstraintViolationException(ConstraintViolationException ex) {
            Map<String, Collection<String>> errors = new LinkedHashMap<>();
            ex.getConstraintViolations().forEach(constraintViolation -> {
                String queryParamPath = constraintViolation.getPropertyPath().toString();
                log.debug("queryParamPath = {}", queryParamPath);
                String queryParam = queryParamPath.contains(".") ?
                        queryParamPath.substring(queryParamPath.indexOf(".") + 1) :
                        queryParamPath;
                String errorMessage = constraintViolation.getMessage();
                Collection<String> perQueryParamErrors = errors.getOrDefault(queryParam, new ArrayList<>());
                perQueryParamErrors.add(errorMessage);
                errors.put(queryParam, perQueryParamErrors);
            });
            return validationException(new ValidationException("queryParameter", errors));
        }
    

    【讨论】:

    • 感谢 Guillaume Smet 和 @Gunnar 的帮助,为我指引正确的方向。
    • "queryParam" 不适用于嵌套对象。我建议用正则表达式替换它。 String queryParam = queryParamPath.replaceAll(".*\\.", "");
    【解决方案2】:

    像这样向@Min注解添加自定义消息

    @Min(value=0, message="page-number must be equal or greater than {value}")
    

    【讨论】:

    • 是的,但我认为这正是他想要避免的。
    • 是的,这是一个可行的解决方案,但我有大约 50 个服务类,每个类都有很多限制。这会使代码库混乱,并可能引入错误。我想自动化它。此外,如果有人更改了参数的名称并忘记更新消息,则会断开连接。
    【解决方案3】:

    现在,您不能这样做(好吧,除非您为每个注释定义自定义消息,但我想这不是您想要的)。

    有趣的是,最近有人在做非常相似的事情:https://github.com/hibernate/hibernate-validator/pull/1029

    这项工作已合并到 master 分支,但我尚未发布包含此工作的新 6.1 alpha。这只是几天的事情。

    话虽如此,我们已经想到了属性,现在您提出这个问题,我们可能应该将其推广到更多的东西,包括方法参数。

    现在我们有了大致的想法,我认为概括它不应该做太多工作。

    我会与贡献者和团队其他成员讨论这个问题,然后回复你。

    【讨论】:

    • 感谢您的意见!
    • 已经有ParameterNameProvider 合约,可以实现从@RequestParam 注释中获取名称。不过,消息中仍然会有方法名称。
    • 能否请您指导一下或给我发送一个示例实现?
    • 消息中的方法名很好,我可以通过检查字符串轻松删除它。
    • 请参阅the spec 了解实际合同和注册实施的方式。简而言之,您将使用反射从已验证方法的参数中获取 @RequestParam 注释并返回名称列表。如果注释不存在,您可能会退回到默认行为(实际参数名称)。
    【解决方案4】:

    我认为获取查询参数的名称是不可能的,但如果有人知道方法,我希望被证明是错误的。

    正如 Dmitry Bogdanovich 所说,拥有自定义消息是我知道如何做一些接近您需要的事情的最简单也是唯一的方法。如果您说您不想让这些消息使您的代码混乱,您可以这样做:

    在您的资源文件夹中添加一个 ValidationMessages.properties 文件。在这里你可以说:

    page_number.min=page-number must be equal or greater than {value}
    

    现在你可以使用注解来写了:

    @Min(value = 0, message = "{page_number.min}")
    

    这样您就可以在需要时通过单一来源更改有关消息的任何内容。

    【讨论】:

    • 阅读其他答案的cmets
    • 我确实阅读了这些 cmets,我只是为您提供了一种简单的方法,可以将所有这些消息放在一个地方,如果您的 RequestParameter 更改,您可以在其中更改参数名称。据我了解,目前没有办法按照您的方式进行操作,我认为这可能是一种解决方法。
    • 我认为有办法,一旦我证明它有效,我会在这里发布我的解决方案
    猜你喜欢
    • 2023-03-27
    • 2013-06-13
    • 2018-04-18
    • 1970-01-01
    • 2020-02-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-19
    相关资源
    最近更新 更多