英文:
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("/example")
public class ExampleController {
@GetMapping("get")
public List<WhateverObject> 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'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 "must be one of the values included in {values}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* @return {@link Class} of {@link Enum} used to check the value
*/
Class<? extends Enum> 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<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;
}
}
Now you can use it in the following example:
public enum IngredientEnum {
CHEESE,
HAM,
ONION,
PINEAPPLE,
BACON,
MOZZARELLA
}
And the controller:
@AllArgsConstructor
@RestController
@RequestMapping("/test")
@Validated
public class TestController {
@GetMapping("/testAgainstEnum")
public List<WhateverObject> 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<T> {
/**
* Get the value of an internal property included in the {@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;
}
}
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 "must be one of the values included in {values}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* @return {@link Class} of {@link Enum} used to check the value
*/
Class<? extends Enum<? extends IEnumInternalPropertyValue<String>>> 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<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)
.buildConstraintViolationWithTemplate(constraintTemplate)
.addConstraintViolation();
}
return isValid;
}
}
And the controller:
@AllArgsConstructor
@RestController
@RequestMapping("/test")
@Validated
public class TestController {
@GetMapping("/testStringInsideEnum")
public List<WhateverObject> 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>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论