Bean 验证不适用于 kotlin (JSR 380)

所以首先,我想不出这个问题的更好标题,所以我对改变持开放态度。

我正在尝试使用带有弹簧引导的bean验证机制(JSR-380)来验证bean。

所以我得到了一个这样的控制器:

@Controller
@RequestMapping("/users")
class UserController {
    @PostMapping
    fun createUser(@Valid user: User, bindingResult: BindingResult): ModelAndView {
        return ModelAndView("someview", "user", user)
    }
}

这是用 kotlin 编写的 User 类:

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[]"))
}

无效角色将映射到 null。

如您所见,我想包含至少一个项目,其中没有一个项目为空。但是,在测试上述代码时,如果包含 null 值,则不会报告绑定错误。但是,如果集合为空,则它确实会报告错误。我在想,这可能是kotlin代码如何编译的问题,因为当User类用java编写时,相同的代码工作得很好。喜欢这个:rolesroles

@Data // just lombok...
public class User {
    @NotEmpty
    private Set<@NotNull Role> roles = new HashSet<>();
}

相同的控制器,相同的测试。

检查字节码后,我注意到kotlin版本不包括嵌套注释(见下文)。@NotNull

爪哇岛:

private Ljava/util/Set; roles
@Ljavax/validation/constraints/NotEmpty;()
@Ljavax/validation/constraints/NotNull;() : FIELD, 0;
@Ljavax/validation/constraints/NotEmpty;() : FIELD, null

Kotlin:

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

现在的问题是为什么?

这里有一个示例项目,以防你想尝试一些东西。


答案 1

答案 (Kotlin 1.3.70)

确保使用 jvm 目标 1.8 或更高版本编译 kotlin 代码,并通过在编译时提供 启用此功能。-Xemit-jvm-type-annotations

对于 Spring Boot 项目,您只需执行以下更改(使用 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>
    

示例项目

Jetbrains 发行说明


解决方法(Kotlin 1.3.70 之前的版)

拉法尔·已经指出,我们可以使用自定义验证器作为解决方法。所以这里有一些代码:

注释:

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 }
    }
}

最后是更新的用户类:

data class User(
    @field:NotEmpty
    @field:NoNullElements
    var roles: MutableSet<Role> = HashSet()
)

Altough 验证现在有效,生成的 ConstrainViolation 略有不同。例如,和 不同,如下所示。elementTypepropertyPath

爪哇岛:

The Java Version

Kotlin:

The Kotlin Version

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

再次感谢您的帮助 Rafal G.


答案 2

尝试像这样添加:?

data class User(
    @field:Valid
    @field:NotEmpty
    var roles: MutableSet<@NotNull Role?> = HashSet()
)

然后kotlin编译器应该意识到角色可以是,并且它可能会尊重验证,我对JSR380知之甚少,所以我只是猜测。null


推荐