pekko/akka-serialization-jackson/src/main/scala/akka/serialization/jackson/JacksonObjectMapperProvider.scala
Patrik Nordwall 93017d05c7 serializer for akka.actor.typed.ActorRef
* most convenient for users to include it akka-serialization-jackson
  and load it when akka-actor-typed is in classpath
* provided dependency to akka-actor-typed
2019-05-30 15:08:48 +02:00

295 lines
12 KiB
Scala

/*
* 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
if (isModuleEnabled(fqcn, dynamicAccess)) {
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
}
} else
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 isModuleEnabled(fqcn: String, dynamicAccess: DynamicAccess): Boolean = {
// akka-actor-typed dependency is "provided" and may not be included
if (fqcn == "akka.serialization.jackson.AkkaTypedJacksonModule") {
dynamicAccess.getClassFor("akka.actor.typed.ActorRef") match {
case Failure(_: ClassNotFoundException | _: NoClassDefFoundError) =>
false // akka-actor-typed not in classpath
case _ =>
true
}
} else
true
}
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
}