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

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

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

问题

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

我有这个:

  1. @RequestMapping("/example")
  2. public class ExampleController {
  3. @GetMapping("get")
  4. public List<WhateverObject> getWhateverObjects(@RequestParam String objectName) {
  5. /* 代码 */
  6. }
  7. }

我们正在使用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:

  1. @RequestMapping(&quot;/example&quot;)
  2. public class ExampleController {
  3. @GetMapping(&quot;get&quot;)
  4. public List&lt;WhateverObject&gt; getWhateverObjects(@RequestParam String objectName) {
  5. /* Code */
  6. }
  7. }

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

  1. 你可以创建自己的`ConstraintValidator`,然而你并没有说明你是否需要将你的值与`Enum`的值或其内部属性进行比较。我将在下面的几个部分分别提供这两种情况的示例。
  2. ----------
  3. ## 与枚举值比较 ##
  4. 正如 **greenPadawan** 提到的,你可以通过你的`Enum`来改变参数的类型,如果你能够/只需要这样做,那是最好的选择。
  5. 下面的示例将解释如何自定义在这种情况下使用的约束,如果你想保持`String`(甚至在更新它以包含更多/其他检查时),第一步是创建你将用于检查约束的注解:
  6. /**
  7. * 被注解的元素必须包含在给定的{@link Enum}的接受值中。
  8. */
  9. @Documented
  10. @Retention(RUNTIME)
  11. @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
  12. @Constraint(validatedBy = EnumHasValueValidator.class)
  13. public @interface EnumHasValue {
  14. String message() default "必须是以下值之一:{values}";
  15. Class<?>[] groups() default {};
  16. Class<? extends Payload>[] payload() default {};
  17. /**
  18. * @return 用于检查值的{@link Enum}的{@link Class}
  19. */
  20. Class<? extends Enum> enumClass();
  21. /**
  22. * @return 如果接受{@code null}作为有效值则返回{@code true},否则返回{@code false}。
  23. */
  24. boolean isNullAccepted() default false;
  25. }
  26. 第二步是创建验证器本身:
  27. /**
  28. * 验证给定的{@link String}是否与所提供的{@link Enum}的值匹配
  29. */
  30. public class EnumHasValueValidator implements ConstraintValidator<EnumHasValue, String> {
  31. private static final String ERROR_MESSAGE_PARAMETER = "values";
  32. List<String> enumValidValues;
  33. String constraintTemplate;
  34. private boolean isNullAccepted;
  35. @Override
  36. public void initialize(final EnumHasValue hasValue) {
  37. enumValidValues = Arrays.stream(hasValue.enumClass().getEnumConstants())
  38. .map(Enum::name)
  39. .collect(Collectors.toList());
  40. constraintTemplate = hasValue.message();
  41. isNullAccepted = hasValue.isNullAccepted();
  42. }
  43. @Override
  44. public boolean isValid(String value, ConstraintValidatorContext context) {
  45. boolean isValid = null == value ? isNullAccepted
  46. : enumValidValues.contains(value);
  47. if (!isValid) {
  48. HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
  49. hibernateContext.disableDefaultConstraintViolation();
  50. hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
  51. .buildConstraintViolationWithTemplate(constraintTemplate)
  52. .addConstraintViolation();
  53. }
  54. return isValid;
  55. }
  56. }
  57. 现在你可以在以下示例中使用它:
  58. public enum IngredientEnum {
  59. CHEESE,
  60. HAM,
  61. ONION,
  62. PINEAPPLE,
  63. BACON,
  64. MOZZARELLA
  65. }
  66. 以及控制器:
  67. @AllArgsConstructor
  68. @RestController
  69. @RequestMapping("/test")
  70. @Validated
  71. public class TestController {
  72. @GetMapping("/testAgainstEnum")
  73. public List<WhateverObject> testAgainstEnum(@RequestParam @EnumHasValue(enumClass=IngredientEnum.class) String objectName) {
  74. ...
  75. }
  76. }
  77. 你可以在下面的图片中看到一个示例:
  78. [![EnumHasValue示例][1]][1]
  79. (*如你所见,在这种情况下,大小写是被考虑在内的,如果你愿意,你可以在验证器中进行更改*)
  80. ----------
  81. ## 与枚举内部属性比较 ##
  82. 在这种情况下,第一步是定义一种提取这种内部属性的方法:
  83. /**
  84. * 用于获取{@link Enum}中的内部属性的值。
  85. */
  86. public interface IEnumInternalPropertyValue<T> {
  87. /**
  88. * 获取包含在{@link Enum}中的内部属性的值。
  89. */
  90. T getInternalPropertyValue();
  91. }
  92. public enum PizzaEnum implements IEnumInternalPropertyValue<String> {
  93. MARGUERITA("Margherita"),
  94. CARBONARA("Carbonara");
  95. private String internalValue;
  96. PizzaEnum(String internalValue) {
  97. this.internalValue = internalValue;
  98. }
  99. @Override
  100. public String getInternalPropertyValue() {
  101. return this.internalValue;
  102. }
  103. }
  104. 所需的注解和相关的验证器与之前的类似:
  105. /**
  106. * 被注解的元素必须包含在给定的接受{@link Class}的{@link Enum}的内部{@link String}属性中。
  107. */
  108. @Documented
  109. @Retention(RUNTIME)
  110. @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
  111. @Constraint(validatedBy = EnumHasInternalStringValueValidator.class)
  112. public @interface EnumHasInternalStringValue {
  113. String message() default "必须是以下值之一:{values}";
  114. Class<?>[] groups() default {};
  115. Class<? extends Payload>[] payload() default {};
  116. /**
  117. * @return 用于检查值的{@link Enum}的{@link Class}
  118. */
  119. Class<? extends Enum<? extends IEnumInternalPropertyValue<String>>> enumClass();
  120. /**
  121. * @return 如果接受{@code null}作为有效值则返回{@code true},否则返回{@code false}。
  122. */
  123. boolean isNullAccepted() default false;
  124. }
  125. 验证器:
  126. /**
  127. * 验证给定的{@link String}是否与所提供的{@link Enum}的内部{@link String}属性之一匹配
  128. */
  129. public class EnumHasInternalStringValueValidator implements ConstraintValidator<EnumHasInternalStringValue, String> {
  130. private static final String ERROR_MESSAGE_PARAMETER = "values";
  131. List<String> enumValidValues;
  132. String constraintTemplate;
  133. private boolean isNullAccepted;
  134. @Override
  135. public void initialize(final EnumHasInternalStringValue hasInternalStringValue) {
  136. enumValidValues = Arrays.stream(hasInternalStringValue.enumClass().getEnumConstants())
  137. .map(e -> ((IEnumInternalPropertyValue<String>)e).getInternalPropertyValue())
  138. .collect(Collectors.toList());
  139. constraintTemplate = hasInternalStringValue.message();
  140. isNullAccepted = hasInternalStringValue.isNullAccepted();
  141. }
  142. @Override
  143. public boolean isValid(String value, ConstraintValidatorContext context) {
  144. boolean isValid = null == value ? isNullAccepted
  145. : enumValidValues.contains(value);
  146. if (!isValid) {
  147. HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
  148. hibernateContext.disableDefaultConstraintViolation();
  149. hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
  150. <details>
  151. <summary>英文:</summary>
  152. 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.
  153. ----------
  154. ## Compare against enum values ##
  155. As **greenPadawan** mentioned, you can change the type of parameter by your `Enum`, if you can/only need it, that is the best option.
  156. 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:
  157. /**
  158. * The annotated element must be included in value of the given accepted {@link Class} of {@link Enum}.
  159. */
  160. @Documented
  161. @Retention(RUNTIME)
  162. @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
  163. @Constraint(validatedBy = EnumHasValueValidator.class)
  164. public @interface EnumHasValue {
  165. String message() default &quot;must be one of the values included in {values}&quot;;
  166. Class&lt;?&gt;[] groups() default {};
  167. Class&lt;? extends Payload&gt;[] payload() default {};
  168. /**
  169. * @return {@link Class} of {@link Enum} used to check the value
  170. */
  171. Class&lt;? extends Enum&gt; enumClass();
  172. /**
  173. * @return {@code true} if {@code null} is accepted as a valid value, {@code false} otherwise.
  174. */
  175. boolean isNullAccepted() default false;
  176. }
  177. The second is create your validator itself:
  178. /**
  179. * Validates if the given {@link String} matches with one of the values belonging to the
  180. * provided {@link Class} of {@link Enum}
  181. */
  182. public class EnumHasValueValidator implements ConstraintValidator&lt;EnumHasValue, String&gt; {
  183. private static final String ERROR_MESSAGE_PARAMETER = &quot;values&quot;;
  184. List&lt;String&gt; enumValidValues;
  185. String constraintTemplate;
  186. private boolean isNullAccepted;
  187. @Override
  188. public void initialize(final EnumHasValue hasValue) {
  189. enumValidValues = Arrays.stream(hasValue.enumClass().getEnumConstants())
  190. .map(Enum::name)
  191. .collect(Collectors.toList());
  192. constraintTemplate = hasValue.message();
  193. isNullAccepted = hasValue.isNullAccepted();
  194. }
  195. @Override
  196. public boolean isValid(String value, ConstraintValidatorContext context) {
  197. boolean isValid = null == value ? isNullAccepted
  198. : enumValidValues.contains(value);
  199. if (!isValid) {
  200. HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
  201. hibernateContext.disableDefaultConstraintViolation();
  202. hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
  203. .buildConstraintViolationWithTemplate(constraintTemplate)
  204. .addConstraintViolation();
  205. }
  206. return isValid;
  207. }
  208. }
  209. Now you can use it in the following example:
  210. public enum IngredientEnum {
  211. CHEESE,
  212. HAM,
  213. ONION,
  214. PINEAPPLE,
  215. BACON,
  216. MOZZARELLA
  217. }
  218. And the controller:
  219. @AllArgsConstructor
  220. @RestController
  221. @RequestMapping(&quot;/test&quot;)
  222. @Validated
  223. public class TestController {
  224. @GetMapping(&quot;/testAgainstEnum&quot;)
  225. public List&lt;WhateverObject&gt; testAgainstEnum(@RequestParam @EnumHasValue(enumClass=IngredientEnum.class) String objectName) {
  226. ...
  227. }
  228. }
  229. You can see an example in the following picture:
  230. [![EnumHasValue example][1]][1]
  231. (*As you can see, in this case, lower/upper case are taking into account, you can change it in the validator if you want*)
  232. ----------
  233. ## Compare against internal enum property ##
  234. In this case, the first step is define a way to extract such internal property:
  235. /**
  236. * Used to get the value of an internal property in an {@link Enum}.
  237. */
  238. public interface IEnumInternalPropertyValue&lt;T&gt; {
  239. /**
  240. * Get the value of an internal property included in the {@link Enum}.
  241. */
  242. T getInternalPropertyValue();
  243. }
  244. public enum PizzaEnum implements IEnumInternalPropertyValue&lt;String&gt; {
  245. MARGUERITA(&quot;Margherita&quot;),
  246. CARBONARA(&quot;Carbonara&quot;);
  247. private String internalValue;
  248. PizzaEnum(String internalValue) {
  249. this.internalValue = internalValue;
  250. }
  251. @Override
  252. public String getInternalPropertyValue() {
  253. return this.internalValue;
  254. }
  255. }
  256. The required annotation and related validator are quite similar to the previous ones:
  257. /**
  258. * The annotated element must be included in an internal {@link String} property of the given accepted
  259. * {@link Class} of {@link Enum}.
  260. */
  261. @Documented
  262. @Retention(RUNTIME)
  263. @Target({FIELD, ANNOTATION_TYPE, PARAMETER})
  264. @Constraint(validatedBy = EnumHasInternalStringValueValidator.class)
  265. public @interface EnumHasInternalStringValue {
  266. String message() default &quot;must be one of the values included in {values}&quot;;
  267. Class&lt;?&gt;[] groups() default {};
  268. Class&lt;? extends Payload&gt;[] payload() default {};
  269. /**
  270. * @return {@link Class} of {@link Enum} used to check the value
  271. */
  272. Class&lt;? extends Enum&lt;? extends IEnumInternalPropertyValue&lt;String&gt;&gt;&gt; enumClass();
  273. /**
  274. * @return {@code true} if {@code null} is accepted as a valid value, {@code false} otherwise.
  275. */
  276. boolean isNullAccepted() default false;
  277. }
  278. Validator:
  279. /**
  280. * Validates if the given {@link String} matches with one of the internal {@link String} property belonging to the
  281. * provided {@link Class} of {@link Enum}
  282. */
  283. public class EnumHasInternalStringValueValidator implements ConstraintValidator&lt;EnumHasInternalStringValue, String&gt; {
  284. private static final String ERROR_MESSAGE_PARAMETER = &quot;values&quot;;
  285. List&lt;String&gt; enumValidValues;
  286. String constraintTemplate;
  287. private boolean isNullAccepted;
  288. @Override
  289. public void initialize(final EnumHasInternalStringValue hasInternalStringValue) {
  290. enumValidValues = Arrays.stream(hasInternalStringValue.enumClass().getEnumConstants())
  291. .map(e -&gt; ((IEnumInternalPropertyValue&lt;String&gt;)e).getInternalPropertyValue())
  292. .collect(Collectors.toList());
  293. constraintTemplate = hasInternalStringValue.message();
  294. isNullAccepted = hasInternalStringValue.isNullAccepted();
  295. }
  296. @Override
  297. public boolean isValid(String value, ConstraintValidatorContext context) {
  298. boolean isValid = null == value ? isNullAccepted
  299. : enumValidValues.contains(value);
  300. if (!isValid) {
  301. HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
  302. hibernateContext.disableDefaultConstraintViolation();
  303. hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
  304. .buildConstraintViolationWithTemplate(constraintTemplate)
  305. .addConstraintViolation();
  306. }
  307. return isValid;
  308. }
  309. }
  310. And the controller:
  311. @AllArgsConstructor
  312. @RestController
  313. @RequestMapping(&quot;/test&quot;)
  314. @Validated
  315. public class TestController {
  316. @GetMapping(&quot;/testStringInsideEnum&quot;)
  317. public List&lt;WhateverObject&gt; testStringInsideEnum(@RequestParam @EnumHasInternalStringValue(enumClass=PizzaEnum.class) String objectName) {
  318. ...
  319. }
  320. }
  321. You can see an example in the following picture:
  322. [![EnumHasValue example][2]][2]
  323. The source code of the last annotation and validator can be found [here][3]
  324. [1]: https://i.stack.imgur.com/S3s3U.png
  325. [2]: https://i.stack.imgur.com/YR9rv.png
  326. [3]: https://github.com/doctore/Spring5Microservices/tree/master/common/src/main/java/com/spring5microservices/common/validator
  327. </details>
  328. # 答案2
  329. **得分**: 2
  330. 你可以将枚举用作参数的类型,而不是使用 `String`
  331. <details>
  332. <summary>英文:</summary>
  333. You can use your enum as the type of your parameter instead of `String`
  334. </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:

确定