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 进行测试):

  1. 在您的 pom 中设置以下 属性:
    <properties>
        <java.version>11</java.version>
        <kotlin.version>1.4.0</kotlin.version>
    </properties>
    
  2. <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>
    

Sample Project

Jetbrains Release Notes


解决方法(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 略有不同。例如 elementTypepropertyPath 不同,如下所示。

Java:

科特林:

来源可在此处获得:https://github.com/DarkAtra/jsr380-kotlin-issue/tree/workaround

再次感谢您的帮助Rafal G.