Spring 如何在后期初始化中保留不可为null的属性

Spring 如何在后期初始化中保留不可为null的属性,spring,spring-boot,kotlin,dto,data-class,Spring,Spring Boot,Kotlin,Dto,Data Class,以下问题:在具有Spring Boot和Kotlin的客户机/服务器环境中,客户机希望创建类型为a的对象,因此通过RESTful端点将数据发布到服务器 实体A在Kotlin中实现为数据类,如下所示: data class A(val mandatoryProperty: String) 从业务角度来看,该属性(也是主键)决不能为null。然而,客户端并不知道它,因为它是由服务器上的Spring@servicebean生成的,成本相当高 现在,在端点处,Spring尝试将客户端的有效负载反序列化

以下问题:在具有
Spring Boot
Kotlin
的客户机/服务器环境中,客户机希望创建类型为a的对象,因此通过RESTful端点将数据发布到服务器

实体A在Kotlin中实现为
数据类
,如下所示:

data class A(val mandatoryProperty: String)
从业务角度来看,该属性(也是主键)决不能为null。然而,客户端并不知道它,因为它是由服务器上的Spring@servicebean生成的,成本相当高

现在,在端点处,Spring尝试将客户端的有效负载反序列化为类型A的对象,但是,
mandatoryProperty
在该时间点未知,这将导致映射异常

有几种方法可以避免这个问题,但没有一种真正让我感到惊讶

  • 不要期望端点处有一个类型为A的对象,而是获取一组描述A的参数,这些参数将一直传递,直到实体实际创建并且mandatoryProperty出现为止。实际上相当麻烦,因为这里的房产比那一个多得多

  • 与1非常相似,但创建一个DTO。但是,我最喜欢的一种方法是,由于
    数据类
    无法扩展,这意味着要将类型A的属性复制到DTO中(强制属性除外),然后将它们复制过来。此外,当A增长时,DTO也必须增长

  • 使mandatoryProperty为空并使用!!整个代码中的运算符。可能是最糟糕的解决方案,因为它挫败了可空变量和不可空变量的感觉

  • 客户端将为mandatoryProperty设置一个虚拟值,该值在生成属性后立即被替换。但是,A由端点验证,因此伪值必须遵守其
    @模式
    约束。所以每个伪值都是一个有效的主键,这让我感觉不好


  • 还有其他更可行的方法吗?

    我认为没有通用的答案。。。所以我会给你我的2美分关于你的变种

    您的第一个变体有一个其他变体所没有的好处,即您不会将给定对象用于其他设计目的(即仅用于端点或后端),但这可能会导致繁琐的开发

    第二个变体很好,但可能会导致其他一些开发错误,例如,当您认为您使用了实际的
    A
    ,但实际上是在DTO上操作时

    变量3和4在这方面与2相似。。。您可以将其用作
    A
    ,即使它仅具有DTO的所有属性

    所以。。。如果您想走安全路线,即任何人都不应将此对象用于任何其他用途,则其特定用途可能应使用第一种变体。4听起来很像黑客。2号和3号可能没问题。3因为当您将它用作DTO时,实际上没有
    mandatoryProperty

    尽管如此,由于您有您最喜欢的(2),我也有一个,我将集中讨论2和3,从2开始,使用子类方法将
    密封类作为超类型:

    sealed class AbstractA {
      // just some properties for demo purposes
      lateinit var sharedResettable: String 
      abstract val sharedReadonly: String
    }
    
    data class A(
      val mandatoryProperty: Long = 0,
      override val sharedReadonly: String
      // we deliberately do not override the sharedResettable here... also for demo purposes only
    ) : AbstractA()
    
    data class ADTO(
      // this has no mandatoryProperty
      override val sharedReadonly: String
    ) : AbstractA()
    
    一些演示代码,演示其用法:

    // just some random setup:
    val a = A(123, "from backend").apply { sharedResettable = "i am from backend" }
    val dto = ADTO("from dto").apply { sharedResettable = "i am dto" }
    
    listOf(a, dto).forEach { anA ->
      // somewhere receiving an A... we do not know what it is exactly... it's just an AbstractA
      val param: AbstractA = anA
      println("Starting with: $param sharedResettable=${param.sharedResettable}")
    
      // set something on it... we do not mind yet, what it is exactly...
      param.sharedResettable = UUID.randomUUID().toString()
    
      // now we want to store it... but wait... did we have an A here? or a newly created DTO? 
      // lets check: (demo purpose again)
      when (param) {
        is ADTO -> store(param) // which now returns an A
        is A -> update(param) // maybe updated also our A so a current A is returned
      }.also { certainlyA ->
        println("After saving/updating: $certainlyA sharedResettable=${certainlyA.sharedResettable /* this was deliberately not part of the data class toString() */}")
      }
    }
    
    // assume the following signature for store & update:
    fun <T> update(param : T) : T
    fun store(a : AbstractA) : A
    
    我还没有给你看丑陋的部分,但你自己已经提到了。。。如何将您的
    ADTO
    转换为
    A
    ,反之亦然?我会让你决定的。这里有几种方法(手动、使用反射或映射实用程序等)。 此变体将所有特定于DTO的属性与非特定于DTO的属性清晰地分开。但是,它也会导致冗余代码(所有
    覆盖
    等)。但至少您知道您操作的对象类型,并且可以相应地设置签名

    像3这样的东西可能更容易设置和维护(关于
    数据类本身;-)。如果您正确设置了边界,当其中有
    null
    时以及当不存在时,它甚至可能是清晰的。。。所以也展示了这个例子。首先从一个相当恼人的变量开始(恼人的是,如果还没有设置变量,当您尝试访问该变量时,它会抛出异常),但至少您可以节省
    null
    -在此处检查:

    data class B(
      val sharedOnly : String,
      var sharedResettable : String
    ) {
      // why nullable? Let it hurt ;-)
      lateinit var mandatoryProperty: ID // ok... Long is not usable with lateinit... that's why there is this ID instead
    }
    data class ID(val id : Long)
    
    演示:

    但是:即使访问
    mandatoryProperty
    会引发一个
    异常
    ,但它在
    toString
    中不可见,如果您需要检查它是否已经初始化(即使用
    ::mandatoryProperty::isInitialized
    ),它也不好看

    因此,我向您展示另一个变体(同时也是我最喜欢的,但是…使用
    null
    ):

    用法:

    val c = C("dto", "resettable") // C(mandatoryProperty=null, sharedOnly=dto, sharedResettable=resettable)
    when {
        c.hasID() -> update(c)
        else -> store(c)
    }.also {newC ->
        // from now on you should know that you are actually dealing with an object that has everything in place...
        println("$newC") // prints: C(mandatoryProperty=123, sharedOnly=dto, sharedResettable=resettable)
    }
    
    最后一种方法的好处是,您可以再次使用
    copy
    -方法,例如:

    val myNewObj = c.copy(mandatoryProperty = 123) // well, you probably don't do that yourself...
    // but the following might rather be a valid case:
    val myNewDTO = c.copy(mandatoryProperty = null)
    
    最后一个是我最喜欢的,因为它需要最少的代码,并使用
    val
    (因此也不可能意外重写,也不可能对副本进行操作)。如果您不喜欢使用
    ,您也可以为
    mandatoryProperty
    添加一个访问器,例如

    fun getMandatoryProperty() = mandatoryProperty ?: throw Exception("You didn't set it!")
    

    最后,如果您有一些助手方法,比如
    hasID
    isDTO
    或其他什么),那么从上下文中也可以清楚地知道您到底在做什么。最重要的可能是建立一个每个人都能理解的约定,这样他们就知道什么时候应用什么或者什么时候期望特定的东西。

    关于2:你不能从数据类扩展,但可以从一般类扩展(密封的,“普通”等)。如果您随后将
    val
    放在超类中,那么您至少会被迫在数据子类中设置适当的
    override
    。。。然而。。。这可能比自己复制所有内容要好一步;-)下一个问题可能会出现在您希望在这些数据之间进行匹配或复制时。;-)Wh
    val c = C("dto", "resettable") // C(mandatoryProperty=null, sharedOnly=dto, sharedResettable=resettable)
    when {
        c.hasID() -> update(c)
        else -> store(c)
    }.also {newC ->
        // from now on you should know that you are actually dealing with an object that has everything in place...
        println("$newC") // prints: C(mandatoryProperty=123, sharedOnly=dto, sharedResettable=resettable)
    }
    
    val myNewObj = c.copy(mandatoryProperty = 123) // well, you probably don't do that yourself...
    // but the following might rather be a valid case:
    val myNewDTO = c.copy(mandatoryProperty = null)
    
    fun getMandatoryProperty() = mandatoryProperty ?: throw Exception("You didn't set it!")