Jackson serializer as replacement for Java serialization, #24155

* Copied from Lagom, with the following differences
  * Jsonable and CompressedJsonable not included
  * pcollection and guava modules not enabled by default
  * added scala and afterburner modules
* JSON, CBOR and Smile options (different serializers)
* JMH benchmark
* jackson version 2.9.9
* test polymorphism
* serializer for ActorRef
* Address serializer
* FiniteDuration serializer, same as java.time.Duration
* use blacklist from Jackson databind against gadgets
* disallow binding to open ended types, such as java.io.Serializable
* Configurable ObjectMapper ser/deser features
* testing date formats with WRITE_DATES_AS_TIMESTAMPS on/off
* ActorSystemSetup for ObjectMapper creation
  * and possibility to lookup created ObjectMapper via ObjectMapperProvider extension
* createObjectMapper without ActorSystem, needed by Lagom test
* add basic docs
* skip Scala 2.13 for akka-serialization-jackson for now, until the Jackson
  Scala module has been released
This commit is contained in:
Patrik Nordwall 2018-02-11 19:56:52 +01:00
parent dd6924465b
commit 6122966fca
34 changed files with 3049 additions and 103 deletions

View file

@ -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)
}
}

View file

@ -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)

View 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.

View 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
}
}

View file

@ -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]
}
}

View file

@ -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

View file

@ -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]
}
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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)""""
}