mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 07:29:22 +08:00
Add AdapterRegistryGenerator and related functionality for default adapter registration
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen.api
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.STAR
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
|
||||
private const val DEFAULT_ADAPTER_REGISTRY_PACKAGE_NAME_PREFIX = "com.squareup.moshi.generated"
|
||||
private const val DEFAULT_ADAPTER_REGISTRY_CLASS_NAME = "DefaultMoshiAdapterRegistry"
|
||||
|
||||
public data class AdapterRegistration(
|
||||
val targetClassName: ClassName,
|
||||
val adapterClassName: ClassName,
|
||||
val hasTypeParameters: Boolean
|
||||
)
|
||||
|
||||
/** Generates a registry of adapters. */
|
||||
@InternalMoshiCodegenApi
|
||||
public class AdapterRegistryGenerator(
|
||||
private val adapterRegistrations: List<AdapterRegistration>
|
||||
) {
|
||||
|
||||
public fun generate(): FileSpec {
|
||||
// Get the top-level package name of the project (inferred from the shortest package name)
|
||||
val packageName = run {
|
||||
val firstPackage = adapterRegistrations
|
||||
.filter { it.targetClassName.packageName.isNotEmpty() }
|
||||
.minByOrNull { it.targetClassName.packageName.length }
|
||||
?.targetClassName?.packageName
|
||||
|
||||
firstPackage
|
||||
} ?: "default" // fallback package name
|
||||
|
||||
// Generate a package hash of the package name to avoid potential collisions.
|
||||
val packageHash = packageName.hashCode().toUInt().toString(16).take(12)
|
||||
val registryPackageName = "$DEFAULT_ADAPTER_REGISTRY_PACKAGE_NAME_PREFIX.r$packageHash"
|
||||
|
||||
val moshiClass = ClassName("com.squareup.moshi", "Moshi")
|
||||
val adapterRegistryClass = moshiClass.nestedClass("AdapterRegistry")
|
||||
val jsonAdapterClass = ClassName("com.squareup.moshi", "JsonAdapter")
|
||||
val typeClass = ClassName("java.lang.reflect", "Type")
|
||||
|
||||
val adaptersMapBuilder = CodeBlock.builder()
|
||||
adaptersMapBuilder.add("mapOf(")
|
||||
|
||||
val nonGenericAdapters = adapterRegistrations.filter { !it.hasTypeParameters }
|
||||
nonGenericAdapters.forEachIndexed { index, registration ->
|
||||
if (index > 0) adaptersMapBuilder.add(",")
|
||||
adaptersMapBuilder.add(
|
||||
"\n %T::class.java to %T::class.java",
|
||||
registration.targetClassName,
|
||||
registration.adapterClassName
|
||||
)
|
||||
}
|
||||
|
||||
if (nonGenericAdapters.isNotEmpty()) {
|
||||
adaptersMapBuilder.add("\n")
|
||||
}
|
||||
adaptersMapBuilder.add(")")
|
||||
|
||||
val adaptersProperty = PropertySpec.builder(
|
||||
"adapters",
|
||||
Map::class.asClassName().parameterizedBy(
|
||||
typeClass,
|
||||
Class::class.asClassName().parameterizedBy(
|
||||
WildcardTypeName.producerOf(jsonAdapterClass.parameterizedBy(STAR))
|
||||
)
|
||||
)
|
||||
)
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.getter(
|
||||
FunSpec.getterBuilder()
|
||||
.addCode("return %L", adaptersMapBuilder.build())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
val registryClass = TypeSpec.classBuilder(DEFAULT_ADAPTER_REGISTRY_CLASS_NAME)
|
||||
.addSuperinterface(adapterRegistryClass)
|
||||
.addProperty(adaptersProperty)
|
||||
.build()
|
||||
|
||||
val file = FileSpec.builder(registryPackageName, DEFAULT_ADAPTER_REGISTRY_CLASS_NAME)
|
||||
.addFileComment(
|
||||
"""
|
||||
This file is auto generated by Moshi Kotlin CodeGen.
|
||||
**DO NOT EDIT THIS FILE MANUALLY**
|
||||
""".trimIndent()
|
||||
)
|
||||
.addType(registryClass)
|
||||
.build()
|
||||
|
||||
return file
|
||||
}
|
||||
}
|
@@ -37,6 +37,27 @@ public object Options {
|
||||
*/
|
||||
public const val OPTION_GENERATE_PROGUARD_RULES: String = "moshi.generateProguardRules"
|
||||
|
||||
/**
|
||||
* This boolean processing option can enable omission of `-keepnames` rules for target classes.
|
||||
* Moshi's reflective lookup of adapters by class name will fail if obfuscation is applied
|
||||
* to the target class. Only enable this if you are sure you won't be using Moshi's
|
||||
* reflective adapter lookup.
|
||||
* If you set [OPTION_GENERATE_DEFAULT_ADAPTER_REGISTRY] to `true` and decide to
|
||||
* manually set the generated registry, you can also enable this option to
|
||||
* avoid keeping target class names. (LET THEM BE OBFUSCATION, I DON'T WANT THE CLASS NAME TO BE RETAINED!)
|
||||
* This option is automatically invalid if [OPTION_GENERATE_PROGUARD_RULES] is set to `false`.
|
||||
* This is disabled by default for backward compatibility.
|
||||
*/
|
||||
public const val OPTION_PROGUARD_RULES_DONT_KEEP_CLASS_NAMES: String = "moshi.proguardRulesDontKeepClassNames"
|
||||
|
||||
/**
|
||||
* This boolean processing option can enable generation of a default adapter registry that
|
||||
* a compile-time adapter registry that doesn't depend on class names for lookup.
|
||||
* The generated registry content is a class annotated with `@JsonClass(generateAdapter = true)`.
|
||||
* This is disabled by default for backward compatibility.
|
||||
*/
|
||||
public const val OPTION_GENERATE_DEFAULT_ADAPTER_REGISTRY: String = "moshi.generateDefaultAdapterRegistry"
|
||||
|
||||
/**
|
||||
* This boolean processing option controls whether or not Moshi will directly instantiate
|
||||
* JsonQualifier annotations in Kotlin 1.6+. Note that this is enabled by default in Kotlin 1.6
|
||||
|
@@ -45,7 +45,7 @@ public data class ProguardConfig(
|
||||
return "META-INF/proguard/moshi-$canonicalName"
|
||||
}
|
||||
|
||||
public fun writeTo(out: Appendable): Unit = out.run {
|
||||
public fun writeTo(out: Appendable, dontKeepClassNames: Boolean): Unit = out.run {
|
||||
//
|
||||
// -keepnames class {the target class}
|
||||
// -if class {the target class}
|
||||
@@ -56,11 +56,17 @@ public data class ProguardConfig(
|
||||
//
|
||||
val targetName = targetClass.reflectionName()
|
||||
val adapterCanonicalName = ClassName(targetClass.packageName, adapterName).canonicalName
|
||||
// Keep the class name for Moshi's reflective lookup based on it
|
||||
appendLine("-keepnames class $targetName")
|
||||
|
||||
// Keep the class name for Moshi's reflective lookup based on it,
|
||||
// but only if the user hasn't opted out of this.
|
||||
if (!dontKeepClassNames)
|
||||
appendLine("-keepnames class $targetName")
|
||||
|
||||
// Keep the `JsonClass` annotation on the target class, R8 will shrink it away otherwise.
|
||||
appendLine("-keep${if (dontKeepClassNames) ",allowobfuscation" else ""} @com.squareup.moshi.JsonClass class *")
|
||||
|
||||
appendLine("-if class $targetName")
|
||||
appendLine("-keep class $adapterCanonicalName {")
|
||||
appendLine("-${if (dontKeepClassNames) "keepclassmembers" else "keep"} class $adapterCanonicalName {")
|
||||
// Keep the constructor for Moshi's reflective lookup
|
||||
val constructorArgs = adapterConstructorParams.joinToString(",")
|
||||
appendLine(" public <init>($constructorArgs);")
|
||||
|
@@ -24,16 +24,22 @@ import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSFile
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
|
||||
import com.squareup.kotlinpoet.ksp.toClassName
|
||||
import com.squareup.kotlinpoet.ksp.writeTo
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator
|
||||
import com.squareup.moshi.kotlin.codegen.api.AdapterRegistration
|
||||
import com.squareup.moshi.kotlin.codegen.api.AdapterRegistryGenerator
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_DEFAULT_ADAPTER_REGISTRY
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_PROGUARD_RULES_DONT_KEEP_CLASS_NAMES
|
||||
import com.squareup.moshi.kotlin.codegen.api.Options.POSSIBLE_GENERATED_NAMES
|
||||
import com.squareup.moshi.kotlin.codegen.api.ProguardConfig
|
||||
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
|
||||
@@ -63,6 +69,10 @@ private class JsonClassSymbolProcessor(
|
||||
}
|
||||
}
|
||||
private val generateProguardRules = environment.options[OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true
|
||||
private val proguardRulesDontKeepClassNames = environment.options[OPTION_PROGUARD_RULES_DONT_KEEP_CLASS_NAMES]?.toBooleanStrictOrNull() ?: false
|
||||
private val generateDefaultAdapterRegistry = environment.options[OPTION_GENERATE_DEFAULT_ADAPTER_REGISTRY]?.toBooleanStrictOrNull() ?: false
|
||||
|
||||
private val adapterRegistrations = mutableListOf<AdapterRegistration>()
|
||||
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val generatedAnnotation = generatedOption?.let {
|
||||
@@ -100,13 +110,40 @@ private class JsonClassSymbolProcessor(
|
||||
.build()
|
||||
}
|
||||
preparedAdapter.spec.writeTo(codeGenerator, aggregating = false)
|
||||
preparedAdapter.proguardConfig?.writeTo(codeGenerator, originatingFile)
|
||||
preparedAdapter.proguardConfig?.writeTo(codeGenerator, originatingFile, proguardRulesDontKeepClassNames)
|
||||
|
||||
// Collect adapter registration information.
|
||||
if (generateDefaultAdapterRegistry && type is KSClassDeclaration) {
|
||||
val targetClassName = type.toClassName()
|
||||
val adapterClassName = ClassName(
|
||||
targetClassName.packageName,
|
||||
"${targetClassName.simpleNames.joinToString("_")}JsonAdapter"
|
||||
)
|
||||
val hasTypeParameters = type.typeParameters.isNotEmpty()
|
||||
|
||||
adapterRegistrations.add(
|
||||
AdapterRegistration(
|
||||
targetClassName = targetClassName,
|
||||
adapterClassName = adapterClassName,
|
||||
hasTypeParameters = hasTypeParameters
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error(
|
||||
"Error preparing ${type.simpleName.asString()}: ${e.stackTrace.joinToString("\n")}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (adapterRegistrations.isNotEmpty()) {
|
||||
// FileAlreadyExistsException can happen if multiple compilations.
|
||||
runCatching {
|
||||
val adapterRegistryGenerator = adapterRegistryGenerator()
|
||||
adapterRegistryGenerator.generate().writeTo(codeGenerator, aggregating = false)
|
||||
}
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@@ -144,10 +181,12 @@ private class JsonClassSymbolProcessor(
|
||||
|
||||
return AdapterGenerator(type, sortedProperties)
|
||||
}
|
||||
|
||||
private fun adapterRegistryGenerator() = AdapterRegistryGenerator(adapterRegistrations)
|
||||
}
|
||||
|
||||
/** Writes this config to a [codeGenerator]. */
|
||||
private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator, originatingKSFile: KSFile) {
|
||||
private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator, originatingKSFile: KSFile, dontKeepClassNames: Boolean) {
|
||||
val file = codeGenerator.createNewFile(
|
||||
dependencies = Dependencies(aggregating = false, originatingKSFile),
|
||||
packageName = "",
|
||||
@@ -156,5 +195,5 @@ private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator, originatingKSFi
|
||||
)
|
||||
// Don't use writeTo(file) because that tries to handle directories under the hood
|
||||
OutputStreamWriter(file, StandardCharsets.UTF_8)
|
||||
.use(::writeTo)
|
||||
.use { writeTo(it, dontKeepClassNames) }
|
||||
}
|
||||
|
@@ -45,6 +45,8 @@ public class Moshi internal constructor(builder: Builder) {
|
||||
private val lookupChainThreadLocal = ThreadLocal<LookupChain>()
|
||||
private val adapterCache = LinkedHashMap<Any?, JsonAdapter<*>?>()
|
||||
|
||||
internal val registry = builder.registry
|
||||
|
||||
/** Returns a JSON adapter for `type`, creating it if necessary. */
|
||||
@CheckReturnValue
|
||||
public fun <T> adapter(type: Type): JsonAdapter<T> = adapter(type, NO_ANNOTATIONS)
|
||||
@@ -191,6 +193,7 @@ public class Moshi internal constructor(builder: Builder) {
|
||||
public class Builder {
|
||||
internal val factories = mutableListOf<JsonAdapter.Factory>()
|
||||
internal var lastOffset = 0
|
||||
internal var registry: AdapterRegistry? = null
|
||||
|
||||
@CheckReturnValue
|
||||
@ExperimentalStdlibApi
|
||||
@@ -239,10 +242,42 @@ public class Moshi internal constructor(builder: Builder) {
|
||||
addLast(AdapterMethodsFactory(adapter))
|
||||
}
|
||||
|
||||
public fun setRegistry(adapterRegistry: AdapterRegistry): Builder = apply {
|
||||
this.registry = adapterRegistry
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public fun build(): Moshi = Moshi(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry of [JsonAdapter]s to be registered with a [Moshi] instance.
|
||||
*
|
||||
* To use, create a subclass and implement [adapters]. Then set an instance of your subclass on
|
||||
* [Moshi.Builder.setRegistry].
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* class MyAdapterRegistry : Moshi.AdapterRegistry {
|
||||
* override val adapters = mapOf(
|
||||
* MyType::class.java to MyTypeJsonAdapter::class.java,
|
||||
* Types.newParameterizedType(List::class.java, String::class.java) to MyStringListJsonAdapter::class.java
|
||||
* )
|
||||
* }
|
||||
*
|
||||
* val myRegistry = MyAdapterRegistry()
|
||||
*
|
||||
* val builder = Moshi.Builder().setRegistry(myRegistry)
|
||||
* val moshi = builder.build()
|
||||
* ```
|
||||
*/
|
||||
public interface AdapterRegistry {
|
||||
/**
|
||||
* A map of [Type] to [JsonAdapter] to be registered.
|
||||
*/
|
||||
public val adapters: Map<Type, Class<out JsonAdapter<*>>>
|
||||
}
|
||||
|
||||
/**
|
||||
* A possibly-reentrant chain of lookups for JSON adapters.
|
||||
*
|
||||
|
@@ -378,12 +378,24 @@ public fun Moshi.generatedAdapter(
|
||||
if (jsonClass == null || !jsonClass.generateAdapter) {
|
||||
return null
|
||||
}
|
||||
val adapterClassName = Types.generatedJsonAdapterName(rawType.name)
|
||||
|
||||
val adapters = registry?.adapters
|
||||
|
||||
val rawAdapterClassName = Types.generatedJsonAdapterName(rawType.name)
|
||||
var possiblyFoundAdapter: Class<out JsonAdapter<*>>? = null
|
||||
|
||||
return try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val adapterClass = Class.forName(adapterClassName, true, rawType.classLoader) as Class<out JsonAdapter<*>>
|
||||
val adapterClass = adapters?.firstNotNullOfOrNull { (regType, regAdapter) ->
|
||||
if (typesMatch(rawType, regType)) {
|
||||
regAdapter
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: Class.forName(rawAdapterClassName, true, rawType.classLoader) as? Class<out JsonAdapter<*>>?
|
||||
?: throw ClassNotFoundException()
|
||||
possiblyFoundAdapter = adapterClass
|
||||
|
||||
var constructor: Constructor<out JsonAdapter<*>>
|
||||
var args: Array<Any>
|
||||
if (type is ParameterizedType) {
|
||||
|
Reference in New Issue
Block a user