最近在做 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


风雨兼程路,雨雪初霁时