SpringBoot:在REST端点中为@RequestParam参数自定义验证

huangapple go评论78阅读模式
英文:

SpringBoot: Custom validation for a @RequestParam parameter in a REST endpint

问题

问题很简单,但我还没有找到解决方法:

我有这个:

@RequestMapping("/example")
public class ExampleController {

    @GetMapping("get")
    public List<WhateverObject> getWhateverObjects(@RequestParam String objectName) {
        
        /* 代码 */
    }

}

我们正在使用Spring Boot,我想要验证"objectName"是否与一组预定义的值匹配(这组值在枚举类型中,但该部分容易更改,所以如果需要手动写下这些值,我也不介意)。关于验证@RequestParam对象,我所看到的只涵盖了基本的内容(@Min(value)@NotNull等等)。

我知道可以为bean使用CustomValidators,但它不适用于我当前的问题(而且我不能更改参数的类型)。Spring是否有针对此自定义验证的特定功能,还是我需要在/* 代码 */部分直接进行验证?

英文:

Question is easy, but I haven't found a solution for this:

I got this:

@RequestMapping(&quot;/example&quot;)
public class ExampleController {

	@GetMapping(&quot;get&quot;)
    public List&lt;WhateverObject&gt; getWhateverObjects(@RequestParam String objectName) {
		
		/* Code */
    }

}

We are using SpringBoot, and I'm looking to validate "objectName" against a defined list of values (This list is in an enum type, but that part is prone to change, so I wont mind if I need to write the values down by hand). All I've seen regarding validation of @RequestParam objects covers just basic stuff (@Min(value), @NotNull and all that.

I know about CustomValidators for beans, but it does not applies to my current problematic (And I can't change the type of parameter). Does Spring has something specific for this custom validation or do I need to make the validation "directly" in the /* Code */ section?

答案1

得分: 4

你可以创建自己的`ConstraintValidator`,然而你并没有说明你是否需要将你的值与`Enum`的值或其内部属性进行比较。我将在下面的几个部分分别提供这两种情况的示例。

----------

## 与枚举值比较 ##

正如 **greenPadawan** 提到的,你可以通过你的`Enum`来改变参数的类型,如果你能够/只需要这样做,那是最好的选择。

下面的示例将解释如何自定义在这种情况下使用的约束,如果你想保持`String`(甚至在更新它以包含更多/其他检查时),第一步是创建你将用于检查约束的注解:

    /**
     * 被注解的元素必须包含在给定的{@link Enum}的接受值中。
     */
    @Documented
    @Retention(RUNTIME)
    @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
    @Constraint(validatedBy = EnumHasValueValidator.class)
    public @interface EnumHasValue {

      String message() default "必须是以下值之一:{values}";

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

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

      /**
       * @return 用于检查值的{@link Enum}的{@link Class}
       */
      Class<? extends Enum> enumClass();

      /**
       * @return 如果接受{@code null}作为有效值则返回{@code true},否则返回{@code false}。
       */
      boolean isNullAccepted() default false;

    }

第二步是创建验证器本身:

    /**
     * 验证给定的{@link String}是否与所提供的{@link Enum}的值匹配
     */
    public class EnumHasValueValidator implements ConstraintValidator<EnumHasValue, String> {

      private static final String ERROR_MESSAGE_PARAMETER = "values";

      List<String> enumValidValues;
      String constraintTemplate;
      private boolean isNullAccepted;

      @Override
      public void initialize(final EnumHasValue hasValue) {
        enumValidValues = Arrays.stream(hasValue.enumClass().getEnumConstants())
                                .map(Enum::name)
                                .collect(Collectors.toList());
        constraintTemplate = hasValue.message();
        isNullAccepted = hasValue.isNullAccepted();
      }

      @Override
      public boolean isValid(String value, ConstraintValidatorContext context) {
        boolean isValid = null == value ? isNullAccepted
                                        : enumValidValues.contains(value);
        if (!isValid) {
          HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
          hibernateContext.disableDefaultConstraintViolation();
          hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
                          .buildConstraintViolationWithTemplate(constraintTemplate)
                          .addConstraintViolation();
        }
        return isValid;
      }
    }

现在你可以在以下示例中使用它:

    public enum IngredientEnum {
      CHEESE,
      HAM,
      ONION,
      PINEAPPLE,
      BACON,
      MOZZARELLA
    }

以及控制器:

    @AllArgsConstructor
    @RestController
    @RequestMapping("/test")
    @Validated
    public class TestController {

      @GetMapping("/testAgainstEnum")
      public List<WhateverObject> testAgainstEnum(@RequestParam @EnumHasValue(enumClass=IngredientEnum.class) String objectName) {
        ...
      }
    }

你可以在下面的图片中看到一个示例:

[![EnumHasValue示例][1]][1]

(*如你所见,在这种情况下,大小写是被考虑在内的,如果你愿意,你可以在验证器中进行更改*)


----------

## 与枚举内部属性比较 ##

在这种情况下,第一步是定义一种提取这种内部属性的方法:

    /**
     * 用于获取{@link Enum}中的内部属性的值。
     */
    public interface IEnumInternalPropertyValue<T> {

      /**
       * 获取包含在{@link Enum}中的内部属性的值。
       */
      T getInternalPropertyValue();
    }

    public enum PizzaEnum implements IEnumInternalPropertyValue<String> {
      MARGUERITA("Margherita"),
      CARBONARA("Carbonara");

      private String internalValue;

      PizzaEnum(String internalValue) {
        this.internalValue = internalValue;
      }

      @Override
      public String getInternalPropertyValue() {
        return this.internalValue;
      }
    }

所需的注解和相关的验证器与之前的类似:

    /**
     * 被注解的元素必须包含在给定的接受{@link Class}的{@link Enum}的内部{@link String}属性中。
     */
    @Documented
    @Retention(RUNTIME)
    @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
    @Constraint(validatedBy = EnumHasInternalStringValueValidator.class)
    public @interface EnumHasInternalStringValue {

      String message() default "必须是以下值之一:{values}";

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

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

      /**
       * @return 用于检查值的{@link Enum}的{@link Class}
       */
      Class<? extends Enum<? extends IEnumInternalPropertyValue<String>>> enumClass();

      /**
       * @return 如果接受{@code null}作为有效值则返回{@code true},否则返回{@code false}。
       */
      boolean isNullAccepted() default false;
    }

验证器:

    /**
     * 验证给定的{@link String}是否与所提供的{@link Enum}的内部{@link String}属性之一匹配
     */
    public class EnumHasInternalStringValueValidator implements ConstraintValidator<EnumHasInternalStringValue, String> {

      private static final String ERROR_MESSAGE_PARAMETER = "values";

      List<String> enumValidValues;
      String constraintTemplate;
      private boolean isNullAccepted;

      @Override
      public void initialize(final EnumHasInternalStringValue hasInternalStringValue) {
        enumValidValues = Arrays.stream(hasInternalStringValue.enumClass().getEnumConstants())
                                .map(e -> ((IEnumInternalPropertyValue<String>)e).getInternalPropertyValue())
                                .collect(Collectors.toList());
        constraintTemplate = hasInternalStringValue.message();
        isNullAccepted = hasInternalStringValue.isNullAccepted();
      }

      @Override
      public boolean isValid(String value, ConstraintValidatorContext context) {
        boolean isValid = null == value ? isNullAccepted
                                        : enumValidValues.contains(value);
        if (!isValid) {
          HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
          hibernateContext.disableDefaultConstraintViolation();
          hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)


<details>
<summary>英文:</summary>

You can create your own `ConstraintValidator`, however you don&#39;t say if you need to compare your value against the values of an `Enum` or with an internal property inside it. I will include an example of both cases in the next sections.

----------

## Compare against enum values ##

As **greenPadawan** mentioned, you can change the type of parameter by your `Enum`, if you can/only need it, that is the best option. 

The following example explains you how to customize that use case if you want to keep the `String` (even updating it to include more/other checks if you want). The first step is create the annotation you will use to check the constraint:

    /**
     * The annotated element must be included in value of the given accepted {@link Class} of {@link Enum}.
     */
    @Documented
    @Retention(RUNTIME)
    @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
    @Constraint(validatedBy = EnumHasValueValidator.class)
    public @interface EnumHasValue {

	  String message() default &quot;must be one of the values included in {values}&quot;;

	  Class&lt;?&gt;[] groups() default {};

	  Class&lt;? extends Payload&gt;[] payload() default {};

	  /**
	   * @return {@link Class} of {@link Enum} used to check the value
	   */
	  Class&lt;? extends Enum&gt; enumClass();

	  /**
	   * @return {@code true} if {@code null} is accepted as a valid value, {@code false} otherwise.
	   */
	  boolean isNullAccepted() default false;

    }

The second is create your validator itself:

    /**
     *    Validates if the given {@link String} matches with one of the values belonging to the
     * provided {@link Class} of {@link Enum}
     */
    public class EnumHasValueValidator implements ConstraintValidator&lt;EnumHasValue, String&gt; {

	  private static final String ERROR_MESSAGE_PARAMETER = &quot;values&quot;;

	  List&lt;String&gt; enumValidValues;
	  String constraintTemplate;
	  private boolean isNullAccepted;

	  @Override
      public void initialize(final EnumHasValue hasValue) {
		enumValidValues = Arrays.stream(hasValue.enumClass().getEnumConstants())
				                .map(Enum::name)
				                .collect(Collectors.toList());
		constraintTemplate = hasValue.message();
		isNullAccepted = hasValue.isNullAccepted();
      }


	  @Override
	  public boolean isValid(String value, ConstraintValidatorContext context) {
		boolean isValid = null == value ? isNullAccepted
				                        : enumValidValues.contains(value);
		if (!isValid) {
			HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
			hibernateContext.disableDefaultConstraintViolation();
			hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
					        .buildConstraintViolationWithTemplate(constraintTemplate)
					        .addConstraintViolation();
		}
		return isValid;
	  }
    }

Now you can use it in the following example:

    public enum IngredientEnum {
      CHEESE,
      HAM,
      ONION,
      PINEAPPLE,
      BACON,
      MOZZARELLA
    }

And the controller:

    @AllArgsConstructor
    @RestController
    @RequestMapping(&quot;/test&quot;)
    @Validated
    public class TestController {

      @GetMapping(&quot;/testAgainstEnum&quot;)
      public List&lt;WhateverObject&gt; testAgainstEnum(@RequestParam @EnumHasValue(enumClass=IngredientEnum.class) String objectName) {
        ...
      }
    }

You can see an example in the following picture: 

[![EnumHasValue example][1]][1]

(*As you can see, in this case, lower/upper case are taking into account, you can change it in the validator if you want*)


----------

## Compare against internal enum property ##

In this case, the first step is define a way to extract such internal property:

    /**
     * Used to get the value of an internal property in an {@link Enum}.
     */
    public interface IEnumInternalPropertyValue&lt;T&gt; {

      /**
       * Get the value of an internal property included in the {@link Enum}.
       */
      T getInternalPropertyValue();
    }


    public enum PizzaEnum implements IEnumInternalPropertyValue&lt;String&gt; {
      MARGUERITA(&quot;Margherita&quot;),
      CARBONARA(&quot;Carbonara&quot;);

      private String internalValue;

      PizzaEnum(String internalValue) {
        this.internalValue = internalValue;
      }

      @Override
      public String getInternalPropertyValue() {
        return this.internalValue;
      }
    }

The required annotation and related validator are quite similar to the previous ones:

    /**
     *    The annotated element must be included in an internal {@link String} property of the given accepted
     * {@link Class} of {@link Enum}.
     */
    @Documented
    @Retention(RUNTIME)
    @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
    @Constraint(validatedBy = EnumHasInternalStringValueValidator.class)
    public @interface EnumHasInternalStringValue {

	  String message() default &quot;must be one of the values included in {values}&quot;;

	  Class&lt;?&gt;[] groups() default {};

	  Class&lt;? extends Payload&gt;[] payload() default {};

	  /**
	   * @return {@link Class} of {@link Enum} used to check the value
	   */
	  Class&lt;? extends Enum&lt;? extends IEnumInternalPropertyValue&lt;String&gt;&gt;&gt; enumClass();

	  /**
	   * @return {@code true} if {@code null} is accepted as a valid value, {@code false} otherwise.
	   */
	  boolean isNullAccepted() default false;
    }

Validator:

    /**
     *    Validates if the given {@link String} matches with one of the internal {@link String} property belonging to the
     * provided {@link Class} of {@link Enum}
     */
    public class EnumHasInternalStringValueValidator implements ConstraintValidator&lt;EnumHasInternalStringValue, String&gt; {

	  private static final String ERROR_MESSAGE_PARAMETER = &quot;values&quot;;

	  List&lt;String&gt; enumValidValues;
	  String constraintTemplate;
	  private boolean isNullAccepted;

	  @Override
      public void initialize(final EnumHasInternalStringValue hasInternalStringValue) {
		enumValidValues = Arrays.stream(hasInternalStringValue.enumClass().getEnumConstants())
				                .map(e -&gt; ((IEnumInternalPropertyValue&lt;String&gt;)e).getInternalPropertyValue())
				                .collect(Collectors.toList());
		constraintTemplate = hasInternalStringValue.message();
		isNullAccepted = hasInternalStringValue.isNullAccepted();
      }


	  @Override
	  public boolean isValid(String value, ConstraintValidatorContext context) {
		boolean isValid = null == value ? isNullAccepted
				                        : enumValidValues.contains(value);
		if (!isValid) {
			HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
			hibernateContext.disableDefaultConstraintViolation();
			hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
					        .buildConstraintViolationWithTemplate(constraintTemplate)
					        .addConstraintViolation();
		}
		return isValid;
	  }
    }

And the controller:

    @AllArgsConstructor
    @RestController
    @RequestMapping(&quot;/test&quot;)
    @Validated
    public class TestController {

      @GetMapping(&quot;/testStringInsideEnum&quot;)
      public List&lt;WhateverObject&gt; testStringInsideEnum(@RequestParam @EnumHasInternalStringValue(enumClass=PizzaEnum.class) String objectName) {
        ...
      }
    }

You can see an example in the following picture: 

[![EnumHasValue example][2]][2]

The source code of the last annotation and validator can be found [here][3]

  [1]: https://i.stack.imgur.com/S3s3U.png
  [2]: https://i.stack.imgur.com/YR9rv.png
  [3]: https://github.com/doctore/Spring5Microservices/tree/master/common/src/main/java/com/spring5microservices/common/validator

</details>



# 答案2
**得分**: 2

你可以将枚举用作参数的类型,而不是使用 `String`。

<details>
<summary>英文:</summary>

You can use your enum as the type of your parameter instead of `String`

</details>



huangapple
  • 本文由 发表于 2020年8月19日 20:52:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/63487389.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定