bean 验证不适用于 kotlin (JSR 380)
bean validation not working with kotlin (JSR 380)
所以首先我想不出这个问题的更好标题,所以我愿意接受更改。
我正在尝试使用带有 spring 引导的 bean 验证机制 (JSR-380) 来验证 bean。
所以我得到了这样一个控制器:
@Controller
@RequestMapping("/users")
class UserController {
@PostMapping
fun createUser(@Valid user: User, bindingResult: BindingResult): ModelAndView {
return ModelAndView("someview", "user", user)
}
}
这是用 kotlin 编写的用户 class:
data class User(
@field:NotEmpty
var roles: MutableSet<@NotNull Role> = HashSet()
)
这是测试:
@Test
internal fun shouldNotCreateNewTestWithInvalidParams() {
mockMvc.perform(post("/users")
.param("roles", "invalid role"))
.andExpect(model().attributeHasFieldErrors("user", "roles[]"))
}
无效角色映射为空。
如您所见,我希望 roles
包含至少一项,其中 none 项为空。
但是,在测试上述代码时,如果 roles
包含空值,则不会报告绑定错误。如果集合为空,它确实会报告错误。
我在想这可能是 kotlin 代码如何编译的一个问题,因为当用户 class 写在 java 中时,相同的代码工作得很好。像这样:
@Data // just lombok...
public class User {
@NotEmpty
private Set<@NotNull Role> roles = new HashSet<>();
}
相同的控制器,相同的测试。
检查字节码后,我注意到 kotlin 版本不包括嵌套的 @NotNull
注释(见下文)。
Java:
private Ljava/util/Set; roles
@Ljavax/validation/constraints/NotEmpty;()
@Ljavax/validation/constraints/NotNull;() : FIELD, 0;
@Ljavax/validation/constraints/NotEmpty;() : FIELD, null
科特林:
private Ljava/util/Set; roles
@Ljavax/validation/constraints/NotEmpty;()
@Lorg/jetbrains/annotations/NotNull;() // added because roles is not nullable in kotlin. this does not affect validation
现在的问题是为什么?
这里有一个 sample project 如果您想尝试一些东西。
尝试像这样添加 ?
:
data class User(
@field:Valid
@field:NotEmpty
var roles: MutableSet<@NotNull Role?> = HashSet()
)
然后 kotlin 编译器应该意识到角色可以是 null
,它可能会接受验证,我对 JSR380 知之甚少所以我只是猜测。
答案(Kotlin 1.3.70)
确保使用 jvm 目标 1.8 或更高版本编译 kotlin 代码,并通过在编译时提供 -Xemit-jvm-type-annotations
来启用此功能。
对于 Spring 引导项目,您只需进行以下更改(使用 Spring Boot 2.3.3 和 Kotlin 1.4.0 进行测试):
- 在您的 pom 中设置以下 属性:
<properties>
<java.version>11</java.version>
<kotlin.version>1.4.0</kotlin.version>
</properties>
- 将
<arg>-Xemit-jvm-type-annotations</arg>
添加到kotlin-maven-plugin
:
<build>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
<arg>-Xemit-jvm-type-annotations</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</build>
解决方法(Kotlin 1.3.70 之前)
Rafal G. 已经指出我们可以使用自定义验证器作为解决方法。所以这里有一些代码:
注释:
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.CONSTRUCTOR
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.TYPE_PARAMETER
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
import kotlin.reflect.KClass
@MustBeDocumented
@Constraint(validatedBy = [NoNullElementsValidator::class])
@Target(allowedTargets = [FUNCTION, FIELD, ANNOTATION_CLASS, CONSTRUCTOR, VALUE_PARAMETER, TYPE_PARAMETER])
@Retention(AnnotationRetention.RUNTIME)
annotation class NoNullElements(
val message: String = "must not contain null elements",
val groups: Array<KClass<out Any>> = [],
val payload: Array<KClass<out Payload>> = []
)
约束验证器:
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
class NoNullElementsValidator : ConstraintValidator<NoNullElements, Collection<Any>> {
override fun isValid(value: Collection<Any>?, context: ConstraintValidatorContext): Boolean {
// null values are valid
if (value == null) {
return true
}
return value.stream().noneMatch { it == null }
}
}
最后更新的用户 class:
data class User(
@field:NotEmpty
@field:NoNullElements
var roles: MutableSet<Role> = HashSet()
)
虽然现在验证有效,但生成的 ConstrainViolation 略有不同。例如 elementType
和 propertyPath
不同,如下所示。
Java:
科特林:
来源可在此处获得:https://github.com/DarkAtra/jsr380-kotlin-issue/tree/workaround
再次感谢您的帮助Rafal G.
所以首先我想不出这个问题的更好标题,所以我愿意接受更改。
我正在尝试使用带有 spring 引导的 bean 验证机制 (JSR-380) 来验证 bean。
所以我得到了这样一个控制器:
@Controller
@RequestMapping("/users")
class UserController {
@PostMapping
fun createUser(@Valid user: User, bindingResult: BindingResult): ModelAndView {
return ModelAndView("someview", "user", user)
}
}
这是用 kotlin 编写的用户 class:
data class User(
@field:NotEmpty
var roles: MutableSet<@NotNull Role> = HashSet()
)
这是测试:
@Test
internal fun shouldNotCreateNewTestWithInvalidParams() {
mockMvc.perform(post("/users")
.param("roles", "invalid role"))
.andExpect(model().attributeHasFieldErrors("user", "roles[]"))
}
无效角色映射为空。
如您所见,我希望 roles
包含至少一项,其中 none 项为空。
但是,在测试上述代码时,如果 roles
包含空值,则不会报告绑定错误。如果集合为空,它确实会报告错误。
我在想这可能是 kotlin 代码如何编译的一个问题,因为当用户 class 写在 java 中时,相同的代码工作得很好。像这样:
@Data // just lombok...
public class User {
@NotEmpty
private Set<@NotNull Role> roles = new HashSet<>();
}
相同的控制器,相同的测试。
检查字节码后,我注意到 kotlin 版本不包括嵌套的 @NotNull
注释(见下文)。
Java:
private Ljava/util/Set; roles
@Ljavax/validation/constraints/NotEmpty;()
@Ljavax/validation/constraints/NotNull;() : FIELD, 0;
@Ljavax/validation/constraints/NotEmpty;() : FIELD, null
科特林:
private Ljava/util/Set; roles
@Ljavax/validation/constraints/NotEmpty;()
@Lorg/jetbrains/annotations/NotNull;() // added because roles is not nullable in kotlin. this does not affect validation
现在的问题是为什么?
这里有一个 sample project 如果您想尝试一些东西。
尝试像这样添加 ?
:
data class User(
@field:Valid
@field:NotEmpty
var roles: MutableSet<@NotNull Role?> = HashSet()
)
然后 kotlin 编译器应该意识到角色可以是 null
,它可能会接受验证,我对 JSR380 知之甚少所以我只是猜测。
答案(Kotlin 1.3.70)
确保使用 jvm 目标 1.8 或更高版本编译 kotlin 代码,并通过在编译时提供 -Xemit-jvm-type-annotations
来启用此功能。
对于 Spring 引导项目,您只需进行以下更改(使用 Spring Boot 2.3.3 和 Kotlin 1.4.0 进行测试):
- 在您的 pom 中设置以下 属性:
<properties> <java.version>11</java.version> <kotlin.version>1.4.0</kotlin.version> </properties>
- 将
<arg>-Xemit-jvm-type-annotations</arg>
添加到kotlin-maven-plugin
:<build> <plugin> <artifactId>kotlin-maven-plugin</artifactId> <groupId>org.jetbrains.kotlin</groupId> <configuration> <args> <arg>-Xjsr305=strict</arg> <arg>-Xemit-jvm-type-annotations</arg> </args> <compilerPlugins> <plugin>spring</plugin> </compilerPlugins> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>${kotlin.version}</version> </dependency> </dependencies> </plugin> </build>
解决方法(Kotlin 1.3.70 之前)
Rafal G. 已经指出我们可以使用自定义验证器作为解决方法。所以这里有一些代码:
注释:
import javax.validation.Constraint
import javax.validation.Payload
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.CONSTRUCTOR
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.TYPE_PARAMETER
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
import kotlin.reflect.KClass
@MustBeDocumented
@Constraint(validatedBy = [NoNullElementsValidator::class])
@Target(allowedTargets = [FUNCTION, FIELD, ANNOTATION_CLASS, CONSTRUCTOR, VALUE_PARAMETER, TYPE_PARAMETER])
@Retention(AnnotationRetention.RUNTIME)
annotation class NoNullElements(
val message: String = "must not contain null elements",
val groups: Array<KClass<out Any>> = [],
val payload: Array<KClass<out Payload>> = []
)
约束验证器:
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
class NoNullElementsValidator : ConstraintValidator<NoNullElements, Collection<Any>> {
override fun isValid(value: Collection<Any>?, context: ConstraintValidatorContext): Boolean {
// null values are valid
if (value == null) {
return true
}
return value.stream().noneMatch { it == null }
}
}
最后更新的用户 class:
data class User(
@field:NotEmpty
@field:NoNullElements
var roles: MutableSet<Role> = HashSet()
)
虽然现在验证有效,但生成的 ConstrainViolation 略有不同。例如 elementType
和 propertyPath
不同,如下所示。
Java:
科特林:
来源可在此处获得:https://github.com/DarkAtra/jsr380-kotlin-issue/tree/workaround
再次感谢您的帮助Rafal G.