Merge pull request #26870 from akka/wip-24155-jackson2-patriknw
Jackson serializer as replacement for Java serialization, #24155
This commit is contained in:
commit
7c18a01b26
34 changed files with 3049 additions and 103 deletions
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright (C) 2018-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor._
|
||||
import akka.serialization.Serialization
|
||||
import akka.serialization.SerializationExtension
|
||||
import akka.serialization.SerializerWithStringManifest
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.openjdk.jmh.annotations._
|
||||
|
||||
object JacksonSerializationBench {
|
||||
trait TestMessage
|
||||
|
||||
final case class Small(name: String, num: Int) extends TestMessage
|
||||
|
||||
final case class Medium(
|
||||
field1: String,
|
||||
field2: String,
|
||||
field3: String,
|
||||
num1: Int,
|
||||
num2: Int,
|
||||
num3: Int,
|
||||
nested1: Small,
|
||||
nested2: Small,
|
||||
nested3: Small)
|
||||
extends TestMessage
|
||||
|
||||
final case class Large(
|
||||
nested1: Medium,
|
||||
nested2: Medium,
|
||||
nested3: Medium,
|
||||
vector: Vector[Medium],
|
||||
map: Map[String, Medium])
|
||||
extends TestMessage
|
||||
|
||||
// FIXME try with plain java classes (not case class)
|
||||
}
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
@BenchmarkMode(Array(Mode.Throughput))
|
||||
@Fork(2)
|
||||
@Warmup(iterations = 4)
|
||||
@Measurement(iterations = 5)
|
||||
class JacksonSerializationBench {
|
||||
import JacksonSerializationBench._
|
||||
|
||||
val smallMsg1 = Small("abc", 17)
|
||||
val smallMsg2 = Small("def", 18)
|
||||
val smallMsg3 = Small("ghi", 19)
|
||||
val mediumMsg1 = Medium("abc", "def", "ghi", 1, 2, 3, smallMsg1, smallMsg2, smallMsg3)
|
||||
val mediumMsg2 = Medium("ABC", "DEF", "GHI", 10, 20, 30, smallMsg1, smallMsg2, smallMsg3)
|
||||
val mediumMsg3 = Medium("abcABC", "defDEF", "ghiGHI", 100, 200, 300, smallMsg1, smallMsg2, smallMsg3)
|
||||
val largeMsg = Large(
|
||||
mediumMsg1,
|
||||
mediumMsg2,
|
||||
mediumMsg3,
|
||||
Vector(mediumMsg1, mediumMsg2, mediumMsg3),
|
||||
Map("a" -> mediumMsg1, "b" -> mediumMsg2, "c" -> mediumMsg3))
|
||||
|
||||
var system: ActorSystem = _
|
||||
var serialization: Serialization = _
|
||||
|
||||
@Param(Array("jackson-json", "jackson-smile", "jackson-cbor", "java"))
|
||||
private var serializerName: String = _
|
||||
|
||||
@Setup(Level.Trial)
|
||||
def setupTrial(): Unit = {
|
||||
val config = ConfigFactory.parseString(s"""
|
||||
akka {
|
||||
loglevel = WARNING
|
||||
actor {
|
||||
serialization-bindings {
|
||||
"akka.serialization.jackson.JacksonSerializationBench$$TestMessage" = $serializerName
|
||||
}
|
||||
}
|
||||
serialization.jackson {
|
||||
#compress-larger-than = 100 b
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
system = ActorSystem("JacksonSerializationBench", config)
|
||||
serialization = SerializationExtension(system)
|
||||
}
|
||||
|
||||
@TearDown(Level.Trial)
|
||||
def tearDownTrial(): Unit = {
|
||||
Await.result(system.terminate(), 5.seconds)
|
||||
}
|
||||
|
||||
private def serializeDeserialize[T <: AnyRef](msg: T): T = {
|
||||
serialization.findSerializerFor(msg) match {
|
||||
case serializer: SerializerWithStringManifest ⇒
|
||||
val blob = serializer.toBinary(msg)
|
||||
serializer.fromBinary(blob, serializer.manifest(msg)).asInstanceOf[T]
|
||||
case serializer ⇒
|
||||
val blob = serializer.toBinary(msg)
|
||||
if (serializer.includeManifest)
|
||||
serializer.fromBinary(blob, Some(msg.getClass)).asInstanceOf[T]
|
||||
else
|
||||
serializer.fromBinary(blob, None).asInstanceOf[T]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
def small(): Small = {
|
||||
serializeDeserialize(smallMsg1)
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
def medium(): Medium = {
|
||||
serializeDeserialize(mediumMsg1)
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
def large(): Large = {
|
||||
serializeDeserialize(largeMsg)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
* [distributed-data](distributed-data.md)
|
||||
* [cluster-dc](cluster-dc.md)
|
||||
* [serialization](serialization.md)
|
||||
* [serialization-jackson](serialization-jackson.md)
|
||||
* [multi-jvm-testing](multi-jvm-testing.md)
|
||||
* [multi-node-testing](multi-node-testing.md)
|
||||
* [remoting-artery](remoting-artery.md)
|
||||
|
|
|
|||
223
akka-docs/src/main/paradox/serialization-jackson.md
Normal file
223
akka-docs/src/main/paradox/serialization-jackson.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# Serialization with Jackson
|
||||
|
||||
## Dependency
|
||||
|
||||
To use Serialization, you must add the following dependency in your project:
|
||||
|
||||
@@dependency[sbt,Maven,Gradle] {
|
||||
group="com.typesafe.akka"
|
||||
artifact="akka-serialization-jackson_$scala.binary_version$"
|
||||
version="$akka.version$"
|
||||
}
|
||||
|
||||
## Introduction
|
||||
|
||||
You find general concepts for for Akka serialization in the @ref:[Serialization](serialization.md) section.
|
||||
This section describes how to use the Jackson serializer for application specific messages and persistent
|
||||
event and snapshots.
|
||||
|
||||
[Jackson](https://github.com/FasterXML/jackson) has support for both text based JSON and
|
||||
binary formats.
|
||||
|
||||
In many cases ordinary classes can be serialized by Jackson without any additional hints, but sometimes
|
||||
annotations are needed to specify how to convert the objects to JSON/bytes.
|
||||
|
||||
## Usage
|
||||
|
||||
To enable Jackson serialization for a class you need to configure it or one of its super classes
|
||||
in serialization-bindings configuration. Typically you will create a marker @scala[trait]@java[interface]
|
||||
for that purpose and let the messages @scala[extend]@java[implement] that.
|
||||
|
||||
Scala
|
||||
: @@snip [SerializationDocSpec.scala](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #marker-interface }
|
||||
|
||||
Java
|
||||
: @@snip [MySerializable.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/MySerializable.java) { #marker-interface }
|
||||
|
||||
Then you configure the class name of marker @scala[trait]@java[interface] in `serialization-bindings` to
|
||||
one of the supported Jackson formats: `jackson-json`, `jackson-cbor` or `jackson-smile`
|
||||
|
||||
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #serialization-bindings }
|
||||
|
||||
That is all that is needed for basic classes where Jackson understands the structure. A few cases that requires
|
||||
annotations are described below.
|
||||
|
||||
Note that it's only the top level class or its marker @scala[trait]@java[interface] that must be defined in
|
||||
`serialization-bindings`, not nested classes that it references in member fields.
|
||||
|
||||
@@@ note
|
||||
|
||||
Add the `-parameters` Java compiler option for usage by the [ParameterNamesModule](https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names).
|
||||
It reduces the need for some annotations.
|
||||
|
||||
@@@
|
||||
|
||||
## Security
|
||||
|
||||
For security reasons it is disallowed to bind the Jackson serializers to
|
||||
open ended types that might be target for [serialization gadgets](https://medium.com/@cowtowncoder/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062),
|
||||
such as:
|
||||
|
||||
* `java.lang.Object`
|
||||
* `java.io.Serializable`
|
||||
* `java.util.Comparable`.
|
||||
|
||||
The blacklist of possible serialization gadget classes defined by Jackson databind are checked
|
||||
and disallowed for deserialization.
|
||||
|
||||
### Formats
|
||||
|
||||
The following formats are supported, and you select which one to use in the `serialization-bindings`
|
||||
configuration as described above.
|
||||
|
||||
* `jackson-json` - ordinary text based JSON
|
||||
* `jackson-cbor` - binary [CBOR data format](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/cbor)
|
||||
* `jackson-smile` - binary [Smile data format](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/smile)
|
||||
|
||||
The binary formats are more compact and have slightly better better performance than the JSON format.
|
||||
|
||||
TODO: It's undecided if we will support both CBOR or and Smile since the difference is small
|
||||
|
||||
## Annotations
|
||||
|
||||
TODO examples when annotations are needed
|
||||
|
||||
## Schema Evolution
|
||||
|
||||
When using Event Sourcing, but also for rolling updates, schema evolution becomes an important aspect of
|
||||
developing your application. The requirements as well as our own understanding of the business domain may
|
||||
(and will) change over time.
|
||||
|
||||
The Jackson serializer provides a way to perform transformations of the JSON tree model during deserialization.
|
||||
This is working in the same way for the textual and binary formats.
|
||||
|
||||
We will look at a few scenarios of how the classes may be evolved.
|
||||
|
||||
### Remove Field
|
||||
|
||||
Removing a field can be done without any migration code. The Jackson serializer will ignore properties that does
|
||||
not exist in the class.
|
||||
|
||||
### Add Field
|
||||
|
||||
Adding an optional field can be done without any migration code. The default value will be @scala[None]@java[`Optional.empty`].
|
||||
|
||||
Old class:
|
||||
|
||||
Java
|
||||
: @@snip [ItemAdded.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v1/ItemAdded.java) { #add-optional }
|
||||
|
||||
TODO: Scala examples
|
||||
|
||||
New class with a new optional `discount` property and a new `note` field with default value:
|
||||
|
||||
Java
|
||||
: @@snip [ItemAdded.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2a/ItemAdded.java) { #add-optional }
|
||||
|
||||
Let's say we want to have a mandatory `discount` property without default value instead:
|
||||
|
||||
Java
|
||||
: @@snip [ItemAdded.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2b/ItemAdded.java) { #add-mandatory }
|
||||
|
||||
To add a new mandatory field we have to use a `JacksonMigration` class and set the default value in the migration code.
|
||||
|
||||
This is how a migration class would look like for adding a `discount` field:
|
||||
|
||||
Java
|
||||
: @@snip [ItemAddedMigration.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2b/ItemAddedMigration.java) { #add-mandatory }
|
||||
|
||||
Override the `currentVersion` method to define the version number of the current (latest) version. The first version,
|
||||
when no migration was used, is always 1. Increase this version number whenever you perform a change that is not
|
||||
backwards compatible without migration code.
|
||||
|
||||
Implement the transformation of the old JSON structure to the new JSON structure in the `transform` method.
|
||||
The [JsonNode](https://fasterxml.github.io/jackson-databind/javadoc/2.9/com/fasterxml/jackson/databind/JsonNode.html)
|
||||
is mutable so you can add and remove fields, or change values. Note that you have to cast to specific sub-classes
|
||||
such as [ObjectNode](https://fasterxml.github.io/jackson-databind/javadoc/2.9/com/fasterxml/jackson/databind/node/ObjectNode.html)
|
||||
and [ArrayNode](https://fasterxml.github.io/jackson-databind/javadoc/2.9/com/fasterxml/jackson/databind/node/ArrayNode.html)
|
||||
to get access to mutators.
|
||||
|
||||
The migration class must be defined in configuration file:
|
||||
|
||||
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #migrations-conf }
|
||||
|
||||
### Rename Field
|
||||
|
||||
Let's say that we want to rename the `productId` field to `itemId` in the previous example.
|
||||
|
||||
Java
|
||||
: @@snip [ItemAdded.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2c/ItemAdded.java) { #rename }
|
||||
|
||||
The migration code would look like:
|
||||
|
||||
Java
|
||||
: @@snip [ItemAddedMigration.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2c/ItemAddedMigration.java) { #rename }
|
||||
|
||||
### Structural Changes
|
||||
|
||||
In a similar way we can do arbitrary structural changes.
|
||||
|
||||
Old class:
|
||||
|
||||
Java
|
||||
: @@snip [Customer.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v1/Customer.java) { #structural }
|
||||
|
||||
New class:
|
||||
|
||||
Java
|
||||
: @@snip [Customer.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2a/Customer.java) { #structural }
|
||||
|
||||
with the `Address` class:
|
||||
|
||||
Java
|
||||
: @@snip [Address.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2a/Address.java) { #structural }
|
||||
|
||||
The migration code would look like:
|
||||
|
||||
Java
|
||||
: @@snip [CustomerMigration.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2a/CustomerMigration.java) { #structural }
|
||||
|
||||
### Rename Class
|
||||
|
||||
It is also possible to rename the class. For example, let's rename `OrderAdded` to `OrderPlaced`.
|
||||
|
||||
Old class:
|
||||
|
||||
Java
|
||||
: @@snip [OrderAdded.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v1/OrderAdded.java) { #rename-class }
|
||||
|
||||
New class:
|
||||
|
||||
Java
|
||||
: @@snip [OrderPlaced.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2a/OrderPlaced.java) { #rename-class }
|
||||
|
||||
The migration code would look like:
|
||||
|
||||
Java
|
||||
: @@snip [OrderPlacedMigration.java](/akka-serialization-jackson/src/test/java/jdoc/akka/serialization/jackson/v2a/OrderPlacedMigration.java) { #rename-class }
|
||||
|
||||
Note the override of the `transformClassName` method to define the new class name.
|
||||
|
||||
That type of migration must be configured with the old class name as key. The actual class can be removed.
|
||||
|
||||
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #migrations-conf-rename }
|
||||
|
||||
## Jackson Modules
|
||||
|
||||
The following Jackson modules are enabled by default:
|
||||
|
||||
@@snip [reference.conf](/akka-serialization-jackson/src/main/resources/reference.conf) { #jackson-modules }
|
||||
|
||||
You can amend the configuration `akka.serialization.jackson.jackson-modules` to enable other modules.
|
||||
|
||||
The [ParameterNamesModule](https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names) requires that the `-parameters`
|
||||
Java compiler option is enabled.
|
||||
|
||||
### Compression
|
||||
|
||||
JSON can be rather verbose and for large messages it can be beneficial compress large payloads. Messages larger
|
||||
than the following configuration are compressed with GZIP.
|
||||
|
||||
@@snip [reference.conf](/akka-serialization-jackson/src/main/resources/reference.conf) { #compression }
|
||||
|
||||
TODO: The binary formats are currently also compressed. That may change since it might not be needed for those.
|
||||
82
akka-serialization-jackson/src/main/resources/reference.conf
Normal file
82
akka-serialization-jackson/src/main/resources/reference.conf
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
##########################################
|
||||
# Akka Serialization Jackson Config File #
|
||||
##########################################
|
||||
|
||||
# This is the reference config file that contains all the default settings.
|
||||
# Make your edits/overrides in your application.conf.
|
||||
|
||||
#//#jackson-modules
|
||||
akka.serialization.jackson {
|
||||
|
||||
# The Jackson JSON serializer will register these modules.
|
||||
# It is also possible to use jackson-modules = ["*"] to dynamically
|
||||
# find and register all modules in the classpath.
|
||||
jackson-modules += "akka.serialization.jackson.AkkaJacksonModule"
|
||||
jackson-modules += "com.fasterxml.jackson.module.paramnames.ParameterNamesModule"
|
||||
jackson-modules += "com.fasterxml.jackson.datatype.jdk8.Jdk8Module"
|
||||
jackson-modules += "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"
|
||||
jackson-modules += "com.fasterxml.jackson.module.scala.DefaultScalaModule"
|
||||
jackson-modules += "com.fasterxml.jackson.module.afterburner.AfterburnerModule"
|
||||
#jackson-modules += "com.fasterxml.jackson.datatype.pcollections.PCollectionsModule"
|
||||
#jackson-modules += "com.fasterxml.jackson.datatype.guava.GuavaModule"
|
||||
}
|
||||
#//#jackson-modules
|
||||
|
||||
#//#compression
|
||||
akka.serialization.jackson {
|
||||
# The serializer will compress the payload when it's larger than this value.
|
||||
compress-larger-than = 10 KiB
|
||||
}
|
||||
#//#compression
|
||||
|
||||
akka.serialization.jackson {
|
||||
# When enabled and akka.loglevel=DEBUG serialization time and payload size
|
||||
# is logged for each messages.
|
||||
verbose-debug-logging = off
|
||||
|
||||
# Define data migration transformations of old formats to current
|
||||
# format here as a mapping between the (old) class name to be
|
||||
# transformed to the JacksonJsonMigration class that implements
|
||||
# the transformation.
|
||||
migrations {
|
||||
}
|
||||
|
||||
# Configuration of the ObjectMapper serialization features.
|
||||
# See com.fasterxml.jackson.databind.SerializationFeature
|
||||
# Enum values corresponding to the SerializationFeature and their boolean value.
|
||||
serialization-features {
|
||||
|
||||
}
|
||||
|
||||
# Configuration of the ObjectMapper deserialization features.
|
||||
# See com.fasterxml.jackson.databind.SeserializationFeature
|
||||
# Enum values corresponding to the DeserializationFeature and their boolean value.
|
||||
deserialization-features {
|
||||
FAIL_ON_UNKNOWN_PROPERTIES = off
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
akka.actor {
|
||||
serializers {
|
||||
jackson-json = "akka.serialization.jackson.JacksonJsonSerializer"
|
||||
jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer"
|
||||
jackson-smile = "akka.serialization.jackson.JacksonSmileSerializer"
|
||||
}
|
||||
serialization-bindings {
|
||||
# Define bindings for classes or interfaces use Jackson serializer, e.g.
|
||||
# "com.example.Jsonable" = jackson-json
|
||||
# "com.example.MyMessage" = jackson-cbor
|
||||
#
|
||||
# For security reasons it is disallowed to bind the Jackson serializers to
|
||||
# open ended types that might be target to be deserialization gadgets, such as
|
||||
# java.lang.Object, java.io.Serializable, java.util.Comparable
|
||||
|
||||
}
|
||||
serialization-identifiers {
|
||||
"akka.serialization.jackson.JacksonJsonSerializer" = 31
|
||||
"akka.serialization.jackson.JacksonCborSerializer" = 32
|
||||
"akka.serialization.jackson.JacksonSmileSerializer" = 33
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
// FIXME maybe move many things to `akka.serialization.jackson.internal` package?
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.annotation.InternalApi
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonTokenId
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer
|
||||
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer
|
||||
|
||||
// FIXME add serializer for Typed ActorRef also (probably have to be in akka-cluster-typed module)
|
||||
|
||||
/**
|
||||
* INTERNAL API: Adds support for serializing and deserializing [[ActorRef]].
|
||||
*/
|
||||
@InternalApi private[akka] trait ActorRefModule extends JacksonModule {
|
||||
addSerializer(classOf[ActorRef], () => ActorRefSerializer.instance, () => ActorRefDeserializer.instance)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object ActorRefSerializer {
|
||||
val instance: ActorRefSerializer = new ActorRefSerializer
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] class ActorRefSerializer
|
||||
extends StdScalarSerializer[ActorRef](classOf[ActorRef])
|
||||
with ActorSystemAccess {
|
||||
override def serialize(value: ActorRef, jgen: JsonGenerator, provider: SerializerProvider): Unit = {
|
||||
val serializedActorRef = value.path.toSerializationFormatWithAddress(currentSystem().provider.getDefaultAddress)
|
||||
jgen.writeString(serializedActorRef)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object ActorRefDeserializer {
|
||||
val instance: ActorRefDeserializer = new ActorRefDeserializer
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] class ActorRefDeserializer
|
||||
extends StdScalarDeserializer[ActorRef](classOf[ActorRef])
|
||||
with ActorSystemAccess {
|
||||
|
||||
def deserialize(jp: JsonParser, ctxt: DeserializationContext): ActorRef = {
|
||||
if (jp.currentTokenId() == JsonTokenId.ID_STRING) {
|
||||
val serializedActorRef = jp.getText()
|
||||
currentSystem().provider.resolveActorRef(serializedActorRef)
|
||||
} else
|
||||
ctxt.handleUnexpectedToken(handledType(), jp).asInstanceOf[ActorRef]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import akka.actor.ExtendedActorSystem
|
||||
import akka.annotation.InternalApi
|
||||
import akka.serialization.Serialization
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] trait ActorSystemAccess {
|
||||
def currentSystem(): ExtendedActorSystem = {
|
||||
Serialization.currentTransportInformation.value match {
|
||||
case null =>
|
||||
throw new IllegalStateException(
|
||||
"Can't access current ActorSystem, Serialization.currentTransportInformation was not set.")
|
||||
case Serialization.Information(_, system) => system.asInstanceOf[ExtendedActorSystem]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object ActorSystemAccess extends ActorSystemAccess
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import akka.actor.Address
|
||||
import akka.actor.AddressFromURIString
|
||||
import akka.annotation.InternalApi
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonTokenId
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer
|
||||
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer
|
||||
|
||||
/**
|
||||
* INTERNAL API: Adds support for serializing and deserializing [[Address]].
|
||||
*/
|
||||
@InternalApi private[akka] trait AddressModule extends JacksonModule {
|
||||
addSerializer(classOf[Address], () => AddressSerializer.instance, () => AddressDeserializer.instance)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object AddressSerializer {
|
||||
val instance: AddressSerializer = new AddressSerializer
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] class AddressSerializer extends StdScalarSerializer[Address](classOf[Address]) {
|
||||
override def serialize(value: Address, jgen: JsonGenerator, provider: SerializerProvider): Unit = {
|
||||
jgen.writeString(value.toString)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object AddressDeserializer {
|
||||
val instance: AddressDeserializer = new AddressDeserializer
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] class AddressDeserializer extends StdScalarDeserializer[Address](classOf[Address]) {
|
||||
|
||||
def deserialize(jp: JsonParser, ctxt: DeserializationContext): Address = {
|
||||
if (jp.currentTokenId() == JsonTokenId.ID_STRING) {
|
||||
val serializedAddress = jp.getText()
|
||||
AddressFromURIString(serializedAddress)
|
||||
} else
|
||||
ctxt.handleUnexpectedToken(handledType(), jp).asInstanceOf[Address]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
/**
|
||||
* Complete module with support for all custom serializers.
|
||||
*/
|
||||
class AkkaJacksonModule extends JacksonModule with ActorRefModule with AddressModule with FiniteDurationModule {
|
||||
override def getModuleName = "AkkaJacksonModule"
|
||||
}
|
||||
|
||||
object AkkaJacksonModule extends AkkaJacksonModule
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
import akka.annotation.InternalApi
|
||||
import akka.util.JavaDurationConverters._
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer
|
||||
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer
|
||||
|
||||
/**
|
||||
* INTERNAL API: Adds support for serializing and deserializing [[FiniteDuration]].
|
||||
*/
|
||||
@InternalApi private[akka] trait FiniteDurationModule extends JacksonModule {
|
||||
addSerializer(
|
||||
classOf[FiniteDuration],
|
||||
() => FiniteDurationSerializer.instance,
|
||||
() => FiniteDurationDeserializer.instance)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object FiniteDurationSerializer {
|
||||
val instance: FiniteDurationSerializer = new FiniteDurationSerializer
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API: Delegates to DurationSerializer in `jackson-modules-java8`
|
||||
*/
|
||||
@InternalApi private[akka] class FiniteDurationSerializer
|
||||
extends StdScalarSerializer[FiniteDuration](classOf[FiniteDuration]) {
|
||||
override def serialize(value: FiniteDuration, jgen: JsonGenerator, provider: SerializerProvider): Unit = {
|
||||
DurationSerializer.INSTANCE.serialize(value.asJava, jgen, provider)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object FiniteDurationDeserializer {
|
||||
val instance: FiniteDurationDeserializer = new FiniteDurationDeserializer
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API: Delegates to DurationDeserializer in `jackson-modules-java8`
|
||||
*/
|
||||
@InternalApi private[akka] class FiniteDurationDeserializer
|
||||
extends StdScalarDeserializer[FiniteDuration](classOf[FiniteDuration]) {
|
||||
|
||||
def deserialize(jp: JsonParser, ctxt: DeserializationContext): FiniteDuration = {
|
||||
DurationDeserializer.INSTANCE.deserialize(jp, ctxt).asScala
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import akka.util.unused
|
||||
|
||||
/**
|
||||
* Data migration of old formats to current format can
|
||||
* be implemented in a concrete subclass and configured to
|
||||
* be used by the `JacksonSerializer` for a changed class.
|
||||
*
|
||||
* It is used when deserializing data of older version than the
|
||||
* [[JacksonMigration#currentVersion]]. You implement the transformation of the
|
||||
* JSON structure in the [[JacksonMigration#transform]] method. If you have changed the
|
||||
* class name you should override [[JacksonMigration#transformClassName]] and return
|
||||
* current class name.
|
||||
*/
|
||||
abstract class JacksonMigration {
|
||||
|
||||
/**
|
||||
* Define current version. The first version, when no migration was used,
|
||||
* is always 1.
|
||||
*/
|
||||
def currentVersion: Int
|
||||
|
||||
/**
|
||||
* Override this method if you have changed the class name. Return
|
||||
* current class name.
|
||||
*/
|
||||
def transformClassName(@unused fromVersion: Int, className: String): String =
|
||||
className
|
||||
|
||||
/**
|
||||
* Implement the transformation of the old JSON structure to the new
|
||||
* JSON structure. The `JsonNode` is mutable so you can add and remove fields,
|
||||
* or change values. Note that you have to cast to specific sub-classes such
|
||||
* as `ObjectNode` and `ArrayNode` to get access to mutators.
|
||||
*
|
||||
* @param fromVersion the version of the old data
|
||||
* @param json the old JSON data
|
||||
*/
|
||||
def transform(fromVersion: Int, json: JsonNode): JsonNode
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import akka.annotation.InternalApi
|
||||
import com.fasterxml.jackson.core.Version
|
||||
import com.fasterxml.jackson.core.util.VersionUtil
|
||||
import com.fasterxml.jackson.databind.BeanDescription
|
||||
import com.fasterxml.jackson.databind.DeserializationConfig
|
||||
import com.fasterxml.jackson.databind.JavaType
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.Module
|
||||
import com.fasterxml.jackson.databind.Module.SetupContext
|
||||
import com.fasterxml.jackson.databind.SerializationConfig
|
||||
import com.fasterxml.jackson.databind.`type`.TypeModifier
|
||||
import com.fasterxml.jackson.databind.deser.Deserializers
|
||||
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier
|
||||
import com.fasterxml.jackson.databind.ser.Serializers
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object JacksonModule {
|
||||
|
||||
lazy val version: Version = {
|
||||
val groupId = "com.typesafe.akka"
|
||||
val artifactId = "akka-serialization-jackson"
|
||||
val version = akka.Version.current
|
||||
VersionUtil.parseVersion(version, groupId, artifactId)
|
||||
}
|
||||
|
||||
class SerializerResolverByClass(clazz: Class[_], deserializer: () => JsonSerializer[_]) extends Serializers.Base {
|
||||
|
||||
override def findSerializer(
|
||||
config: SerializationConfig,
|
||||
javaType: JavaType,
|
||||
beanDesc: BeanDescription): JsonSerializer[_] = {
|
||||
if (clazz.isAssignableFrom(javaType.getRawClass))
|
||||
deserializer()
|
||||
else
|
||||
super.findSerializer(config, javaType, beanDesc)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DeserializerResolverByClass(clazz: Class[_], serializer: () => JsonDeserializer[_]) extends Deserializers.Base {
|
||||
|
||||
override def findBeanDeserializer(
|
||||
javaType: JavaType,
|
||||
config: DeserializationConfig,
|
||||
beanDesc: BeanDescription): JsonDeserializer[_] = {
|
||||
if (clazz.isAssignableFrom(javaType.getRawClass))
|
||||
serializer()
|
||||
else
|
||||
super.findBeanDeserializer(javaType, config, beanDesc)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object VersionExtractor {
|
||||
def unapply(v: Version) = Some((v.getMajorVersion, v.getMinorVersion))
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] trait JacksonModule extends Module {
|
||||
import JacksonModule._
|
||||
|
||||
private val initializers = Seq.newBuilder[SetupContext => Unit]
|
||||
|
||||
def version: Version = JacksonModule.version
|
||||
|
||||
def setupModule(context: SetupContext): Unit = {
|
||||
initializers.result().foreach(_.apply(context))
|
||||
}
|
||||
|
||||
def addSerializer(
|
||||
clazz: Class[_],
|
||||
serializer: () => JsonSerializer[_],
|
||||
deserializer: () => JsonDeserializer[_]): this.type = {
|
||||
this += { ctx =>
|
||||
ctx.addSerializers(new SerializerResolverByClass(clazz, serializer))
|
||||
ctx.addDeserializers(new DeserializerResolverByClass(clazz, deserializer))
|
||||
}
|
||||
}
|
||||
|
||||
protected def +=(init: SetupContext => Unit): this.type = { initializers += init; this }
|
||||
protected def +=(ser: Serializers): this.type = this += (_.addSerializers(ser))
|
||||
protected def +=(deser: Deserializers): this.type = this += (_.addDeserializers(deser))
|
||||
protected def +=(typeMod: TypeModifier): this.type = this += (_.addTypeModifier(typeMod))
|
||||
protected def +=(beanSerMod: BeanSerializerModifier): this.type = this += (_.addBeanSerializerModifier(beanSerMod))
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import scala.collection.immutable
|
||||
import scala.util.Failure
|
||||
import scala.util.Success
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.DynamicAccess
|
||||
import akka.actor.ExtendedActorSystem
|
||||
import akka.actor.Extension
|
||||
import akka.actor.ExtensionId
|
||||
import akka.actor.ExtensionIdProvider
|
||||
import akka.actor.setup.Setup
|
||||
import akka.annotation.InternalStableApi
|
||||
import akka.event.Logging
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.util.unused
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor
|
||||
import com.fasterxml.jackson.core.JsonFactory
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.Module
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
|
||||
import com.typesafe.config.Config
|
||||
|
||||
object JacksonObjectMapperProvider extends ExtensionId[JacksonObjectMapperProvider] with ExtensionIdProvider {
|
||||
override def get(system: ActorSystem): JacksonObjectMapperProvider = super.get(system)
|
||||
|
||||
override def lookup = JacksonObjectMapperProvider
|
||||
|
||||
override def createExtension(system: ExtendedActorSystem): JacksonObjectMapperProvider =
|
||||
new JacksonObjectMapperProvider(system)
|
||||
|
||||
/**
|
||||
* INTERNAL API: Use [[JacksonObjectMapperProvider#create]]
|
||||
*
|
||||
* This is needed by one test in Lagom where the ObjectMapper is created without starting and ActorSystem.
|
||||
*/
|
||||
@InternalStableApi
|
||||
def createObjectMapper(
|
||||
serializerIdentifier: Int,
|
||||
jsonFactory: Option[JsonFactory],
|
||||
objectMapperFactory: JacksonObjectMapperFactory,
|
||||
config: Config,
|
||||
dynamicAccess: DynamicAccess,
|
||||
log: Option[LoggingAdapter]) = {
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
val mapper = objectMapperFactory.newObjectMapper(serializerIdentifier, jsonFactory)
|
||||
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
|
||||
|
||||
val configuredSerializationFeatures =
|
||||
features(config, "akka.serialization.jackson.serialization-features").map {
|
||||
case (enumName, value) => SerializationFeature.valueOf(enumName) -> value
|
||||
}
|
||||
val serializationFeatures =
|
||||
objectMapperFactory.overrideConfiguredSerializationFeatures(serializerIdentifier, configuredSerializationFeatures)
|
||||
serializationFeatures.foreach {
|
||||
case (feature, value) => mapper.configure(feature, value)
|
||||
}
|
||||
|
||||
val configuredDeserializationFeatures =
|
||||
features(config, "akka.serialization.jackson.deserialization-features").map {
|
||||
case (enumName, value) => DeserializationFeature.valueOf(enumName) -> value
|
||||
}
|
||||
val deserializationFeatures =
|
||||
objectMapperFactory.overrideConfiguredDeserializationFeatures(
|
||||
serializerIdentifier,
|
||||
configuredDeserializationFeatures)
|
||||
deserializationFeatures.foreach {
|
||||
case (feature, value) => mapper.configure(feature, value)
|
||||
}
|
||||
|
||||
val configuredModules = config.getStringList("akka.serialization.jackson.jackson-modules").asScala
|
||||
val modules1 =
|
||||
if (configuredModules.contains("*"))
|
||||
ObjectMapper.findModules(dynamicAccess.classLoader).asScala
|
||||
else
|
||||
configuredModules.flatMap { fqcn ⇒
|
||||
dynamicAccess.createInstanceFor[Module](fqcn, Nil) match {
|
||||
case Success(m) ⇒ Some(m)
|
||||
case Failure(e) ⇒
|
||||
log.foreach(
|
||||
_.error(
|
||||
e,
|
||||
s"Could not load configured Jackson module [$fqcn], " +
|
||||
"please verify classpath dependencies or amend the configuration " +
|
||||
"[akka.serialization.jackson-modules]. Continuing without this module."))
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
val modules2 = modules1.map { module ⇒
|
||||
if (module.isInstanceOf[ParameterNamesModule])
|
||||
// ParameterNamesModule needs a special case for the constructor to ensure that single-parameter
|
||||
// constructors are handled the same way as constructors with multiple parameters.
|
||||
// See https://github.com/FasterXML/jackson-module-parameter-names#delegating-creator
|
||||
new ParameterNamesModule(JsonCreator.Mode.PROPERTIES)
|
||||
else module
|
||||
}.toList
|
||||
|
||||
val modules3 = objectMapperFactory.overrideConfiguredModules(serializerIdentifier, modules2)
|
||||
|
||||
modules3.foreach { module =>
|
||||
mapper.registerModule(module)
|
||||
log.foreach(_.debug("Registered Jackson module [{}]", module.getClass.getName))
|
||||
}
|
||||
|
||||
mapper
|
||||
}
|
||||
|
||||
private def features(config: Config, section: String): immutable.Seq[(String, Boolean)] = {
|
||||
import scala.collection.JavaConverters._
|
||||
val cfg = config.getConfig(section)
|
||||
cfg.root.keySet().asScala.map(key => key -> cfg.getBoolean(key)).toList
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME docs
|
||||
final class JacksonObjectMapperProvider(system: ExtendedActorSystem) extends Extension {
|
||||
private val objectMappers = new ConcurrentHashMap[Int, ObjectMapper]
|
||||
|
||||
/**
|
||||
* Returns an existing Jackson `ObjectMapper` that was created previously with this method, or
|
||||
* creates a new instance.
|
||||
*
|
||||
* The `ObjectMapper` is created with sensible defaults and modules configured
|
||||
* in `akka.serialization.jackson.jackson-modules`. It's using [[JacksonObjectMapperProviderSetup]]
|
||||
* if the `ActorSystem` is started with such [[akka.actor.setup.ActorSystemSetup]].
|
||||
*
|
||||
* The returned `ObjecctMapper` must not be modified, because it may already be in use and such
|
||||
* modifications are not thread-safe.
|
||||
*
|
||||
* @param serializerIdentifier the identifier of the serializer that is using this `ObjectMapper`,
|
||||
* there will be one `ObjectInstance` per serializer
|
||||
* @param jsonFactory optional `JsonFactory` such as `SmileFactory`, for plain JSON `None` (defaults)
|
||||
* can be used
|
||||
*/
|
||||
def getOrCreate(serializerIdentifier: Int, jsonFactory: Option[JsonFactory]): ObjectMapper = {
|
||||
objectMappers.computeIfAbsent(serializerIdentifier, _ => create(serializerIdentifier, jsonFactory))
|
||||
}
|
||||
|
||||
// FIXME Java API, Optional vs Option
|
||||
|
||||
/**
|
||||
* Creates a new instance of a Jackson `ObjectMapper` with sensible defaults and modules configured
|
||||
* in `akka.serialization.jackson.jackson-modules`. It's using [[JacksonObjectMapperProviderSetup]]
|
||||
* if the `ActorSystem` is started with such [[akka.actor.setup.ActorSystemSetup]].
|
||||
*
|
||||
* @param serializerIdentifier the identifier of the serializer that is using this `ObjectMapper`,
|
||||
* there will be one `ObjectInstance` per serializer
|
||||
* @param jsonFactory optional `JsonFactory` such as `SmileFactory`, for plain JSON `None` (defaults)
|
||||
* can be used
|
||||
* @see [[JacksonObjectMapperProvider#getOrCreate]]
|
||||
*/
|
||||
def create(serializerIdentifier: Int, jsonFactory: Option[JsonFactory]): ObjectMapper = {
|
||||
val log = Logging.getLogger(system, JacksonObjectMapperProvider.getClass)
|
||||
val config = system.settings.config
|
||||
val dynamicAccess = system.dynamicAccess
|
||||
|
||||
val factory = system.settings.setup.get[JacksonObjectMapperProviderSetup] match {
|
||||
case Some(setup) => setup.factory
|
||||
case None => new JacksonObjectMapperFactory // default
|
||||
}
|
||||
|
||||
JacksonObjectMapperProvider.createObjectMapper(
|
||||
serializerIdentifier,
|
||||
jsonFactory,
|
||||
factory,
|
||||
config,
|
||||
dynamicAccess,
|
||||
Some(log))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object JacksonObjectMapperProviderSetup {
|
||||
|
||||
/**
|
||||
* Scala API: factory for defining a `JacksonObjectMapperProvider` that is passed in when ActorSystem
|
||||
* is created rather than creating one from configured class name.
|
||||
*/
|
||||
def apply(factory: JacksonObjectMapperFactory): JacksonObjectMapperProviderSetup =
|
||||
new JacksonObjectMapperProviderSetup(factory)
|
||||
|
||||
/**
|
||||
* Java API: factory for defining a `JacksonObjectMapperProvider` that is passed in when ActorSystem
|
||||
* is created rather than creating one from configured class name.
|
||||
*/
|
||||
def create(factory: JacksonObjectMapperFactory): JacksonObjectMapperProviderSetup =
|
||||
apply(factory)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup for defining a `JacksonObjectMapperProvider` that can be passed in when ActorSystem
|
||||
* is created rather than creating one from configured class name. Create a subclass of
|
||||
* [[JacksonObjectMapperFactory]] and override the methods to amend the defaults.
|
||||
*/
|
||||
final class JacksonObjectMapperProviderSetup(val factory: JacksonObjectMapperFactory) extends Setup
|
||||
|
||||
/**
|
||||
* Used with [[JacksonObjectMapperProviderSetup]] for defining a `JacksonObjectMapperProvider` that can be
|
||||
* passed in when ActorSystem is created rather than creating one from configured class name.
|
||||
* Create a subclass and override the methods to amend the defaults.
|
||||
*/
|
||||
class JacksonObjectMapperFactory {
|
||||
|
||||
/**
|
||||
* Override this method to create a new custom instance of `ObjectMapper` for the given `serializerIdentifier`.
|
||||
*
|
||||
* @param serializerIdentifier the identifier of the serializer that is using this `ObjectMapper`,
|
||||
* there will be one `ObjectInstance` per serializer
|
||||
* @param jsonFactory optional `JsonFactory` such as `SmileFactory`, for plain JSON `None` (defaults)
|
||||
* can be used
|
||||
*/
|
||||
def newObjectMapper(@unused serializerIdentifier: Int, jsonFactory: Option[JsonFactory]): ObjectMapper =
|
||||
new ObjectMapper(jsonFactory.orNull)
|
||||
|
||||
// FIXME Java API
|
||||
|
||||
/**
|
||||
* After construction of the `ObjectMapper` the configured serialization features are applied to
|
||||
* the mapper. These features can be amended programatically by overriding this method and
|
||||
* return the features that are to be applied to the `ObjectMapper`.
|
||||
*
|
||||
* @param serializerIdentifier the identifier of the serializer that is using this `ObjectMapper`,
|
||||
* there will be one `ObjectInstance` per serializer
|
||||
* @param configuredFeatures the list of `SerializationFeature` that were configured in
|
||||
* `akka.serialization.jackson.serialization-features`
|
||||
*/
|
||||
def overrideConfiguredSerializationFeatures(
|
||||
@unused serializerIdentifier: Int,
|
||||
configuredFeatures: immutable.Seq[(SerializationFeature, Boolean)])
|
||||
: immutable.Seq[(SerializationFeature, Boolean)] =
|
||||
configuredFeatures
|
||||
|
||||
/**
|
||||
* After construction of the `ObjectMapper` the configured deserialization features are applied to
|
||||
* the mapper. These features can be amended programatically by overriding this method and
|
||||
* return the features that are to be applied to the `ObjectMapper`.
|
||||
*
|
||||
* @param serializerIdentifier the identifier of the serializer that is using this `ObjectMapper`,
|
||||
* there will be one `ObjectInstance` per serializer
|
||||
* @param configuredFeatures the list of `DeserializationFeature` that were configured in
|
||||
* `akka.serialization.jackson.deserialization-features`
|
||||
*/
|
||||
def overrideConfiguredDeserializationFeatures(
|
||||
@unused serializerIdentifier: Int,
|
||||
configuredFeatures: immutable.Seq[(DeserializationFeature, Boolean)])
|
||||
: immutable.Seq[(DeserializationFeature, Boolean)] =
|
||||
configuredFeatures
|
||||
|
||||
/**
|
||||
* After construction of the `ObjectMapper` the configured modules are added to
|
||||
* the mapper. These modules can be amended programatically by overriding this method and
|
||||
* return the modules that are to be applied to the `ObjectMapper`.
|
||||
*
|
||||
* @param serializerIdentifier the identifier of the serializer that is using this `ObjectMapper`,
|
||||
* there will be one `ObjectInstance` per serializer
|
||||
* @param configuredModules the list of `Modules` that were configured in
|
||||
* `akka.serialization.jackson.deserialization-features`
|
||||
*/
|
||||
def overrideConfiguredModules(
|
||||
@unused serializerIdentifier: Int,
|
||||
configuredModules: immutable.Seq[Module]): immutable.Seq[Module] =
|
||||
configuredModules
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.NotSerializableException
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.Failure
|
||||
import scala.util.Success
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
import akka.actor.ExtendedActorSystem
|
||||
import akka.annotation.InternalApi
|
||||
import akka.event.LogMarker
|
||||
import akka.event.Logging
|
||||
import akka.serialization.BaseSerializer
|
||||
import akka.serialization.SerializationExtension
|
||||
import akka.serialization.SerializerWithStringManifest
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.SubTypeValidator
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORFactory
|
||||
import com.fasterxml.jackson.dataformat.smile.SmileFactory
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
@InternalApi private[akka] object JacksonSerializer {
|
||||
|
||||
/**
|
||||
* Using the blacklist from Jackson databind of class names that shouldn't be allowed.
|
||||
* Not nice to depend on implementation details of Jackson, but good to use the same
|
||||
* list to automatically have the list updated when new classes are added in Jackson.
|
||||
*/
|
||||
class GadgetClassBlacklist extends SubTypeValidator {
|
||||
|
||||
private def defaultNoDeserClassNames: java.util.Set[String] =
|
||||
SubTypeValidator.DEFAULT_NO_DESER_CLASS_NAMES // it's has protected visibility
|
||||
|
||||
private val prefixSpring: String = "org.springframework."
|
||||
private val prefixC3P0: String = "com.mchange.v2.c3p0."
|
||||
|
||||
def isAllowedClassName(className: String): Boolean = {
|
||||
if (defaultNoDeserClassNames.contains(className))
|
||||
false
|
||||
else if (className.startsWith(prefixC3P0) && className.endsWith("DataSource"))
|
||||
false
|
||||
else
|
||||
true
|
||||
}
|
||||
|
||||
def isAllowedClass(clazz: Class[_]): Boolean = {
|
||||
if (clazz.getName.startsWith(prefixSpring)) {
|
||||
isAllowedSpringClass(clazz)
|
||||
} else
|
||||
true
|
||||
}
|
||||
|
||||
@tailrec private def isAllowedSpringClass(clazz: Class[_]): Boolean = {
|
||||
if (clazz == null || clazz.equals(classOf[java.lang.Object]))
|
||||
true
|
||||
else {
|
||||
val name = clazz.getSimpleName
|
||||
// looking for "AbstractBeanFactoryPointcutAdvisor" but no point to allow any is there?
|
||||
if ("AbstractPointcutAdvisor".equals(name)
|
||||
// ditto for "FileSystemXmlApplicationContext": block all ApplicationContexts
|
||||
|| "AbstractApplicationContext".equals(name))
|
||||
false
|
||||
else
|
||||
isAllowedSpringClass(clazz.getSuperclass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val disallowedSerializationBindings: Set[Class[_]] =
|
||||
Set(classOf[java.io.Serializable], classOf[java.io.Serializable], classOf[java.lang.Comparable[_]])
|
||||
|
||||
}
|
||||
|
||||
object JacksonJsonSerializer {
|
||||
val Identifier = 31
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API: only public by configuration
|
||||
*
|
||||
* Akka serializer for Jackson with JSON.
|
||||
*/
|
||||
@InternalApi private[akka] final class JacksonJsonSerializer(system: ExtendedActorSystem)
|
||||
extends JacksonSerializer(
|
||||
system,
|
||||
JacksonObjectMapperProvider(system).getOrCreate(JacksonJsonSerializer.Identifier, None))
|
||||
|
||||
object JacksonSmileSerializer {
|
||||
val Identifier = 33
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API: only public by configuration
|
||||
*
|
||||
* Akka serializer for Jackson with Smile.
|
||||
*/
|
||||
@InternalApi private[akka] final class JacksonSmileSerializer(system: ExtendedActorSystem)
|
||||
extends JacksonSerializer(
|
||||
system,
|
||||
JacksonObjectMapperProvider(system).getOrCreate(JacksonSmileSerializer.Identifier, Some(new SmileFactory)))
|
||||
|
||||
object JacksonCborSerializer {
|
||||
val Identifier = 32
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API: only public by configuration
|
||||
*
|
||||
* Akka serializer for Jackson with CBOR.
|
||||
*/
|
||||
@InternalApi private[akka] final class JacksonCborSerializer(system: ExtendedActorSystem)
|
||||
extends JacksonSerializer(
|
||||
system,
|
||||
JacksonObjectMapperProvider(system).getOrCreate(JacksonCborSerializer.Identifier, Some(new CBORFactory)))
|
||||
|
||||
// FIXME Look into if we should support both Smile and CBOR, and what we should recommend if there is a choice.
|
||||
// Make dependencies optional/provided.
|
||||
|
||||
/**
|
||||
* INTERNAL API: Base class for Jackson serializers.
|
||||
*
|
||||
* Configuration in `akka.serialization.jackson` section.
|
||||
* It will load Jackson modules defined in configuration `jackson-modules`.
|
||||
*
|
||||
* It will compress the payload if the the payload is larger than the configured
|
||||
* `compress-larger-than` value.
|
||||
*/
|
||||
@InternalApi private[akka] abstract class JacksonSerializer(
|
||||
val system: ExtendedActorSystem,
|
||||
val objectMapper: ObjectMapper)
|
||||
extends SerializerWithStringManifest
|
||||
with BaseSerializer {
|
||||
import JacksonSerializer.GadgetClassBlacklist
|
||||
|
||||
// FIXME it should be possible to implement ByteBufferSerializer as well, using Jackson's
|
||||
// ByteBufferBackedOutputStream/ByteBufferBackedInputStream
|
||||
|
||||
private val log = Logging.withMarker(system, getClass)
|
||||
private val conf = system.settings.config.getConfig("akka.serialization.jackson")
|
||||
private val isDebugEnabled = conf.getBoolean("verbose-debug-logging") && log.isDebugEnabled
|
||||
private final val BufferSize = 1024 * 4
|
||||
private val compressLargerThan: Long = conf.getBytes("compress-larger-than")
|
||||
private val migrations: Map[String, JacksonMigration] = {
|
||||
import scala.collection.JavaConverters._
|
||||
conf.getConfig("migrations").root.unwrapped.asScala.toMap.map {
|
||||
case (k, v) ⇒
|
||||
val transformer = system.dynamicAccess.createInstanceFor[JacksonMigration](v.toString, Nil).get
|
||||
k -> transformer
|
||||
}
|
||||
}
|
||||
private val blacklist: GadgetClassBlacklist = new GadgetClassBlacklist
|
||||
|
||||
// This must lazy otherwise it will deadlock the ActorSystem creation
|
||||
private lazy val serialization = SerializationExtension(system)
|
||||
|
||||
// doesn't have to be volatile, doesn't matter if check is run more than once
|
||||
private var serializationBindingsCheckedOk = false
|
||||
|
||||
override def manifest(obj: AnyRef): String = {
|
||||
checkAllowedSerializationBindings()
|
||||
val className = obj.getClass.getName
|
||||
checkAllowedClassName(className)
|
||||
checkAllowedClass(obj.getClass)
|
||||
migrations.get(className) match {
|
||||
case Some(transformer) ⇒ className + "#" + transformer.currentVersion
|
||||
case None ⇒ className
|
||||
}
|
||||
}
|
||||
|
||||
override def toBinary(obj: AnyRef): Array[Byte] = {
|
||||
checkAllowedSerializationBindings()
|
||||
val startTime = if (isDebugEnabled) System.nanoTime else 0L
|
||||
val bytes = objectMapper.writeValueAsBytes(obj)
|
||||
// FIXME investigate if compression should be used for the binary formats
|
||||
val result =
|
||||
if (bytes.length > compressLargerThan) compress(bytes)
|
||||
else bytes
|
||||
|
||||
if (isDebugEnabled) {
|
||||
val durationMicros = (System.nanoTime - startTime) / 1000
|
||||
if (bytes.length == result.length)
|
||||
log.debug(
|
||||
"Serialization of [{}] took [{}] µs, size [{}] bytes",
|
||||
obj.getClass.getName,
|
||||
durationMicros,
|
||||
result.length)
|
||||
else
|
||||
log.debug(
|
||||
"Serialization of [{}] took [{}] µs, compressed size [{}] bytes, uncompressed size [{}] bytes",
|
||||
obj.getClass.getName,
|
||||
durationMicros,
|
||||
result.length,
|
||||
bytes.length)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = {
|
||||
checkAllowedSerializationBindings()
|
||||
val startTime = if (isDebugEnabled) System.nanoTime else 0L
|
||||
val compressed = isGZipped(bytes)
|
||||
|
||||
val (fromVersion, manifestClassName) = parseManifest(manifest)
|
||||
checkAllowedClassName(manifestClassName)
|
||||
|
||||
val migration = migrations.get(manifestClassName)
|
||||
|
||||
val className = migration match {
|
||||
case Some(transformer) if fromVersion < transformer.currentVersion ⇒
|
||||
transformer.transformClassName(fromVersion, manifestClassName)
|
||||
case Some(transformer) if fromVersion > transformer.currentVersion ⇒
|
||||
throw new IllegalStateException(
|
||||
s"Migration version ${transformer.currentVersion} is " +
|
||||
s"behind version $fromVersion of deserialized type [$manifestClassName]")
|
||||
case _ ⇒ manifestClassName
|
||||
}
|
||||
if (className ne manifestClassName)
|
||||
checkAllowedClassName(className)
|
||||
|
||||
val clazz = system.dynamicAccess.getClassFor[AnyRef](className) match {
|
||||
case Success(c) ⇒ c
|
||||
case Failure(_) ⇒
|
||||
throw new NotSerializableException(
|
||||
s"Cannot find manifest class [$className] for serializer [${getClass.getName}].")
|
||||
}
|
||||
checkAllowedClass(clazz)
|
||||
|
||||
val decompressBytes = if (compressed) decompress(bytes) else bytes
|
||||
|
||||
val result = migration match {
|
||||
case Some(transformer) if fromVersion < transformer.currentVersion ⇒
|
||||
val jsonTree = objectMapper.readTree(decompressBytes)
|
||||
val newJsonTree = transformer.transform(fromVersion, jsonTree)
|
||||
objectMapper.treeToValue(newJsonTree, clazz)
|
||||
case _ ⇒
|
||||
objectMapper.readValue(decompressBytes, clazz)
|
||||
}
|
||||
|
||||
if (isDebugEnabled) {
|
||||
val durationMicros = (System.nanoTime - startTime) / 1000
|
||||
if (bytes.length == decompressBytes.length)
|
||||
log.debug(
|
||||
"Deserialization of [{}] took [{}] µs, size [{}] bytes",
|
||||
clazz.getName,
|
||||
durationMicros,
|
||||
decompressBytes.length)
|
||||
else
|
||||
log.debug(
|
||||
"Deserialization of [{}] took [{}] µs, compressed size [{}] bytes, uncompressed size [{}] bytes",
|
||||
clazz.getName,
|
||||
durationMicros,
|
||||
decompressBytes.length,
|
||||
bytes.length)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
private def checkAllowedClassName(className: String): Unit = {
|
||||
if (!blacklist.isAllowedClassName(className)) {
|
||||
val warnMsg = s"Can't serialize/deserialize object of type [$className] in [${getClass.getName}]. " +
|
||||
s"Blacklisted for security reasons."
|
||||
log.warning(LogMarker.Security, warnMsg)
|
||||
throw new IllegalArgumentException(warnMsg)
|
||||
}
|
||||
}
|
||||
|
||||
private def checkAllowedClass(clazz: Class[_]): Unit = {
|
||||
if (!blacklist.isAllowedClass(clazz)) {
|
||||
val warnMsg = s"Can't serialize/deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " +
|
||||
s"Blacklisted for security reasons."
|
||||
log.warning(LogMarker.Security, warnMsg)
|
||||
throw new IllegalArgumentException(warnMsg)
|
||||
} else if (!isInWhitelist(clazz)) {
|
||||
val warnMsg = s"Can't serialize/deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " +
|
||||
"Only classes that are whitelisted are allowed for security reasons. " +
|
||||
"Configure whitelist with akka.actor.serialization-bindings or " +
|
||||
"akka.serialization.jackson.whitelist-packages."
|
||||
log.warning(LogMarker.Security, warnMsg)
|
||||
throw new IllegalArgumentException(warnMsg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the `serialization-bindings` as source for the whitelist.
|
||||
* Note that the intended usage of serialization-bindings is for lookup of
|
||||
* serializer when serializing (`toBinary`). For deserialization (`fromBinary`) the serializer-id is
|
||||
* used for selecting serializer.
|
||||
* Here we use `serialization-bindings` also and more importantly when deserializing (fromBinary)
|
||||
* to check that the manifest class is of a known (registered) type.
|
||||
* The drawback of using `serialization-bindings` for this is that an old class can't be removed
|
||||
* from `serialization-bindings` when it's not used for serialization but still used for
|
||||
* deserialization (e.g. rolling update with serialization changes). It's also
|
||||
* not possible to change a binding from a JacksonSerializer to another serializer (e.g. protobuf)
|
||||
* and still bind with the same class (interface).
|
||||
* If this is too limiting we can add another config property as an additional way to
|
||||
* whitelist classes that are not bound to this serializer with serialization-bindings.
|
||||
*/
|
||||
private def isInWhitelist(clazz: Class[_]): Boolean = {
|
||||
try {
|
||||
// The reason for using isInstanceOf rather than `eq this` is to allow change of
|
||||
// serializizer within the Jackson family, but we don't trust other serializers
|
||||
// because they might be bound to open-ended interfaces like java.io.Serializable.
|
||||
val boundSerializer = serialization.serializerFor(clazz)
|
||||
boundSerializer.isInstanceOf[JacksonSerializer] ||
|
||||
// to support rolling updates in Lagom we also trust the binding to the Lagom 1.5.x JacksonJsonSerializer,
|
||||
// which is named OldJacksonJsonSerializer in Lagom 1.6.x
|
||||
// FIXME maybe make this configurable, but I don't see any other usages than for Lagom?
|
||||
boundSerializer.getClass.getName == "com.lightbend.lagom.internal.jackson.OldJacksonJsonSerializer"
|
||||
} catch {
|
||||
case NonFatal(_) => false // not bound
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that serialization-bindings are not configured with open-ended interfaces,
|
||||
* like java.lang.Object, bound to this serializer.
|
||||
*
|
||||
* This check is run on first access since it can't be run from constructor because SerializationExtension
|
||||
* can't be accessed from there.
|
||||
*/
|
||||
private def checkAllowedSerializationBindings(): Unit = {
|
||||
if (!serializationBindingsCheckedOk) {
|
||||
def isBindingOk(clazz: Class[_]): Boolean =
|
||||
try {
|
||||
serialization.serializerFor(clazz) ne this
|
||||
} catch {
|
||||
case NonFatal(_) => true // not bound
|
||||
}
|
||||
|
||||
JacksonSerializer.disallowedSerializationBindings.foreach { clazz =>
|
||||
if (!isBindingOk(clazz)) {
|
||||
val warnMsg = "For security reasons it's not allowed to bind open-ended interfaces like " +
|
||||
s"[${clazz.getName}] to [${getClass.getName}]. " +
|
||||
"Change your akka.actor.serialization-bindings configuration."
|
||||
log.warning(LogMarker.Security, warnMsg)
|
||||
throw new IllegalArgumentException(warnMsg)
|
||||
}
|
||||
}
|
||||
serializationBindingsCheckedOk = true
|
||||
}
|
||||
}
|
||||
|
||||
private def parseManifest(manifest: String) = {
|
||||
val i = manifest.lastIndexOf('#')
|
||||
val fromVersion = if (i == -1) 1 else manifest.substring(i + 1).toInt
|
||||
val manifestClassName = if (i == -1) manifest else manifest.substring(0, i)
|
||||
(fromVersion, manifestClassName)
|
||||
}
|
||||
|
||||
def compress(bytes: Array[Byte]): Array[Byte] = {
|
||||
val bos = new ByteArrayOutputStream(BufferSize)
|
||||
val zip = new GZIPOutputStream(bos)
|
||||
try zip.write(bytes)
|
||||
finally zip.close()
|
||||
bos.toByteArray
|
||||
}
|
||||
|
||||
def decompress(bytes: Array[Byte]): Array[Byte] = {
|
||||
val in = new GZIPInputStream(new ByteArrayInputStream(bytes))
|
||||
val out = new ByteArrayOutputStream()
|
||||
// FIXME pool of recycled buffers?
|
||||
val buffer = new Array[Byte](BufferSize)
|
||||
|
||||
@tailrec def readChunk(): Unit = in.read(buffer) match {
|
||||
case -1 ⇒ ()
|
||||
case n ⇒
|
||||
out.write(buffer, 0, n)
|
||||
readChunk()
|
||||
}
|
||||
|
||||
try readChunk()
|
||||
finally in.close()
|
||||
out.toByteArray
|
||||
}
|
||||
|
||||
def isGZipped(bytes: Array[Byte]): Boolean = {
|
||||
(bytes != null) && (bytes.length >= 2) &&
|
||||
(bytes(0) == GZIPInputStream.GZIP_MAGIC.toByte) &&
|
||||
(bytes(1) == (GZIPInputStream.GZIP_MAGIC >> 8).toByte)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.IntNode;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public class JavaTestEventMigration extends JacksonMigration {
|
||||
|
||||
@Override
|
||||
public int currentVersion() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String transformClassName(int fromVersion, String className) {
|
||||
return JavaTestMessages.Event2.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode transform(int fromVersion, JsonNode json) {
|
||||
ObjectNode root = (ObjectNode) json;
|
||||
root.set("field1V2", root.get("field1"));
|
||||
root.remove("field1");
|
||||
root.set("field2", IntNode.valueOf(17));
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,428 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson;
|
||||
|
||||
import akka.actor.ActorRef;
|
||||
import akka.actor.Address;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface JavaTestMessages {
|
||||
|
||||
public interface TestMessage {}
|
||||
|
||||
public class SimpleCommand implements TestMessage {
|
||||
private final String name;
|
||||
|
||||
// FIXME document gotchas like this (or is there a better way?)
|
||||
// @JsonProperty needed due to single argument constructor, see
|
||||
// https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names
|
||||
public SimpleCommand(@JsonProperty("name") String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SimpleCommand that = (SimpleCommand) o;
|
||||
return name != null ? name.equals(that.name) : that.name == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name != null ? name.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class SimpleCommand2 implements TestMessage {
|
||||
public final String name;
|
||||
public final String name2;
|
||||
|
||||
// note that no annotation needed here, `javac -parameters` and not single param constructor
|
||||
public SimpleCommand2(String name, String name2) {
|
||||
this.name = name;
|
||||
this.name2 = name2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
SimpleCommand2 that = (SimpleCommand2) o;
|
||||
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) return false;
|
||||
return name2 != null ? name2.equals(that.name2) : that.name2 == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name != null ? name.hashCode() : 0;
|
||||
result = 31 * result + (name2 != null ? name2.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class OptionalCommand implements TestMessage {
|
||||
private final Optional<String> maybe;
|
||||
|
||||
public OptionalCommand(@JsonProperty("maybe") Optional<String> maybe) {
|
||||
this.maybe = maybe;
|
||||
}
|
||||
|
||||
public Optional<String> getMaybe() {
|
||||
return maybe;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
OptionalCommand that = (OptionalCommand) o;
|
||||
|
||||
return maybe != null ? maybe.equals(that.maybe) : that.maybe == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return maybe != null ? maybe.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class BooleanCommand implements TestMessage {
|
||||
private final boolean published;
|
||||
|
||||
public BooleanCommand(@JsonProperty("published") boolean published) {
|
||||
this.published = published;
|
||||
}
|
||||
|
||||
public boolean isPublished() {
|
||||
return published;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
BooleanCommand that = (BooleanCommand) o;
|
||||
|
||||
return published == that.published;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (published ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectionsCommand implements TestMessage {
|
||||
private final List<String> strings;
|
||||
// if this was List<Object> it would not automatically work,
|
||||
// which is good, otherwise arbitrary classes could be loaded
|
||||
private final List<SimpleCommand> objects;
|
||||
|
||||
public CollectionsCommand(List<String> strings, List<SimpleCommand> objects) {
|
||||
this.strings = strings;
|
||||
this.objects = objects;
|
||||
}
|
||||
|
||||
public List<String> getStrings() {
|
||||
return strings;
|
||||
}
|
||||
|
||||
public List<SimpleCommand> getObjects() {
|
||||
return objects;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
CollectionsCommand that = (CollectionsCommand) o;
|
||||
|
||||
if (strings != null ? !strings.equals(that.strings) : that.strings != null) return false;
|
||||
return objects != null ? objects.equals(that.objects) : that.objects == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = strings != null ? strings.hashCode() : 0;
|
||||
result = 31 * result + (objects != null ? objects.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class TimeCommand implements TestMessage {
|
||||
public final LocalDateTime timestamp;
|
||||
public final Duration duration;
|
||||
|
||||
public TimeCommand(LocalDateTime timestamp, Duration duration) {
|
||||
this.timestamp = timestamp;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
TimeCommand that = (TimeCommand) o;
|
||||
|
||||
if (timestamp != null ? !timestamp.equals(that.timestamp) : that.timestamp != null)
|
||||
return false;
|
||||
return duration != null ? duration.equals(that.duration) : that.duration == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = timestamp != null ? timestamp.hashCode() : 0;
|
||||
result = 31 * result + (duration != null ? duration.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TimeCommand{" + "timestamp=" + timestamp + ", duration=" + duration + '}';
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandWithActorRef implements TestMessage {
|
||||
public final String name;
|
||||
public final ActorRef replyTo;
|
||||
|
||||
public CommandWithActorRef(String name, ActorRef replyTo) {
|
||||
this.name = name;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
CommandWithActorRef that = (CommandWithActorRef) o;
|
||||
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) return false;
|
||||
return replyTo != null ? replyTo.equals(that.replyTo) : that.replyTo == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name != null ? name.hashCode() : 0;
|
||||
result = 31 * result + (replyTo != null ? replyTo.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandWithAddress implements TestMessage {
|
||||
public final String name;
|
||||
public final Address address;
|
||||
|
||||
public CommandWithAddress(String name, Address address) {
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
CommandWithAddress that = (CommandWithAddress) o;
|
||||
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) return false;
|
||||
return address != null ? address.equals(that.address) : that.address == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name != null ? name.hashCode() : 0;
|
||||
result = 31 * result + (address != null ? address.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class Event1 implements TestMessage {
|
||||
|
||||
private final String field1;
|
||||
|
||||
public Event1(String field1) {
|
||||
this.field1 = field1;
|
||||
}
|
||||
|
||||
public String getField1() {
|
||||
return field1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Event1 event1 = (Event1) o;
|
||||
|
||||
return field1 != null ? field1.equals(event1.field1) : event1.field1 == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return field1 != null ? field1.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class Event2 implements TestMessage {
|
||||
private final String field1V2; // renamed from field1
|
||||
private final int field2; // new mandatory field
|
||||
|
||||
public Event2(String field1V2, int field2) {
|
||||
this.field1V2 = field1V2;
|
||||
this.field2 = field2;
|
||||
}
|
||||
|
||||
public String getField1V2() {
|
||||
return field1V2;
|
||||
}
|
||||
|
||||
public int getField2() {
|
||||
return field2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Event2 event2 = (Event2) o;
|
||||
|
||||
if (field2 != event2.field2) return false;
|
||||
return field1V2 != null ? field1V2.equals(event2.field1V2) : event2.field1V2 == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = field1V2 != null ? field1V2.hashCode() : 0;
|
||||
result = 31 * result + field2;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class Zoo implements TestMessage {
|
||||
public final Animal first;
|
||||
|
||||
public Zoo(@JsonProperty("first") Animal first) {
|
||||
this.first = first;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Zoo zoo = (Zoo) o;
|
||||
|
||||
return first != null ? first.equals(zoo.first) : zoo.first == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return first != null ? first.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = Lion.class, name = "lion"),
|
||||
@JsonSubTypes.Type(value = Elephant.class, name = "elephant")
|
||||
})
|
||||
interface Animal {}
|
||||
|
||||
public final class Lion implements Animal {
|
||||
public final String name;
|
||||
|
||||
public Lion(@JsonProperty("name") String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Lion lion = (Lion) o;
|
||||
|
||||
return name != null ? name.equals(lion.name) : lion.name == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name != null ? name.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public final class Elephant implements Animal {
|
||||
public final String name;
|
||||
public final int age;
|
||||
|
||||
public Elephant(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Elephant elephant = (Elephant) o;
|
||||
|
||||
if (age != elephant.age) return false;
|
||||
return name != null ? name.equals(elephant.name) : elephant.name == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name != null ? name.hashCode() : 0;
|
||||
result = 31 * result + age;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// not defined in JsonSubTypes
|
||||
final class Cockroach implements Animal {
|
||||
public final String name;
|
||||
|
||||
public Cockroach(@JsonProperty("name") String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Cockroach cockroach = (Cockroach) o;
|
||||
|
||||
return name != null ? name.equals(cockroach.name) : cockroach.name == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name != null ? name.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson;
|
||||
|
||||
// #marker-interface
|
||||
/** Marker interface for messages, events and snapshots that are serialized with Jackson. */
|
||||
public interface MySerializable {}
|
||||
|
||||
class MyMessage implements MySerializable {
|
||||
public final String name;
|
||||
public final int nr;
|
||||
|
||||
public MyMessage(String name, int nr) {
|
||||
this.name = name;
|
||||
this.nr = nr;
|
||||
}
|
||||
}
|
||||
// #marker-interface
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v1;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
// #structural
|
||||
public class Customer implements MySerializable {
|
||||
public final String name;
|
||||
public final String street;
|
||||
public final String city;
|
||||
public final String zipCode;
|
||||
public final String country;
|
||||
|
||||
public Customer(String name, String street, String city, String zipCode, String country) {
|
||||
this.name = name;
|
||||
this.street = street;
|
||||
this.city = city;
|
||||
this.zipCode = zipCode;
|
||||
this.country = country;
|
||||
}
|
||||
}
|
||||
// #structural
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v1;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
// #add-optional
|
||||
public class ItemAdded implements MySerializable {
|
||||
public final String shoppingCartId;
|
||||
public final String productId;
|
||||
public final int quantity;
|
||||
|
||||
public ItemAdded(String shoppingCartId, String productId, int quantity) {
|
||||
this.shoppingCartId = shoppingCartId;
|
||||
this.productId = productId;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
}
|
||||
// #add-optional
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v1;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
// #rename-class
|
||||
public class OrderAdded implements MySerializable {
|
||||
public final String shoppingCartId;
|
||||
|
||||
public OrderAdded(String shoppingCartId) {
|
||||
this.shoppingCartId = shoppingCartId;
|
||||
}
|
||||
}
|
||||
// #rename-class
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2a;
|
||||
|
||||
// #structural
|
||||
public class Address {
|
||||
public final String street;
|
||||
public final String city;
|
||||
public final String zipCode;
|
||||
public final String country;
|
||||
|
||||
public Address(String street, String city, String zipCode, String country) {
|
||||
this.street = street;
|
||||
this.city = city;
|
||||
this.zipCode = zipCode;
|
||||
this.country = country;
|
||||
}
|
||||
}
|
||||
// #structural
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2a;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
// #structural
|
||||
public class Customer implements MySerializable {
|
||||
public final String name;
|
||||
public final Address shippingAddress;
|
||||
public final Optional<Address> billingAddress;
|
||||
|
||||
public Customer(String name, Address shippingAddress, Optional<Address> billingAddress) {
|
||||
this.name = name;
|
||||
this.shippingAddress = shippingAddress;
|
||||
this.billingAddress = billingAddress;
|
||||
}
|
||||
}
|
||||
// #structural
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2a;
|
||||
|
||||
// #structural
|
||||
import akka.serialization.jackson.JacksonMigration;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.DoubleNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class CustomerMigration extends JacksonMigration {
|
||||
|
||||
@Override
|
||||
public int currentVersion() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode transform(int fromVersion, JsonNode json) {
|
||||
ObjectNode root = (ObjectNode) json;
|
||||
if (fromVersion <= 1) {
|
||||
ObjectNode shippingAddress = root.with("shippingAddress");
|
||||
shippingAddress.set("street", root.get("street"));
|
||||
shippingAddress.set("city", root.get("city"));
|
||||
shippingAddress.set("zipCode", root.get("zipCode"));
|
||||
shippingAddress.set("country", root.get("country"));
|
||||
root.remove("street");
|
||||
root.remove("city");
|
||||
root.remove("zipCode");
|
||||
root.remove("country");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
// #structural
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2a;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
// #add-optional
|
||||
public class ItemAdded implements MySerializable {
|
||||
public final String shoppingCartId;
|
||||
public final String productId;
|
||||
public final int quantity;
|
||||
public final Optional<Double> discount;
|
||||
public final String note;
|
||||
|
||||
public ItemAdded(
|
||||
String shoppingCartId,
|
||||
String productId,
|
||||
int quantity,
|
||||
Optional<Double> discount,
|
||||
String note) {
|
||||
this.shoppingCartId = shoppingCartId;
|
||||
this.productId = productId;
|
||||
this.quantity = quantity;
|
||||
this.discount = discount;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public ItemAdded(
|
||||
String shoppingCartId, String productId, int quantity, Optional<Double> discount) {
|
||||
this(shoppingCartId, productId, quantity, discount, "");
|
||||
}
|
||||
}
|
||||
// #add-optional
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2a;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
// #rename-class
|
||||
public class OrderPlaced implements MySerializable {
|
||||
public final String shoppingCartId;
|
||||
|
||||
public OrderPlaced(String shoppingCartId) {
|
||||
this.shoppingCartId = shoppingCartId;
|
||||
}
|
||||
}
|
||||
// #rename-class
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2a;
|
||||
|
||||
import akka.serialization.jackson.JacksonMigration;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
// #rename-class
|
||||
public class OrderPlacedMigration extends JacksonMigration {
|
||||
|
||||
@Override
|
||||
public int currentVersion() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String transformClassName(int fromVersion, String className) {
|
||||
return OrderPlaced.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode transform(int fromVersion, JsonNode json) {
|
||||
return json;
|
||||
}
|
||||
}
|
||||
// #rename-class
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2b;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
// #add-mandatory
|
||||
public class ItemAdded implements MySerializable {
|
||||
public final String shoppingCartId;
|
||||
public final String productId;
|
||||
public final int quantity;
|
||||
public final double discount;
|
||||
|
||||
public ItemAdded(String shoppingCartId, String productId, int quantity, double discount) {
|
||||
this.shoppingCartId = shoppingCartId;
|
||||
this.productId = productId;
|
||||
this.quantity = quantity;
|
||||
this.discount = discount;
|
||||
}
|
||||
}
|
||||
// #add-mandatory
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2b;
|
||||
|
||||
// #add-mandatory
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.DoubleNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import akka.serialization.jackson.JacksonMigration;
|
||||
|
||||
public class ItemAddedMigration extends JacksonMigration {
|
||||
|
||||
@Override
|
||||
public int currentVersion() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode transform(int fromVersion, JsonNode json) {
|
||||
ObjectNode root = (ObjectNode) json;
|
||||
if (fromVersion <= 1) {
|
||||
root.set("discount", DoubleNode.valueOf(0.0));
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
// #add-mandatory
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2c;
|
||||
|
||||
import jdoc.akka.serialization.jackson.MySerializable;
|
||||
|
||||
// #rename
|
||||
public class ItemAdded implements MySerializable {
|
||||
public final String shoppingCartId;
|
||||
|
||||
public final String itemId;
|
||||
|
||||
public final int quantity;
|
||||
|
||||
public ItemAdded(String shoppingCartId, String itemId, int quantity) {
|
||||
this.shoppingCartId = shoppingCartId;
|
||||
this.itemId = itemId;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
}
|
||||
// #rename
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package jdoc.akka.serialization.jackson.v2c;
|
||||
|
||||
// #rename
|
||||
|
||||
import akka.serialization.jackson.JacksonMigration;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class ItemAddedMigration extends JacksonMigration {
|
||||
|
||||
@Override
|
||||
public int currentVersion() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode transform(int fromVersion, JsonNode json) {
|
||||
ObjectNode root = (ObjectNode) json;
|
||||
if (fromVersion <= 1) {
|
||||
root.set("itemId", root.get("productId"));
|
||||
root.remove("productId");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
// #rename
|
||||
|
|
@ -0,0 +1,550 @@
|
|||
/*
|
||||
* Copyright (C) 2016-2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package akka.serialization.jackson
|
||||
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Arrays
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.logging.FileHandler
|
||||
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.actor.ActorSystem
|
||||
import akka.actor.Address
|
||||
import akka.actor.BootstrapSetup
|
||||
import akka.actor.ExtendedActorSystem
|
||||
import akka.actor.Status
|
||||
import akka.actor.setup.ActorSystemSetup
|
||||
import akka.serialization.Serialization
|
||||
import akka.serialization.SerializationExtension
|
||||
import akka.testkit.TestActors
|
||||
import akka.testkit.TestKit
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.core.JsonFactory
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.Module
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException
|
||||
import com.fasterxml.jackson.databind.node.IntNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.fasterxml.jackson.module.afterburner.AfterburnerModule
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.Matchers
|
||||
import org.scalatest.WordSpecLike
|
||||
|
||||
object ScalaTestMessages {
|
||||
trait TestMessage
|
||||
|
||||
final case class SimpleCommand(name: String) extends TestMessage
|
||||
final case class SimpleCommand2(name: String, name2: String) extends TestMessage
|
||||
final case class OptionCommand(maybe: Option[String]) extends TestMessage
|
||||
final case class BooleanCommand(published: Boolean) extends TestMessage
|
||||
final case class TimeCommand(timestamp: LocalDateTime, duration: FiniteDuration) extends TestMessage
|
||||
final case class CollectionsCommand(strings: List[String], objects: Vector[SimpleCommand]) extends TestMessage
|
||||
final case class CommandWithActorRef(name: String, replyTo: ActorRef) extends TestMessage
|
||||
final case class CommandWithAddress(name: String, address: Address) extends TestMessage
|
||||
|
||||
final case class Event1(field1: String) extends TestMessage
|
||||
final case class Event2(field1V2: String, field2: Int) extends TestMessage
|
||||
|
||||
final case class Zoo(first: Animal) extends TestMessage
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
@JsonSubTypes(
|
||||
Array(
|
||||
new JsonSubTypes.Type(value = classOf[Lion], name = "lion"),
|
||||
new JsonSubTypes.Type(value = classOf[Elephant], name = "elephant")))
|
||||
sealed trait Animal
|
||||
final case class Lion(name: String) extends Animal
|
||||
final case class Elephant(name: String, age: Int) extends Animal
|
||||
// not defined in JsonSubTypes
|
||||
final case class Cockroach(name: String) extends Animal
|
||||
|
||||
}
|
||||
|
||||
class ScalaTestEventMigration extends JacksonMigration {
|
||||
override def currentVersion = 3
|
||||
|
||||
override def transformClassName(fromVersion: Int, className: String): String =
|
||||
classOf[ScalaTestMessages.Event2].getName
|
||||
|
||||
override def transform(fromVersion: Int, json: JsonNode): JsonNode = {
|
||||
val root = json.asInstanceOf[ObjectNode]
|
||||
root.set("field1V2", root.get("field1"))
|
||||
root.remove("field1")
|
||||
root.set("field2", IntNode.valueOf(17))
|
||||
root
|
||||
}
|
||||
}
|
||||
|
||||
class JacksonCborSerializerSpec extends JacksonSerializerSpec("jackson-cbor") {
|
||||
"JacksonCborSerializer" must {
|
||||
"have right configured identifier" in {
|
||||
serialization().serializerFor(classOf[JavaTestMessages.TestMessage]).identifier should ===(
|
||||
JacksonCborSerializer.Identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JacksonSmileSerializerSpec extends JacksonSerializerSpec("jackson-smile") {
|
||||
"JacksonSmileSerializer" must {
|
||||
"have right configured identifier" in {
|
||||
serialization().serializerFor(classOf[JavaTestMessages.TestMessage]).identifier should ===(
|
||||
JacksonSmileSerializer.Identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") {
|
||||
|
||||
def serializeToJsonString(obj: AnyRef, sys: ActorSystem = system): String = {
|
||||
val blob = serializeToBinary(obj, sys)
|
||||
new String(blob, "utf-8")
|
||||
}
|
||||
|
||||
def deserializeFromJsonString(
|
||||
json: String,
|
||||
serializerId: Int,
|
||||
manifest: String,
|
||||
sys: ActorSystem = system): AnyRef = {
|
||||
val blob = json.getBytes("utf-8")
|
||||
deserializeFromBinary(blob, serializerId, manifest, sys)
|
||||
}
|
||||
|
||||
"JacksonJsonSerializer" must {
|
||||
"have right configured identifier" in {
|
||||
serialization().serializerFor(classOf[JavaTestMessages.TestMessage]).identifier should ===(
|
||||
JacksonJsonSerializer.Identifier)
|
||||
}
|
||||
|
||||
"support lookup of same ObjectMapper via JacksonObjectMapperProvider" in {
|
||||
val mapper = serialization()
|
||||
.serializerFor(classOf[JavaTestMessages.TestMessage])
|
||||
.asInstanceOf[JacksonSerializer]
|
||||
.objectMapper
|
||||
JacksonObjectMapperProvider(system)
|
||||
.getOrCreate(JacksonJsonSerializer.Identifier, None) shouldBe theSameInstanceAs(mapper)
|
||||
|
||||
val anotherIdentifier = 999
|
||||
val mapper2 = JacksonObjectMapperProvider(system).getOrCreate(anotherIdentifier, None)
|
||||
mapper2 should not be theSameInstanceAs(mapper)
|
||||
JacksonObjectMapperProvider(system).getOrCreate(anotherIdentifier, None) shouldBe theSameInstanceAs(mapper2)
|
||||
}
|
||||
}
|
||||
|
||||
"JacksonJsonSerializer with Java message classes" must {
|
||||
import JavaTestMessages._
|
||||
|
||||
// see SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
|
||||
"by default serialize dates and durations as numeric timestamps" in {
|
||||
val msg = new TimeCommand(LocalDateTime.of(2019, 4, 29, 23, 15, 3, 12345), Duration.of(5, ChronoUnit.SECONDS))
|
||||
val json = serializeToJsonString(msg)
|
||||
val expected = """{"timestamp":[2019,4,29,23,15,3,12345],"duration":5.000000000}"""
|
||||
json should ===(expected)
|
||||
}
|
||||
|
||||
// see SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
|
||||
"be possible to serialize dates and durations as text with default date format " in {
|
||||
withSystem("""
|
||||
akka.serialization.jackson.serialization-features {
|
||||
WRITE_DATES_AS_TIMESTAMPS = off
|
||||
}
|
||||
""") { sys =>
|
||||
val msg = new TimeCommand(LocalDateTime.of(2019, 4, 29, 23, 15, 3, 12345), Duration.of(5, ChronoUnit.SECONDS))
|
||||
val json = serializeToJsonString(msg, sys)
|
||||
// Default format is defined in com.fasterxml.jackson.databind.util.StdDateFormat
|
||||
// ISO-8601 yyyy-MM-dd'T'HH:mm:ss.SSSZ
|
||||
// FIXME is this the same as rfc3339, or do we need something else to support interop with the format used by Play JSON?
|
||||
// FIXME should we make this the default rather than numberic timestamps?
|
||||
val expected = """{"timestamp":"2019-04-29T23:15:03.000012345","duration":"PT5S"}"""
|
||||
json should ===(expected)
|
||||
|
||||
// and full round trip
|
||||
checkSerialization(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// FAIL_ON_UNKNOWN_PROPERTIES = off is default in reference.conf
|
||||
"not fail on unknown properties" in {
|
||||
val json = """{"name":"abc","name2":"def","name3":"ghi"}"""
|
||||
val expected = new SimpleCommand2("abc", "def")
|
||||
val serializer = serializerFor(expected)
|
||||
deserializeFromJsonString(json, serializer.identifier, serializer.manifest(expected)) should ===(expected)
|
||||
}
|
||||
|
||||
"be possible to create custom ObjectMapper" in {
|
||||
pending
|
||||
}
|
||||
}
|
||||
|
||||
"JacksonJsonSerializer with Scala message classes" must {
|
||||
import ScalaTestMessages._
|
||||
|
||||
"be possible to create custom ObjectMapper" in {
|
||||
val customJacksonObjectMapperFactory = new JacksonObjectMapperFactory {
|
||||
override def newObjectMapper(serializerIdentifier: Int, jsonFactory: Option[JsonFactory]): ObjectMapper = {
|
||||
if (serializerIdentifier == JacksonJsonSerializer.Identifier) {
|
||||
val mapper = new ObjectMapper(jsonFactory.orNull)
|
||||
// some customer configuration of the mapper
|
||||
mapper.setLocale(Locale.US)
|
||||
mapper
|
||||
} else
|
||||
super.newObjectMapper(serializerIdentifier, jsonFactory)
|
||||
}
|
||||
|
||||
override def overrideConfiguredSerializationFeatures(
|
||||
serializerIdentifier: Int,
|
||||
configuredFeatures: immutable.Seq[(SerializationFeature, Boolean)])
|
||||
: immutable.Seq[(SerializationFeature, Boolean)] = {
|
||||
if (serializerIdentifier == JacksonJsonSerializer.Identifier) {
|
||||
configuredFeatures :+ (SerializationFeature.INDENT_OUTPUT -> true)
|
||||
} else
|
||||
super.overrideConfiguredSerializationFeatures(serializerIdentifier, configuredFeatures)
|
||||
}
|
||||
|
||||
override def overrideConfiguredModules(
|
||||
serializerIdentifier: Int,
|
||||
configuredModules: immutable.Seq[Module]): immutable.Seq[Module] =
|
||||
if (serializerIdentifier == JacksonJsonSerializer.Identifier) {
|
||||
configuredModules.filterNot(_.isInstanceOf[AfterburnerModule])
|
||||
} else
|
||||
super.overrideConfiguredModules(serializerIdentifier, configuredModules)
|
||||
}
|
||||
|
||||
val config = system.settings.config
|
||||
|
||||
val setup = ActorSystemSetup()
|
||||
.withSetup(JacksonObjectMapperProviderSetup(customJacksonObjectMapperFactory))
|
||||
.withSetup(BootstrapSetup(config))
|
||||
withSystem(setup) { sys =>
|
||||
val msg = SimpleCommand2("a", "b")
|
||||
val json = serializeToJsonString(msg, sys)
|
||||
// using the custom ObjectMapper with pretty printing enabled
|
||||
val expected =
|
||||
"""|{
|
||||
| "name" : "a",
|
||||
| "name2" : "b"
|
||||
|}""".stripMargin
|
||||
json should ===(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class JacksonSerializerSpec(serializerName: String)
|
||||
extends TestKit(
|
||||
ActorSystem(
|
||||
"JacksonJsonSerializerSpec",
|
||||
ConfigFactory.parseString(s"""
|
||||
akka.serialization.jackson.migrations {
|
||||
"akka.serialization.jackson.JavaTestMessages$$Event1" = "akka.serialization.jackson.JavaTestEventMigration"
|
||||
"akka.serialization.jackson.JavaTestMessages$$Event2" = "akka.serialization.jackson.JavaTestEventMigration"
|
||||
"akka.serialization.jackson.ScalaTestMessages$$Event1" = "akka.serialization.jackson.ScalaTestEventMigration"
|
||||
"akka.serialization.jackson.ScalaTestMessages$$Event2" = "akka.serialization.jackson.ScalaTestEventMigration"
|
||||
}
|
||||
akka.actor {
|
||||
allow-java-serialization = off
|
||||
serialization-bindings {
|
||||
"akka.serialization.jackson.ScalaTestMessages$$TestMessage" = $serializerName
|
||||
"akka.serialization.jackson.JavaTestMessages$$TestMessage" = $serializerName
|
||||
}
|
||||
}
|
||||
""")))
|
||||
with WordSpecLike
|
||||
with Matchers
|
||||
with BeforeAndAfterAll {
|
||||
|
||||
def serialization(sys: ActorSystem = system): Serialization = SerializationExtension(sys)
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
shutdown()
|
||||
}
|
||||
|
||||
def withSystem[T](config: String)(block: ActorSystem => T): T = {
|
||||
val sys = ActorSystem(system.name, ConfigFactory.parseString(config).withFallback(system.settings.config))
|
||||
try {
|
||||
block(sys)
|
||||
} finally shutdown(sys)
|
||||
}
|
||||
|
||||
def withSystem[T](setup: ActorSystemSetup)(block: ActorSystem => T): T = {
|
||||
val sys = ActorSystem(system.name, setup)
|
||||
try {
|
||||
block(sys)
|
||||
} finally shutdown(sys)
|
||||
}
|
||||
|
||||
def withTransportInformation[T](sys: ActorSystem = system)(block: () => T): T = {
|
||||
Serialization.withTransportInformation(sys.asInstanceOf[ExtendedActorSystem]) { () =>
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
def checkSerialization(obj: AnyRef, sys: ActorSystem = system): Unit = {
|
||||
val serializer = serializerFor(obj, sys)
|
||||
val manifest = serializer.manifest(obj)
|
||||
val serializerId = serializer.identifier
|
||||
val blob = serializeToBinary(obj)
|
||||
val deserialized = deserializeFromBinary(blob, serializerId, manifest, sys)
|
||||
deserialized should ===(obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return tuple of (blob, serializerId, manifest)
|
||||
*/
|
||||
def serializeToBinary(obj: AnyRef, sys: ActorSystem = system): Array[Byte] = {
|
||||
withTransportInformation(sys) { () =>
|
||||
val serializer = serializerFor(obj, sys)
|
||||
serializer.toBinary(obj)
|
||||
}
|
||||
}
|
||||
|
||||
def deserializeFromBinary(
|
||||
blob: Array[Byte],
|
||||
serializerId: Int,
|
||||
manifest: String,
|
||||
sys: ActorSystem = system): AnyRef = {
|
||||
// TransportInformation added by serialization.deserialize
|
||||
serialization(sys).deserialize(blob, serializerId, manifest).get
|
||||
}
|
||||
|
||||
def serializerFor(obj: AnyRef, sys: ActorSystem = system): JacksonSerializer =
|
||||
serialization(sys).findSerializerFor(obj) match {
|
||||
case serializer: JacksonSerializer ⇒ serializer
|
||||
case s ⇒
|
||||
throw new IllegalStateException(s"Wrong serializer ${s.getClass} for ${obj.getClass}")
|
||||
}
|
||||
|
||||
"JacksonSerializer with Java message classes" must {
|
||||
import JavaTestMessages._
|
||||
|
||||
"serialize simple message with one constructor parameter" in {
|
||||
checkSerialization(new SimpleCommand("Bob"))
|
||||
}
|
||||
|
||||
"serialize simple message with two constructor parameters" in {
|
||||
checkSerialization(new SimpleCommand2("Bob", "Alice"))
|
||||
checkSerialization(new SimpleCommand2("Bob", ""))
|
||||
checkSerialization(new SimpleCommand2("Bob", null))
|
||||
}
|
||||
|
||||
"serialize message with boolean property" in {
|
||||
checkSerialization(new BooleanCommand(true))
|
||||
checkSerialization(new BooleanCommand(false))
|
||||
}
|
||||
|
||||
"serialize message with Optional property" in {
|
||||
checkSerialization(new OptionalCommand(Optional.of("abc")))
|
||||
checkSerialization(new OptionalCommand(Optional.empty()))
|
||||
}
|
||||
|
||||
"serialize message with collections" in {
|
||||
val strings = Arrays.asList("a", "b", "c")
|
||||
val objects = Arrays.asList(new SimpleCommand("a"), new SimpleCommand("2"))
|
||||
val msg = new CollectionsCommand(strings, objects)
|
||||
checkSerialization(msg)
|
||||
}
|
||||
|
||||
"serialize message with time" in {
|
||||
val msg = new TimeCommand(LocalDateTime.now(), Duration.of(5, ChronoUnit.SECONDS))
|
||||
checkSerialization(msg)
|
||||
}
|
||||
|
||||
"serialize with ActorRef" in {
|
||||
val echo = system.actorOf(TestActors.echoActorProps)
|
||||
checkSerialization(new CommandWithActorRef("echo", echo))
|
||||
}
|
||||
|
||||
"serialize with Address" in {
|
||||
val address = Address("akka", "sys", "localhost", 2552)
|
||||
checkSerialization(new CommandWithAddress("echo", address))
|
||||
}
|
||||
|
||||
"serialize with polymorphism" in {
|
||||
checkSerialization(new Zoo(new Lion("Simba")))
|
||||
checkSerialization(new Zoo(new Elephant("Elephant", 49)))
|
||||
intercept[InvalidTypeIdException] {
|
||||
// Cockroach not listed in JsonSubTypes
|
||||
checkSerialization(new Zoo(new Cockroach("huh")))
|
||||
}
|
||||
}
|
||||
|
||||
"deserialize with migrations" in {
|
||||
val event1 = new Event1("a")
|
||||
val serializer = serializerFor(event1)
|
||||
val blob = serializer.toBinary(event1)
|
||||
val event2 = serializer.fromBinary(blob, classOf[Event1].getName).asInstanceOf[Event2]
|
||||
event1.getField1 should ===(event2.getField1V2)
|
||||
event2.getField2 should ===(17)
|
||||
}
|
||||
|
||||
"deserialize with migrations from V2" in {
|
||||
val event1 = new Event1("a")
|
||||
val serializer = serializerFor(event1)
|
||||
val blob = serializer.toBinary(event1)
|
||||
val event2 = serializer.fromBinary(blob, classOf[Event1].getName + "#2").asInstanceOf[Event2]
|
||||
event1.getField1 should ===(event2.getField1V2)
|
||||
event2.getField2 should ===(17)
|
||||
}
|
||||
}
|
||||
|
||||
"JacksonSerializer with Scala message classes" must {
|
||||
import ScalaTestMessages._
|
||||
|
||||
"serialize simple message with one constructor parameter" in {
|
||||
checkSerialization(SimpleCommand("Bob"))
|
||||
}
|
||||
|
||||
"serialize simple message with two constructor parameters" in {
|
||||
checkSerialization(SimpleCommand2("Bob", "Alice"))
|
||||
checkSerialization(SimpleCommand2("Bob", ""))
|
||||
checkSerialization(SimpleCommand2("Bob", null))
|
||||
}
|
||||
|
||||
"serialize message with boolean property" in {
|
||||
checkSerialization(BooleanCommand(true))
|
||||
checkSerialization(BooleanCommand(false))
|
||||
}
|
||||
|
||||
"serialize message with Optional property" in {
|
||||
checkSerialization(OptionCommand(Some("abc")))
|
||||
checkSerialization(OptionCommand(None))
|
||||
}
|
||||
|
||||
"serialize message with collections" in {
|
||||
val strings = "a" :: "b" :: "c" :: Nil
|
||||
val objects = Vector(SimpleCommand("a"), SimpleCommand("2"))
|
||||
val msg = CollectionsCommand(strings, objects)
|
||||
checkSerialization(msg)
|
||||
}
|
||||
|
||||
"serialize message with time" in {
|
||||
val msg = TimeCommand(LocalDateTime.now(), 5.seconds)
|
||||
checkSerialization(msg)
|
||||
}
|
||||
|
||||
"serialize FiniteDuration as java.time.Duration" in {
|
||||
withTransportInformation() { () =>
|
||||
val scalaMsg = TimeCommand(LocalDateTime.now(), 5.seconds)
|
||||
val scalaSerializer = serializerFor(scalaMsg)
|
||||
val blob = scalaSerializer.toBinary(scalaMsg)
|
||||
val javaMsg = new JavaTestMessages.TimeCommand(scalaMsg.timestamp, Duration.ofSeconds(5))
|
||||
val javaSerializer = serializerFor(javaMsg)
|
||||
val deserialized = javaSerializer.fromBinary(blob, javaSerializer.manifest(javaMsg))
|
||||
deserialized should ===(javaMsg)
|
||||
}
|
||||
}
|
||||
|
||||
"serialize with ActorRef" in {
|
||||
val echo = system.actorOf(TestActors.echoActorProps)
|
||||
checkSerialization(CommandWithActorRef("echo", echo))
|
||||
}
|
||||
|
||||
"serialize with Address" in {
|
||||
val address = Address("akka", "sys", "localhost", 2552)
|
||||
checkSerialization(CommandWithAddress("echo", address))
|
||||
}
|
||||
|
||||
"serialize with polymorphism" in {
|
||||
checkSerialization(Zoo(Lion("Simba")))
|
||||
checkSerialization(Zoo(Elephant("Elephant", 49)))
|
||||
intercept[InvalidTypeIdException] {
|
||||
// Cockroach not listed in JsonSubTypes
|
||||
checkSerialization(Zoo(Cockroach("huh")))
|
||||
}
|
||||
}
|
||||
|
||||
"deserialize with migrations" in {
|
||||
val event1 = Event1("a")
|
||||
val serializer = serializerFor(event1)
|
||||
val blob = serializer.toBinary(event1)
|
||||
val event2 = serializer.fromBinary(blob, classOf[Event1].getName).asInstanceOf[Event2]
|
||||
event1.field1 should ===(event2.field1V2)
|
||||
event2.field2 should ===(17)
|
||||
}
|
||||
|
||||
"deserialize with migrations from V2" in {
|
||||
val event1 = Event1("a")
|
||||
val serializer = serializerFor(event1)
|
||||
val blob = serializer.toBinary(event1)
|
||||
val event2 = serializer.fromBinary(blob, classOf[Event1].getName + "#2").asInstanceOf[Event2]
|
||||
event1.field1 should ===(event2.field1V2)
|
||||
event2.field2 should ===(17)
|
||||
}
|
||||
|
||||
"not allow serialization of blacklisted class" in {
|
||||
val serializer = serializerFor(SimpleCommand("ok"))
|
||||
val fileHandler = new FileHandler(s"target/tmp-${this.getClass.getName}")
|
||||
try {
|
||||
intercept[IllegalArgumentException] {
|
||||
serializer.manifest(fileHandler)
|
||||
}.getMessage.toLowerCase should include("blacklist")
|
||||
} finally fileHandler.close()
|
||||
}
|
||||
|
||||
"not allow deserialization of blacklisted class" in {
|
||||
withTransportInformation() { () =>
|
||||
val msg = SimpleCommand("ok")
|
||||
val serializer = serializerFor(msg)
|
||||
val blob = serializer.toBinary(msg)
|
||||
intercept[IllegalArgumentException] {
|
||||
// maliciously changing manifest
|
||||
serializer.fromBinary(blob, classOf[FileHandler].getName)
|
||||
}.getMessage.toLowerCase should include("blacklist")
|
||||
}
|
||||
}
|
||||
|
||||
"not allow serialization of class that is not in serialization-bindings (whitelist)" in {
|
||||
val serializer = serializerFor(SimpleCommand("ok"))
|
||||
intercept[IllegalArgumentException] {
|
||||
serializer.manifest(Status.Success("bad"))
|
||||
}.getMessage.toLowerCase should include("whitelist")
|
||||
}
|
||||
|
||||
"not allow deserialization of class that is not in serialization-bindings (whitelist)" in {
|
||||
withTransportInformation() { () =>
|
||||
val msg = SimpleCommand("ok")
|
||||
val serializer = serializerFor(msg)
|
||||
val blob = serializer.toBinary(msg)
|
||||
intercept[IllegalArgumentException] {
|
||||
// maliciously changing manifest
|
||||
serializer.fromBinary(blob, classOf[Status.Success].getName)
|
||||
}.getMessage.toLowerCase should include("whitelist")
|
||||
}
|
||||
}
|
||||
|
||||
"not allow serialization-bindings of open-ended types" in {
|
||||
JacksonSerializer.disallowedSerializationBindings.foreach { clazz =>
|
||||
val className = clazz.getName
|
||||
withClue(className) {
|
||||
intercept[IllegalArgumentException] {
|
||||
val sys = ActorSystem(
|
||||
system.name,
|
||||
ConfigFactory.parseString(s"""
|
||||
akka.actor.serialization-bindings {
|
||||
"$className" = $serializerName
|
||||
"akka.serialization.jackson.ScalaTestMessages$$TestMessage" = $serializerName
|
||||
}
|
||||
""").withFallback(system.settings.config))
|
||||
try {
|
||||
SerializationExtension(sys).serialize(SimpleCommand("hi")).get
|
||||
} finally shutdown(sys)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME test configured modules with `*` and that the Akka modules are found
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Lightbend Inc. <https://www.lightbend.com>
|
||||
*/
|
||||
|
||||
package doc.akka.serialization.jackson
|
||||
|
||||
//#marker-interface
|
||||
/**
|
||||
* Marker interface for messages, events and snapshots that are serialized with Jackson.
|
||||
*/
|
||||
trait MySerializable
|
||||
|
||||
final case class Message(name: String, nr: Int) extends MySerializable
|
||||
//#marker-interface
|
||||
|
||||
object SerializationDocSpec {
|
||||
val config = """
|
||||
#//#serialization-bindings
|
||||
akka.actor {
|
||||
serialization-bindings {
|
||||
"com.myservice.MySerializable" = jackson-json
|
||||
}
|
||||
}
|
||||
#//#serialization-bindings
|
||||
"""
|
||||
|
||||
val configMigration = """
|
||||
#//#migrations-conf
|
||||
akka.serialization.jackson.migrations {
|
||||
"com.myservice.event.ItemAdded" = "com.myservice.event.ItemAddedMigration"
|
||||
}
|
||||
#//#migrations-conf
|
||||
"""
|
||||
|
||||
val configMigrationRenamClass = """
|
||||
#//#migrations-conf-rename
|
||||
akka.serialization.jackson.migrations {
|
||||
"com.myservice.event.OrederAdded" = "com.myservice.event.OrderPlacedMigration"
|
||||
}
|
||||
#//#migrations-conf-rename
|
||||
"""
|
||||
}
|
||||
// FIXME add real tests for the migrations, see EventMigrationTest.java in Lagom
|
||||
84
build.sbt
84
build.sbt
|
|
@ -32,42 +32,45 @@ shellPrompt := { s =>
|
|||
}
|
||||
resolverSettings
|
||||
|
||||
def isScala213: Boolean = System.getProperty("akka.build.scalaVersion", "").startsWith("2.13")
|
||||
|
||||
// When this is updated the set of modules in ActorSystem.allModules should also be updated
|
||||
lazy val aggregatedProjects: Seq[ProjectReference] = List[ProjectReference](
|
||||
actor,
|
||||
actorTests,
|
||||
actorTestkitTyped,
|
||||
actorTyped,
|
||||
actorTypedTests,
|
||||
benchJmh,
|
||||
benchJmhTyped,
|
||||
cluster,
|
||||
clusterMetrics,
|
||||
clusterSharding,
|
||||
clusterShardingTyped,
|
||||
clusterTools,
|
||||
clusterTyped,
|
||||
coordination,
|
||||
discovery,
|
||||
distributedData,
|
||||
docs,
|
||||
multiNodeTestkit,
|
||||
osgi,
|
||||
persistence,
|
||||
persistenceQuery,
|
||||
persistenceShared,
|
||||
persistenceTck,
|
||||
persistenceTyped,
|
||||
protobuf,
|
||||
remote,
|
||||
remoteTests,
|
||||
slf4j,
|
||||
stream,
|
||||
streamTestkit,
|
||||
streamTests,
|
||||
streamTestsTck,
|
||||
streamTyped,
|
||||
testkit)
|
||||
actor,
|
||||
actorTests,
|
||||
actorTestkitTyped,
|
||||
actorTyped,
|
||||
actorTypedTests,
|
||||
cluster,
|
||||
clusterMetrics,
|
||||
clusterSharding,
|
||||
clusterShardingTyped,
|
||||
clusterTools,
|
||||
clusterTyped,
|
||||
coordination,
|
||||
discovery,
|
||||
distributedData,
|
||||
docs,
|
||||
multiNodeTestkit,
|
||||
osgi,
|
||||
persistence,
|
||||
persistenceQuery,
|
||||
persistenceShared,
|
||||
persistenceTck,
|
||||
persistenceTyped,
|
||||
protobuf,
|
||||
remote,
|
||||
remoteTests,
|
||||
slf4j,
|
||||
stream,
|
||||
streamTestkit,
|
||||
streamTests,
|
||||
streamTestsTck,
|
||||
streamTyped,
|
||||
testkit) ++
|
||||
(if (isScala213) List.empty[ProjectReference]
|
||||
else
|
||||
List[ProjectReference](jackson, benchJmh, benchJmhTyped)) // FIXME move 2.13 condition when Jackson ScalaModule has been released for Scala 2.13.0
|
||||
|
||||
lazy val root = Project(id = "akka", base = file("."))
|
||||
.aggregate(aggregatedProjects: _*)
|
||||
|
|
@ -99,7 +102,7 @@ lazy val akkaScalaNightly = akkaModule("akka-scala-nightly")
|
|||
.disablePlugins(ValidatePullRequest, MimaPlugin, CopyrightHeaderInPr)
|
||||
|
||||
lazy val benchJmh = akkaModule("akka-bench-jmh")
|
||||
.dependsOn(Seq(actor, stream, streamTests, persistence, distributedData, testkit).map(
|
||||
.dependsOn(Seq(actor, stream, streamTests, persistence, distributedData, jackson, testkit).map(
|
||||
_ % "compile->compile;compile->test"): _*)
|
||||
.settings(Dependencies.benchJmh)
|
||||
.enablePlugins(JmhPlugin, ScaladocNoVerificationOfDiagrams, NoPublish, CopyrightHeader)
|
||||
|
|
@ -235,6 +238,17 @@ lazy val docs = akkaModule("akka-docs")
|
|||
.disablePlugins(MimaPlugin, WhiteSourcePlugin)
|
||||
.disablePlugins(ScalafixPlugin)
|
||||
|
||||
lazy val jackson = akkaModule("akka-serialization-jackson")
|
||||
.dependsOn(actor, actorTests % "test->test", testkit % "test->test")
|
||||
.settings(Dependencies.jackson)
|
||||
.settings(AutomaticModuleName.settings("akka.serialization.jackson"))
|
||||
.settings(OSGi.jackson)
|
||||
.settings(javacOptions += "-parameters")
|
||||
// FIXME remove when Jackson ScalaModule has been released for Scala 2.13.0
|
||||
.settings(crossScalaVersions -= Dependencies.scala213Version)
|
||||
.enablePlugins(ScaladocNoVerificationOfDiagrams)
|
||||
.disablePlugins(MimaPlugin)
|
||||
|
||||
lazy val multiNodeTestkit = akkaModule("akka-multi-node-testkit")
|
||||
.dependsOn(remote, testkit)
|
||||
.settings(Protobuf.settings)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@ object Dependencies {
|
|||
val slf4jVersion = "1.7.25"
|
||||
val scalaXmlVersion = "1.0.6"
|
||||
val aeronVersion = "1.15.1"
|
||||
val jacksonVersion = "2.9.9"
|
||||
|
||||
val scala212Version = "2.12.8"
|
||||
val scala213Version = "2.13.0-RC2"
|
||||
|
||||
val Versions = Seq(
|
||||
crossScalaVersions := Seq("2.12.8", "2.13.0-RC2"),
|
||||
crossScalaVersions := Seq(scala212Version, scala213Version),
|
||||
scalaVersion := System.getProperty("akka.build.scalaVersion", crossScalaVersions.value.head),
|
||||
scalaCheckVersion := sys.props.get("akka.build.scalaCheckVersion").getOrElse("1.14.0"),
|
||||
scalaTestVersion := {
|
||||
|
|
@ -41,7 +45,8 @@ object Dependencies {
|
|||
CrossVersion.partialVersion(scalaVersion.value) match {
|
||||
case Some((2, n)) if n >= 13 => "0.4.0"
|
||||
case _ => "0.3.7"
|
||||
}})
|
||||
}
|
||||
})
|
||||
|
||||
object Compile {
|
||||
// Compile
|
||||
|
|
@ -78,6 +83,17 @@ object Dependencies {
|
|||
val aeronDriver = "io.aeron" % "aeron-driver" % aeronVersion // ApacheV2
|
||||
val aeronClient = "io.aeron" % "aeron-client" % aeronVersion // ApacheV2
|
||||
|
||||
val jacksonCore = "com.fasterxml.jackson.core" % "jackson-core" % jacksonVersion // ApacheV2
|
||||
val jacksonAnnotations = "com.fasterxml.jackson.core" % "jackson-annotations" % jacksonVersion // ApacheV2
|
||||
val jacksonDatabind = "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion // ApacheV2
|
||||
val jacksonJdk8 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % jacksonVersion // ApacheV2
|
||||
val jacksonJsr310 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % jacksonVersion // ApacheV2
|
||||
val jacksonScala = "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion // ApacheV2
|
||||
val jacksonParameterNames = "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % jacksonVersion // ApacheV2
|
||||
val jacksonAfterburner = "com.fasterxml.jackson.module" % "jackson-module-afterburner" % jacksonVersion // ApacheV2
|
||||
val jacksonCbor = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % jacksonVersion // ApacheV2
|
||||
val jacksonSmile = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-smile" % jacksonVersion // ApacheV2
|
||||
|
||||
object Docs {
|
||||
val sprayJson = "io.spray" %% "spray-json" % "1.3.5" % "test"
|
||||
val gson = "com.google.code.gson" % "gson" % "2.8.5" % "test"
|
||||
|
|
@ -209,6 +225,20 @@ object Dependencies {
|
|||
|
||||
val persistenceShared = l ++= Seq(Provided.levelDB, Provided.levelDBNative)
|
||||
|
||||
val jackson = l ++= Seq(
|
||||
jacksonCore,
|
||||
jacksonAnnotations,
|
||||
jacksonDatabind,
|
||||
jacksonScala,
|
||||
jacksonJdk8,
|
||||
jacksonJsr310,
|
||||
jacksonParameterNames,
|
||||
jacksonAfterburner,
|
||||
jacksonSmile,
|
||||
jacksonCbor,
|
||||
Test.junit,
|
||||
Test.scalatest.value)
|
||||
|
||||
val osgi = l ++= Seq(
|
||||
osgiCore,
|
||||
osgiCompendium,
|
||||
|
|
|
|||
|
|
@ -16,33 +16,38 @@ object OSGi {
|
|||
// The included osgiSettings that creates bundles also publish the jar files
|
||||
// in the .../bundles directory which makes testing locally published artifacts
|
||||
// a pain. Create bundles but publish them to the normal .../jars directory.
|
||||
def osgiSettings = defaultOsgiSettings ++ Seq(
|
||||
Compile / packageBin := {
|
||||
val bundle = OsgiKeys.bundle.value
|
||||
// This normally happens automatically when loading the
|
||||
// sbt-reproducible-builds plugin, but because we replace
|
||||
// `packageBin` wholesale here we need to invoke the post-processing
|
||||
// manually. See also
|
||||
// https://github.com/raboof/sbt-reproducible-builds#sbt-osgi
|
||||
ReproducibleBuildsPlugin.postProcessJar(bundle)
|
||||
},
|
||||
// This will fail the build instead of accidentally removing classes from the resulting artifact.
|
||||
// Each package contained in a project MUST be known to be private or exported, if it's undecided we MUST resolve this
|
||||
OsgiKeys.failOnUndecidedPackage := true,
|
||||
// By default an entry is generated from module group-id, but our modules do not adhere to such package naming
|
||||
OsgiKeys.privatePackage := Seq(),
|
||||
// Explicitly specify the version of JavaSE required #23795 (rather depend on
|
||||
// figuring that out from the JDK it was built with)
|
||||
OsgiKeys.requireCapability := "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version>=1.8))\""
|
||||
)
|
||||
def osgiSettings =
|
||||
defaultOsgiSettings ++ Seq(
|
||||
Compile / packageBin := {
|
||||
val bundle = OsgiKeys.bundle.value
|
||||
// This normally happens automatically when loading the
|
||||
// sbt-reproducible-builds plugin, but because we replace
|
||||
// `packageBin` wholesale here we need to invoke the post-processing
|
||||
// manually. See also
|
||||
// https://github.com/raboof/sbt-reproducible-builds#sbt-osgi
|
||||
ReproducibleBuildsPlugin.postProcessJar(bundle)
|
||||
},
|
||||
// This will fail the build instead of accidentally removing classes from the resulting artifact.
|
||||
// Each package contained in a project MUST be known to be private or exported, if it's undecided we MUST resolve this
|
||||
OsgiKeys.failOnUndecidedPackage := true,
|
||||
// By default an entry is generated from module group-id, but our modules do not adhere to such package naming
|
||||
OsgiKeys.privatePackage := Seq(),
|
||||
// Explicitly specify the version of JavaSE required #23795 (rather depend on
|
||||
// figuring that out from the JDK it was built with)
|
||||
OsgiKeys.requireCapability := "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version>=1.8))\"")
|
||||
|
||||
val actor = osgiSettings ++ Seq(
|
||||
OsgiKeys.exportPackage := Seq("akka*"),
|
||||
OsgiKeys.privatePackage := Seq("akka.osgi.impl"),
|
||||
//akka-actor packages are not imported, as contained in the CP
|
||||
OsgiKeys.importPackage := (osgiOptionalImports map optionalResolution) ++ Seq("!sun.misc", scalaJava8CompatImport(), scalaVersion(scalaImport).value, configImport(), "*"),
|
||||
// dynamicImportPackage needed for loading classes defined in configuration
|
||||
OsgiKeys.dynamicImportPackage := Seq("*"))
|
||||
OsgiKeys.exportPackage := Seq("akka*"),
|
||||
OsgiKeys.privatePackage := Seq("akka.osgi.impl"),
|
||||
//akka-actor packages are not imported, as contained in the CP
|
||||
OsgiKeys.importPackage := (osgiOptionalImports.map(optionalResolution)) ++ Seq(
|
||||
"!sun.misc",
|
||||
scalaJava8CompatImport(),
|
||||
scalaVersion(scalaImport).value,
|
||||
configImport(),
|
||||
"*"),
|
||||
// dynamicImportPackage needed for loading classes defined in configuration
|
||||
OsgiKeys.dynamicImportPackage := Seq("*"))
|
||||
|
||||
val actorTyped = exports(Seq("akka.actor.typed.*"))
|
||||
|
||||
|
|
@ -60,29 +65,27 @@ object OSGi {
|
|||
|
||||
val protobuf = exports(Seq("akka.protobuf.*"))
|
||||
|
||||
val jackson = exports(Seq("akka.serialization.jackson.*"))
|
||||
|
||||
val remote = exports(Seq("akka.remote.*"))
|
||||
|
||||
val parsing = exports(
|
||||
Seq("akka.parboiled2.*", "akka.shapeless.*"),
|
||||
imports = Seq(optionalResolution("scala.quasiquotes")))
|
||||
val parsing =
|
||||
exports(Seq("akka.parboiled2.*", "akka.shapeless.*"), imports = Seq(optionalResolution("scala.quasiquotes")))
|
||||
|
||||
val httpCore = exports(Seq("akka.http.*"), imports = Seq(scalaJava8CompatImport()))
|
||||
|
||||
val http = exports(
|
||||
Seq("akka.http.impl.server") ++
|
||||
Seq(
|
||||
"akka.http.$DSL$.server.*",
|
||||
"akka.http.$DSL$.client.*",
|
||||
"akka.http.$DSL$.coding.*",
|
||||
"akka.http.$DSL$.common.*",
|
||||
"akka.http.$DSL$.marshalling.*",
|
||||
"akka.http.$DSL$.unmarshalling.*") flatMap { p =>
|
||||
Seq(p.replace("$DSL$", "scaladsl"), p.replace("$DSL$", "javadsl"))
|
||||
},
|
||||
imports = Seq(
|
||||
scalaJava8CompatImport(),
|
||||
akkaImport("akka.stream.*"),
|
||||
akkaImport("akka.parboiled2.*")))
|
||||
(Seq("akka.http.impl.server") ++
|
||||
Seq(
|
||||
"akka.http.$DSL$.server.*",
|
||||
"akka.http.$DSL$.client.*",
|
||||
"akka.http.$DSL$.coding.*",
|
||||
"akka.http.$DSL$.common.*",
|
||||
"akka.http.$DSL$.marshalling.*",
|
||||
"akka.http.$DSL$.unmarshalling.*")).flatMap { p =>
|
||||
Seq(p.replace("$DSL$", "scaladsl"), p.replace("$DSL$", "javadsl"))
|
||||
},
|
||||
imports = Seq(scalaJava8CompatImport(), akkaImport("akka.stream.*"), akkaImport("akka.parboiled2.*")))
|
||||
|
||||
val httpTestkit = exports(Seq("akka.http.scaladsl.testkit.*", "akka.http.javadsl.testkit.*"))
|
||||
|
||||
|
|
@ -94,18 +97,13 @@ object OSGi {
|
|||
|
||||
val stream =
|
||||
exports(
|
||||
packages = Seq(
|
||||
"akka.stream.*",
|
||||
"com.typesafe.sslconfig.akka.*"
|
||||
),
|
||||
packages = Seq("akka.stream.*", "com.typesafe.sslconfig.akka.*"),
|
||||
imports = Seq(
|
||||
scalaJava8CompatImport(),
|
||||
scalaParsingCombinatorImport(),
|
||||
sslConfigCoreImport("com.typesafe.sslconfig.ssl.*"),
|
||||
sslConfigCoreImport("com.typesafe.sslconfig.util.*"),
|
||||
"!com.typesafe.sslconfig.akka.*"
|
||||
)
|
||||
)
|
||||
"!com.typesafe.sslconfig.akka.*"))
|
||||
|
||||
val streamTestkit = exports(Seq("akka.stream.testkit.*"))
|
||||
|
||||
|
|
@ -113,11 +111,7 @@ object OSGi {
|
|||
|
||||
val persistence = exports(
|
||||
Seq("akka.persistence.*"),
|
||||
imports = Seq(
|
||||
optionalResolution("org.fusesource.leveldbjni.*"),
|
||||
optionalResolution("org.iq80.leveldb.*")
|
||||
)
|
||||
)
|
||||
imports = Seq(optionalResolution("org.fusesource.leveldbjni.*"), optionalResolution("org.iq80.leveldb.*")))
|
||||
|
||||
val persistenceTyped = exports(Seq("akka.persistence.typed.*"))
|
||||
|
||||
|
|
@ -135,11 +129,19 @@ object OSGi {
|
|||
// to be able to find reference.conf
|
||||
"akka.testkit")
|
||||
|
||||
def exports(packages: Seq[String] = Seq(), imports: Seq[String] = Nil) = osgiSettings ++ Seq(
|
||||
OsgiKeys.importPackage := imports ++ scalaVersion(defaultImports).value,
|
||||
OsgiKeys.exportPackage := packages)
|
||||
def defaultImports(scalaVersion: String) = Seq("!sun.misc", akkaImport(), configImport(), "!scala.compat.java8.*",
|
||||
"!scala.util.parsing.*", scalaImport(scalaVersion), "*")
|
||||
def exports(packages: Seq[String] = Seq(), imports: Seq[String] = Nil) =
|
||||
osgiSettings ++ Seq(
|
||||
OsgiKeys.importPackage := imports ++ scalaVersion(defaultImports).value,
|
||||
OsgiKeys.exportPackage := packages)
|
||||
def defaultImports(scalaVersion: String) =
|
||||
Seq(
|
||||
"!sun.misc",
|
||||
akkaImport(),
|
||||
configImport(),
|
||||
"!scala.compat.java8.*",
|
||||
"!scala.util.parsing.*",
|
||||
scalaImport(scalaVersion),
|
||||
"*")
|
||||
def akkaImport(packageName: String = "akka.*") = versionedImport(packageName, "2.5", "2.6")
|
||||
def configImport(packageName: String = "com.typesafe.config.*") = versionedImport(packageName, "1.3.0", "1.4.0")
|
||||
def scalaImport(version: String) = {
|
||||
|
|
@ -148,13 +150,20 @@ object OSGi {
|
|||
val ScalaVersion(epoch, major) = version
|
||||
versionedImport(packageName, s"$epoch.$major", s"$epoch.${major.toInt + 1}")
|
||||
}
|
||||
def scalaJava8CompatImport(packageName: String = "scala.compat.java8.*") = versionedImport(packageName, "0.7.0", "1.0.0")
|
||||
def scalaParsingCombinatorImport(packageName: String = "scala.util.parsing.combinator.*") = versionedImport(packageName, "1.1.0", "1.2.0")
|
||||
def sslConfigCoreImport(packageName: String = "com.typesafe.sslconfig") = versionedImport(packageName, "0.2.3", "1.0.0")
|
||||
def sslConfigCoreSslImport(packageName: String = "com.typesafe.sslconfig.ssl.*") = versionedImport(packageName, "0.2.3", "1.0.0")
|
||||
def sslConfigCoreUtilImport(packageName: String = "com.typesafe.sslconfig.util.*") = versionedImport(packageName, "0.2.3", "1.0.0")
|
||||
def kamonImport(packageName: String = "kamon.sigar.*") = optionalResolution(versionedImport(packageName, "1.6.5", "1.6.6"))
|
||||
def sigarImport(packageName: String = "org.hyperic.*") = optionalResolution(versionedImport(packageName, "1.6.5", "1.6.6"))
|
||||
def scalaJava8CompatImport(packageName: String = "scala.compat.java8.*") =
|
||||
versionedImport(packageName, "0.7.0", "1.0.0")
|
||||
def scalaParsingCombinatorImport(packageName: String = "scala.util.parsing.combinator.*") =
|
||||
versionedImport(packageName, "1.1.0", "1.2.0")
|
||||
def sslConfigCoreImport(packageName: String = "com.typesafe.sslconfig") =
|
||||
versionedImport(packageName, "0.2.3", "1.0.0")
|
||||
def sslConfigCoreSslImport(packageName: String = "com.typesafe.sslconfig.ssl.*") =
|
||||
versionedImport(packageName, "0.2.3", "1.0.0")
|
||||
def sslConfigCoreUtilImport(packageName: String = "com.typesafe.sslconfig.util.*") =
|
||||
versionedImport(packageName, "0.2.3", "1.0.0")
|
||||
def kamonImport(packageName: String = "kamon.sigar.*") =
|
||||
optionalResolution(versionedImport(packageName, "1.6.5", "1.6.6"))
|
||||
def sigarImport(packageName: String = "org.hyperic.*") =
|
||||
optionalResolution(versionedImport(packageName, "1.6.5", "1.6.6"))
|
||||
def optionalResolution(packageName: String) = "%s;resolution:=optional".format(packageName)
|
||||
def versionedImport(packageName: String, lower: String, upper: String) = s"""$packageName;version="[$lower,$upper)""""
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue