* This adds the ability to make Jackson serializers not output the class name in the manifest in cases where type information is stored in a more concise format in the JSON itself. * documentation about rolling updates * Ensure programatic bindings are search in Jackson serializer
This commit is contained in:
parent
89165badbb
commit
eb64d05b8c
5 changed files with 202 additions and 16 deletions
|
|
@ -391,6 +391,35 @@ different settings for remote messages and persisted events.
|
||||||
|
|
||||||
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #several-config }
|
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #several-config }
|
||||||
|
|
||||||
|
### Manifest-less serialization
|
||||||
|
|
||||||
|
When using the Jackson serializer for persistence, given that the fully qualified class name is
|
||||||
|
stored in the manifest, this can result in a lot of wasted disk and IO used, especially when the
|
||||||
|
events are small. To address this, a `type-in-manifest` flag can be turned off, which will result
|
||||||
|
in the class name not appearing in the manifest.
|
||||||
|
|
||||||
|
When deserializing, the Jackson serializer will use the type defined in `deserialization-type`, if
|
||||||
|
present, otherwise it will look for exactly one serialization binding class, and use that. For
|
||||||
|
this to be useful, generally that single type must be a
|
||||||
|
@ref:[Polymorphic type](#polymorphic-types), with all type information necessary to deserialize to
|
||||||
|
the various sub types contained in the JSON message.
|
||||||
|
|
||||||
|
When switching serializers, for example, if doing a rolling update as described
|
||||||
|
@ref:[here](additional/rolling-updates.md#from-java-serialization-to-jackson), there will be
|
||||||
|
periods of time when you may have no serialization bindings declared for the type. In such
|
||||||
|
circumstances, you must use the `deserialization-type` configuration attribute to specify which
|
||||||
|
type should be used to deserialize messages.
|
||||||
|
|
||||||
|
Since this configuration can only be applied to a single root type, you will usually only want to
|
||||||
|
apply it to a per binding configuration, not to the regular `jackson-json` or `jackson-cbor`
|
||||||
|
configurations.
|
||||||
|
|
||||||
|
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #manifestless }
|
||||||
|
|
||||||
|
Note that Akka remoting already implements manifest compression, and so this optimization will have
|
||||||
|
no significant impact for messages sent over remoting. It's only useful for messages serialized for
|
||||||
|
other purposes, such as persistence or distributed data.
|
||||||
|
|
||||||
## Additional features
|
## Additional features
|
||||||
|
|
||||||
Additional Jackson serialization features can be enabled/disabled in configuration. The default values from
|
Additional Jackson serialization features can be enabled/disabled in configuration. The default values from
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,29 @@ akka.serialization.jackson {
|
||||||
compress-larger-than = 0 KiB
|
compress-larger-than = 0 KiB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Whether the type should be written to the manifest.
|
||||||
|
# If this is off, then either deserialization-type must be defined, or there must be exactly
|
||||||
|
# one serialization binding declared for this serializer, and the type in that binding will be
|
||||||
|
# used as the deserialization type. This feature will only work if that type either is a
|
||||||
|
# concrete class, or if it is a supertype that uses Jackson polymorphism (ie, the
|
||||||
|
# @JsonTypeInfo annotation) to store type information in the JSON itself. The intention behind
|
||||||
|
# disabling this is to remove extraneous type information (ie, fully qualified class names) when
|
||||||
|
# serialized objects are persisted in Akka persistence or replicated using Akka distributed
|
||||||
|
# data. Note that Akka remoting already has manifest compression optimizations that address this,
|
||||||
|
# so for types that just get sent over remoting, this offers no optimization.
|
||||||
|
type-in-manifest = on
|
||||||
|
|
||||||
|
# The type to use for deserialization.
|
||||||
|
# This is only used if type-in-manifest is disabled. If set, this type will be used to
|
||||||
|
# deserialize all messages. This is useful if the binding configuration you want to use when
|
||||||
|
# disabling type in manifest cannot be expressed as a single type. Examples of when you might
|
||||||
|
# use this include when changing serializers, so you don't want this serializer used for
|
||||||
|
# serialization and you haven't declared any bindings for it, but you still want to be able to
|
||||||
|
# deserialize messages that were serialized with this serializer, as well as situations where
|
||||||
|
# you only want some sub types of a given Jackson polymorphic type to be serialized using this
|
||||||
|
# serializer.
|
||||||
|
deserialization-type = ""
|
||||||
|
|
||||||
# Specific settings for jackson-json binding can be defined in this section to
|
# Specific settings for jackson-json binding can be defined in this section to
|
||||||
# override the settings in 'akka.serialization.jackson'
|
# override the settings in 'akka.serialization.jackson'
|
||||||
jackson-json {}
|
jackson-json {}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import scala.annotation.tailrec
|
||||||
import scala.util.Failure
|
import scala.util.Failure
|
||||||
import scala.util.Success
|
import scala.util.Success
|
||||||
import scala.util.control.NonFatal
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
import akka.actor.ExtendedActorSystem
|
import akka.actor.ExtendedActorSystem
|
||||||
import akka.annotation.InternalApi
|
import akka.annotation.InternalApi
|
||||||
import akka.event.LogMarker
|
import akka.event.LogMarker
|
||||||
|
|
@ -165,10 +164,46 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
|
||||||
import akka.util.ccompat.JavaConverters._
|
import akka.util.ccompat.JavaConverters._
|
||||||
conf.getStringList("whitelist-class-prefix").asScala.toVector
|
conf.getStringList("whitelist-class-prefix").asScala.toVector
|
||||||
}
|
}
|
||||||
|
private val typeInManifest: Boolean = conf.getBoolean("type-in-manifest")
|
||||||
|
// Calculated eagerly so as to fail fast
|
||||||
|
private val configuredDeserializationType: Option[Class[_ <: AnyRef]] = conf.getString("deserialization-type") match {
|
||||||
|
case "" => None
|
||||||
|
case className =>
|
||||||
|
system.dynamicAccess.getClassFor[AnyRef](className) match {
|
||||||
|
case Success(c) => Some(c)
|
||||||
|
case Failure(_) =>
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
s"Cannot find deserialization-type [$className] for Jackson serializer [$bindingName]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This must lazy otherwise it will deadlock the ActorSystem creation
|
// This must lazy otherwise it will deadlock the ActorSystem creation
|
||||||
private lazy val serialization = SerializationExtension(system)
|
private lazy val serialization = SerializationExtension(system)
|
||||||
|
|
||||||
|
// This must be lazy since it depends on serialization above
|
||||||
|
private lazy val deserializationType: Option[Class[_ <: AnyRef]] = if (typeInManifest) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
configuredDeserializationType.orElse {
|
||||||
|
val bindings = serialization.bindings.filter(_._2.identifier == identifier)
|
||||||
|
bindings match {
|
||||||
|
case Nil =>
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
s"Jackson serializer [$bindingName] with type-in-manifest disabled must either declare" +
|
||||||
|
" a deserialization-type or have exactly one binding configured, but none were configured")
|
||||||
|
|
||||||
|
case Seq((clazz, _)) =>
|
||||||
|
Some(clazz.asSubclass(classOf[AnyRef]))
|
||||||
|
|
||||||
|
case multiple =>
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
s"Jackson serializer [$bindingName] with type-in-manifest disabled must either declare" +
|
||||||
|
" a deserialization-type or have exactly one binding configured, but multiple bindings" +
|
||||||
|
s" were configured [${multiple.mkString(", ")}]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// doesn't have to be volatile, doesn't matter if check is run more than once
|
// doesn't have to be volatile, doesn't matter if check is run more than once
|
||||||
private var serializationBindingsCheckedOk = false
|
private var serializationBindingsCheckedOk = false
|
||||||
|
|
||||||
|
|
@ -176,6 +211,13 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
|
||||||
|
|
||||||
override def manifest(obj: AnyRef): String = {
|
override def manifest(obj: AnyRef): String = {
|
||||||
checkAllowedSerializationBindings()
|
checkAllowedSerializationBindings()
|
||||||
|
deserializationType match {
|
||||||
|
case Some(clazz) =>
|
||||||
|
migrations.get(clazz.getName) match {
|
||||||
|
case Some(transformer) => "#" + transformer.currentVersion
|
||||||
|
case None => ""
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
val className = obj.getClass.getName
|
val className = obj.getClass.getName
|
||||||
checkAllowedClassName(className)
|
checkAllowedClassName(className)
|
||||||
checkAllowedClass(obj.getClass)
|
checkAllowedClass(obj.getClass)
|
||||||
|
|
@ -184,6 +226,7 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
|
||||||
case None => className
|
case None => className
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override def toBinary(obj: AnyRef): Array[Byte] = {
|
override def toBinary(obj: AnyRef): Array[Byte] = {
|
||||||
checkAllowedSerializationBindings()
|
checkAllowedSerializationBindings()
|
||||||
|
|
@ -220,9 +263,9 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
|
||||||
val startTime = if (isDebugEnabled) System.nanoTime else 0L
|
val startTime = if (isDebugEnabled) System.nanoTime else 0L
|
||||||
|
|
||||||
val (fromVersion, manifestClassName) = parseManifest(manifest)
|
val (fromVersion, manifestClassName) = parseManifest(manifest)
|
||||||
checkAllowedClassName(manifestClassName)
|
if (typeInManifest) checkAllowedClassName(manifestClassName)
|
||||||
|
|
||||||
val migration = migrations.get(manifestClassName)
|
val migration = migrations.get(deserializationType.fold(manifestClassName)(_.getName))
|
||||||
|
|
||||||
val className = migration match {
|
val className = migration match {
|
||||||
case Some(transformer) if fromVersion < transformer.currentVersion =>
|
case Some(transformer) if fromVersion < transformer.currentVersion =>
|
||||||
|
|
@ -234,7 +277,7 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
|
||||||
case _ => manifestClassName
|
case _ => manifestClassName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (className ne manifestClassName)
|
if (typeInManifest && (className ne manifestClassName))
|
||||||
checkAllowedClassName(className)
|
checkAllowedClassName(className)
|
||||||
|
|
||||||
if (isCaseObject(className)) {
|
if (isCaseObject(className)) {
|
||||||
|
|
@ -250,13 +293,15 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
|
||||||
logFromBinaryDuration(bytes, bytes, startTime, clazz)
|
logFromBinaryDuration(bytes, bytes, startTime, clazz)
|
||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
val clazz = system.dynamicAccess.getClassFor[AnyRef](className) match {
|
val clazz = deserializationType.getOrElse {
|
||||||
|
system.dynamicAccess.getClassFor[AnyRef](className) match {
|
||||||
case Success(c) => c
|
case Success(c) => c
|
||||||
case Failure(_) =>
|
case Failure(_) =>
|
||||||
throw new NotSerializableException(
|
throw new NotSerializableException(
|
||||||
s"Cannot find manifest class [$className] for serializer [${getClass.getName}].")
|
s"Cannot find manifest class [$className] for serializer [${getClass.getName}].")
|
||||||
}
|
}
|
||||||
checkAllowedClass(clazz)
|
}
|
||||||
|
if (typeInManifest) checkAllowedClass(clazz)
|
||||||
|
|
||||||
val decompressedBytes = decompress(bytes)
|
val decompressedBytes = decompress(bytes)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -478,6 +478,70 @@ class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") {
|
||||||
JacksonSerializer.isGZipped(bytes) should ===(false)
|
JacksonSerializer.isGZipped(bytes) should ===(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"JacksonJsonSerializer without type in manifest" should {
|
||||||
|
import ScalaTestMessages._
|
||||||
|
|
||||||
|
"deserialize messages using the serialization bindings" in withSystem(
|
||||||
|
"""
|
||||||
|
akka.actor {
|
||||||
|
serializers.animal = "akka.serialization.jackson.JacksonJsonSerializer"
|
||||||
|
serialization-identifiers.animal = 9091
|
||||||
|
serialization-bindings {
|
||||||
|
"akka.serialization.jackson.ScalaTestMessages$Animal" = animal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
akka.serialization.jackson.animal.type-in-manifest = off
|
||||||
|
""") { sys =>
|
||||||
|
val msg = Elephant("Dumbo", 1)
|
||||||
|
val serializer = serializerFor(msg, sys)
|
||||||
|
serializer.manifest(msg) should ===("")
|
||||||
|
val bytes = serializer.toBinary(msg)
|
||||||
|
val deserialized = serializer.fromBinary(bytes, "")
|
||||||
|
deserialized should ===(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
"deserialize messages using the configured deserialization type" in withSystem(
|
||||||
|
"""
|
||||||
|
akka.actor {
|
||||||
|
serializers.animal = "akka.serialization.jackson.JacksonJsonSerializer"
|
||||||
|
serialization-identifiers.animal = 9091
|
||||||
|
serialization-bindings {
|
||||||
|
"akka.serialization.jackson.ScalaTestMessages$Elephant" = animal
|
||||||
|
"akka.serialization.jackson.ScalaTestMessages$Lion" = animal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
akka.serialization.jackson.animal {
|
||||||
|
type-in-manifest = off
|
||||||
|
deserialization-type = "akka.serialization.jackson.ScalaTestMessages$Animal"
|
||||||
|
}
|
||||||
|
""") { sys =>
|
||||||
|
val msg = Elephant("Dumbo", 1)
|
||||||
|
val serializer = serializerFor(msg, sys)
|
||||||
|
serializer.manifest(msg) should ===("")
|
||||||
|
val bytes = serializer.toBinary(msg)
|
||||||
|
val deserialized = serializer.fromBinary(bytes, "")
|
||||||
|
deserialized should ===(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
"fail if multiple serialization bindings are declared with no deserialization type" in {
|
||||||
|
an[IllegalArgumentException] should be thrownBy {
|
||||||
|
withSystem("""
|
||||||
|
akka.actor {
|
||||||
|
serializers.animal = "akka.serialization.jackson.JacksonJsonSerializer"
|
||||||
|
serialization-identifiers.animal = 9091
|
||||||
|
serialization-bindings {
|
||||||
|
"akka.serialization.jackson.ScalaTestMessages$Elephant" = animal
|
||||||
|
"akka.serialization.jackson.ScalaTestMessages$Lion" = animal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
akka.serialization.jackson.animal {
|
||||||
|
type-in-manifest = off
|
||||||
|
}
|
||||||
|
""")(sys => checkSerialization(Elephant("Dumbo", 1), sys))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class JacksonSerializerSpec(serializerName: String)
|
abstract class JacksonSerializerSpec(serializerName: String)
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,31 @@ object SerializationDocSpec {
|
||||||
#//#several-config
|
#//#several-config
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
val configManifestless = """
|
||||||
|
#//#manifestless
|
||||||
|
akka.actor {
|
||||||
|
serializers {
|
||||||
|
jackson-json-event = "akka.serialization.jackson.JacksonJsonSerializer"
|
||||||
|
}
|
||||||
|
serialization-identifiers {
|
||||||
|
jackson-json-event = 9001
|
||||||
|
}
|
||||||
|
serialization-bindings {
|
||||||
|
"com.myservice.MyEvent" = jackson-json-event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
akka.serialization.jackson {
|
||||||
|
jackson-json-event {
|
||||||
|
type-in-manifest = off
|
||||||
|
# Since there is exactly one serialization binding declared for this
|
||||||
|
# serializer above, this is optional, but if there were none or many,
|
||||||
|
# this would be mandatory.
|
||||||
|
deserialization-type = "com.myservice.MyEvent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#//#manifestless
|
||||||
|
"""
|
||||||
|
|
||||||
//#polymorphism
|
//#polymorphism
|
||||||
final case class Zoo(primaryAttraction: Animal) extends MySerializable
|
final case class Zoo(primaryAttraction: Animal) extends MySerializable
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue