ICHARM
ICHARM
Scala case class object to JsObject by reflection
Scala case class object to JsObject by reflection

最近在做 Play framework for Scala 项目,数据库用的是 mongodb ,使用 ReactiveMongodb 模块来连接数据库,这套技术体系是第一次使用,遇到最频繁的问题就是如何将case class object 转换成JsObject然后再通过ReactiveMongodb转成BSONObject保存在mongodb数据库中,case class object 和 JsObject 如何相互转换。

傻方法

最傻的方法,手动给每一个case class 实现互转的read和write函数 比如:

import models.BaseModel
import play.api.libs.json.{JsObject, JsResult, JsSuccess, JsValue, Json, OWrites, Reads}

case class Test(name: String, value1: Int, value2: Double, value3: String) extends BaseModel[Test]

object Test {
  
  implicit object reads extends Reads[Test] {
    def reads(json: JsValue): JsResult[Test] = json match {
      case obj: JsObject => {
        val name = (obj \ "name").asOpt[String].getOrElse("")
        val value1 = (obj \ "value1").asOpt[Int].getOrElse(0)
        val value2 = (obj \ "value2").asOpt[Double].getOrElse(0.0)
        val value3 = (obj \ "value3").asOpt[String].getOrElse("")
        JsSuccess(Test(name, value1, value2, value3))
      }
    }
  }

  implicit object writes extends OWrites[Test] {
    def writes(test: Test): JsObject = Json.obj(
      "name" -> test.name,
      "value1" -> test.value1,
      "value2" -> test.value2,
      "value3" -> test.value3,
    )
  }
}

懒方法

Play framework 提供了 json-macro  来很轻松的实现这个功能,参考官方文档 ,只需要一行代码就可以代替上面的read和write函数。其原理是在编译的时候自动生成上面read和write函数,有了json-macro,只需简单一句就可搞定,比如:

import models.BaseModel
import play.api.libs.json._

case class Test(name: String, value1: Int, value2: Double, value3: String) extends BaseModel[Test]

object Test {
  
  // Generates Writes and Reads for App thanks to Json Macros
  implicit val format = Json.format[Test]
}

自以为是的方法

还可以通过反射,在运行时动态转换,在BaseModel中利用scala反射动态获取case class object的字段先转成map,再转成JsObject, 例如:

package models

import java.util.Date

import play.api.libs.json.{JsBoolean, JsNull, JsNumber, JsObject, JsString, Json}
import reactivemongo.bson.BSONObjectID
import utils.JsonUtils

import scala.reflect.runtime.universe._

class BaseModel[T: TypeTag] {

  def tag: TypeTag[T]  = typeTag[T]

  /**
    * Convert model object to JsObject.
    * @return
    */
  def toJsObject: JsObject = {
    val map = fieldsMap()
    val jsObj = JsObject(map.map { item =>
      item._1 -> ( item._2 match {
        case value: String => JsString(value)
        case value: Int => JsNumber(value)
        case value: Double => JsNumber(value)
        case value: Boolean => JsBoolean(value)
        case value: Date => JsNumber(value.getTime)
        case value: BSONObjectID => Json.obj("$oid" -> JsString(value.stringify))
        case _ => JsNull
      })
    })
    jsObj
  }

  /**
    * All field without null => value Map
    * @return
    */
  def fieldsMap() : Map[String, Any] = {
    val tpe = this.tag.tpe
    val allAccessors = tpe.decls.collect { case meth: MethodSymbol if meth.isCaseAccessor => meth }

    val m = runtimeMirror(getClass.getClassLoader)
    val im = m.reflect(this)

    var map = Map[String, Any]()
    allAccessors.foreach { symbol =>
      val fieldName = symbol.name.toString
      val fieldMirror = im.reflectField(symbol)
      val fieldValue = fieldMirror.get
      if (fieldValue != null) {
        map = map.updated(fieldName, fieldValue)
      }
    }
    map
  }
}

比较

第一种方法比较简单,而且也很灵活,但是需要手写大量代码。第二种方法最简单,但是没办法修改转换的逻辑,不够灵活。第三种方法写起来比较难,可以自己定义转换逻辑,比较灵活,但是又一个最大的问题,运行很慢,大约比其他两种方法慢三倍,所以高并发的场景慎用,另外利用Scala反射将Model object转成JsObject相对容易实现,反过来要难很多,目前博主还没有实现这个功能。。。

为了比较三者的性能,设计了一个循环100w次,不断的创建对象并转换成JsObject,看三种方法所需的时间,测试代码:

object Main {
  def main(args: Array[String]): Unit = {
    var count_succ = 0
    val json = Json.obj("name" -> "zapya", "value1" -> 11, "value2" -> 33.234, "value3" -> "zapyaValue")
    val startTime = System.currentTimeMillis()
    for (a <- 1 to 1000000) {
      val obj = Test("zapya", 11, 33.234, "zapyaValue")

//      val jsonConvert = Test.writes.writes(obj) // 方法一
      val jsonConvert = Test.format.writes(obj)  // 方法二
//      val jsonConvert = obj.toJsObject  //方法三
      if (jsonConvert.equals(json)) {
        count_succ += 1
      }
    }
    val time = System.currentTimeMillis() - startTime
    println(s"take $time, success count $count_succ")
  }
}

结果:
方法一:take 4173, success count 1000000
方法二:take 5951, success count 1000000
方法三:take 18873, success count 1000000

发表评论

textsms
account_circle
email

ICHARM

Scala case class object to JsObject by reflection
最近在做 Play framework for Scala 项目,数据库用的是 mongodb ,使用 ReactiveMongodb 模块来连接数据库,这套技术体系是第一次使用,遇到最频繁的问题…
扫描二维码继续阅读
2019-05-29