简体   繁体   English

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

[英]bean validation not working with kotlin (JSR 380)

so first of all i could not think of a better title for this question so i'm open for changes.所以首先我想不出这个问题更好的标题,所以我愿意改变。

I am trying to validate a bean using the bean validation mechanism (JSR-380) with spring boot.我正在尝试使用带有 spring 引导的 bean 验证机制 (JSR-380) 来验证 bean。

So i got a controller like this:所以我得到了这样的 controller:

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

with this being the User class written in kotlin:这是用 kotlin 编写的用户 class:

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

and this being the test:这是测试:

@Test
internal fun shouldNotCreateNewTestWithInvalidParams() {
    mockMvc.perform(post("/users")
        .param("roles", "invalid role"))
        .andExpect(model().attributeHasFieldErrors("user",  "roles[]"))
}

Invalid Roles are mapped to null.无效角色映射到 null。

As you can see i want roles to contain at least one item with none of the items being null.如您所见,我希望roles至少包含一项,其中没有一项是 null。 However when testing the above code no binding errors are reported if roles contains null values.但是,在测试上述代码时,如果roles包含 null 值,则不会报告绑定错误。 It does report an error if the set is empty though.如果集合为空,它确实会报告错误。 I was thinking that this might be an issue with how kotlin code compiles as the same code works just fine when the User class is written in java.我在想这可能是 kotlin 代码如何编译的问题,因为当用户 class 是用 java 编写时,相同的代码工作得很好。 Like this:像这样:

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

Same Controller, same test.同样的 Controller,同样的测试。

After checking the bytecode i noticed that the kotlin version is not including the nested @NotNull annotation (see below).检查字节码后,我注意到 kotlin 版本不包括嵌套的@NotNull注释(见下文)。

Java: Java:

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

Kotlin: 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

Now the question is why?现在的问题是为什么?

Here's a sample project in case you want to try some stuff.如果您想尝试一些东西,这是一个示例项目

Answer (Kotlin 1.3.70)答案 (Kotlin 1.3.70)

Make sure to compile the kotlin code with jvm target 1.8 or greater and enable this feature by providing the -Xemit-jvm-type-annotations when compiling.确保使用 jvm target 1.8 或更高版本编译 kotlin 代码,并通过在编译时提供-Xemit-jvm-type-annotations启用此功能。

For Spring Boot projects you only have to do the following changes (tested with Spring Boot 2.3.3 and Kotlin 1.4.0):对于 Spring Boot 项目,您只需进行以下更改(使用 Spring Boot 2.3.3 和 Kotlin 1.4.0 进行测试):

  1. In your pom set the following property:在你的 pom 中设置以下属性:
     <properties> <java.version>11</java.version> <kotlin.version>1.4.0</kotlin.version> </properties>
  2. Add <arg>-Xemit-jvm-type-annotations</arg> to the kotlin-maven-plugin :<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 Jetbrains 发行说明


Workaround (pre Kotlin 1.3.70)解决方法(Kotlin 1.3.70 之前)

Rafal G. already pointed out that we could use a custom validator as a workaround. Rafal G.已经指出我们可以使用自定义验证器作为解决方法。 So here's some code:所以这里有一些代码:

The Annotation:注释:

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>> = []
)

The ConstraintValidator:约束验证器:

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

And finally the updated User class:最后是更新的 User 类:

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

Altough validation works now, the resulting ConstrainViolation is slightly different.尽管现在验证有效,但产生的 ConstrainViolation 略有不同。 For example the elementType and propertyPath differs as you can see below.例如elementTypepropertyPath不同,如下所示。

Java:爪哇:

Java 版本

Kotlin:科特林:

Kotlin 版本

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

Thanks again for your help Rafal G.再次感谢您的帮助Rafal G。

Try adding ?尝试添加? like this:像这样:

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

Then the kotlin compiler should realise roles could be null , and it might honor the validation, I know little about JSR380 so i'm just guessing though.然后 kotlin 编译器应该意识到角色可能是null ,它可能会尊重验证,我对 JSR380 知之甚少,所以我只是猜测。

I had a similar problem.我有一个类似的问题。 The solution was to add this dependency:解决方案是添加此依赖项:

 implementation "org.springframework.boot:spring-boot-starter-validation"

Without this dependency, the REST controller was working but no exception was thrown when the javax.validation bean constraints were not honored.如果没有此依赖关系,则 REST controller 可以正常工作,但在不遵守javax.validation bean 约束时不会引发异常。 Now it throws MethodArgumentNotValidException .现在它抛出MethodArgumentNotValidException

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM