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.

所以我有一个这样的控制器:

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

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

Invalid Roles are mapped to null.

As you can see i want roles to contain at least one item with none of the items being null. However when testing the above code no binding errors are reported if roles contains null values. 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. Like this:

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

Same Controller, same test.

在判断字节码之后,我注意到kotlin版本不包括嵌套的@NotNull注释(见下文).

Java:

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

Now the question is why?

Here's a 100 in case you want to try some stuff.

推荐答案

Answer (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.

对于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. kotlin-maven-plugin的基础上加上<arg>-Xemit-jvm-type-annotations</arg>:
    <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. already pointed out that we could use a custom validator as a workaround. 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:

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

Altough validation works now, the resulting ConstrainViolation is slightly different. For example the elementType and propertyPath differs as you can see below.

Java:

The Java Version

Kotlin:

The Kotlin Version

来源:https://github.com/DarkAtra/jsr380-kotlin-issue/tree/workaround

再次感谢你的帮助

Kotlin相关问答推荐

调用即发即忘方法--哪个作用域?

新的jOOQ Gradle插件无法正确处理自引用关系

如何在Android应用判断上运行多个查询

按钮无法在 Android Studio 上打开新活动

Kotlin:使用另一个列表和字母顺序对列表进行排序的有效方法

如何在 kotlin 中使用带有泛型的密封类

kotest 更改环境变量

如何连接两个 kotlin 流?

Kotlin 使用迭代索引过滤 lambda 数组

如何在 kotlin 中生成 json 对象?

Android Studio 4.0.0 Java 8 库在 D8 和 R8 构建错误中脱糖

更新到版本4.10.1/4.10.2后 Gradle 同步失败

在调用Kotlin数据类中的超类构造函数之前访问函数

如何解决:将Java类转换为Kotlin后出现error: cannot find symbol class ...?

如何在kotlin语言中将字符转换为ascii值

使用 Kotlin 创建自定义 Dagger 2 范围

将字符串编码为Kotlin中的UTF-8

Kotlin中的嵌套let块

Kotlin中对象和数据类的区别是什么?

如何将 Kotlin 的 `with` 表达式用于可空类型