Add AdapterRegistryGenerator and related functionality for default adapter registration

This commit is contained in:
2025-10-01 02:16:56 +08:00
parent 5b313070fe
commit 81d9b31f6c
6 changed files with 239 additions and 9 deletions

View File

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

View File

@@ -37,6 +37,27 @@ public object Options {
*/ */
public const val OPTION_GENERATE_PROGUARD_RULES: String = "moshi.generateProguardRules" 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 * 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 * JsonQualifier annotations in Kotlin 1.6+. Note that this is enabled by default in Kotlin 1.6

View File

@@ -45,7 +45,7 @@ public data class ProguardConfig(
return "META-INF/proguard/moshi-$canonicalName" 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} // -keepnames class {the target class}
// -if class {the target class} // -if class {the target class}
@@ -56,11 +56,17 @@ public data class ProguardConfig(
// //
val targetName = targetClass.reflectionName() val targetName = targetClass.reflectionName()
val adapterCanonicalName = ClassName(targetClass.packageName, adapterName).canonicalName 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("-if class $targetName")
appendLine("-keep class $adapterCanonicalName {") appendLine("-${if (dontKeepClassNames) "keepclassmembers" else "keep"} class $adapterCanonicalName {")
// Keep the constructor for Moshi's reflective lookup // Keep the constructor for Moshi's reflective lookup
val constructorArgs = adapterConstructorParams.joinToString(",") val constructorArgs = adapterConstructorParams.joinToString(",")
appendLine(" public <init>($constructorArgs);") appendLine(" public <init>($constructorArgs);")

View File

@@ -24,16 +24,22 @@ import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated 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.KSDeclaration
import com.google.devtools.ksp.symbol.KSFile import com.google.devtools.ksp.symbol.KSFile
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ksp.addOriginatingKSFile import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.kotlinpoet.ksp.writeTo
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator 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_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_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.Options.POSSIBLE_GENERATED_NAMES
import com.squareup.moshi.kotlin.codegen.api.ProguardConfig import com.squareup.moshi.kotlin.codegen.api.ProguardConfig
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator 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 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> { override fun process(resolver: Resolver): List<KSAnnotated> {
val generatedAnnotation = generatedOption?.let { val generatedAnnotation = generatedOption?.let {
@@ -100,13 +110,40 @@ private class JsonClassSymbolProcessor(
.build() .build()
} }
preparedAdapter.spec.writeTo(codeGenerator, aggregating = false) 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) { } catch (e: Exception) {
logger.error( logger.error(
"Error preparing ${type.simpleName.asString()}: ${e.stackTrace.joinToString("\n")}", "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() return emptyList()
} }
@@ -144,10 +181,12 @@ private class JsonClassSymbolProcessor(
return AdapterGenerator(type, sortedProperties) return AdapterGenerator(type, sortedProperties)
} }
private fun adapterRegistryGenerator() = AdapterRegistryGenerator(adapterRegistrations)
} }
/** Writes this config to a [codeGenerator]. */ /** 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( val file = codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = false, originatingKSFile), dependencies = Dependencies(aggregating = false, originatingKSFile),
packageName = "", 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 // Don't use writeTo(file) because that tries to handle directories under the hood
OutputStreamWriter(file, StandardCharsets.UTF_8) OutputStreamWriter(file, StandardCharsets.UTF_8)
.use(::writeTo) .use { writeTo(it, dontKeepClassNames) }
} }

View File

@@ -45,6 +45,8 @@ public class Moshi internal constructor(builder: Builder) {
private val lookupChainThreadLocal = ThreadLocal<LookupChain>() private val lookupChainThreadLocal = ThreadLocal<LookupChain>()
private val adapterCache = LinkedHashMap<Any?, JsonAdapter<*>?>() private val adapterCache = LinkedHashMap<Any?, JsonAdapter<*>?>()
internal val registry = builder.registry
/** Returns a JSON adapter for `type`, creating it if necessary. */ /** Returns a JSON adapter for `type`, creating it if necessary. */
@CheckReturnValue @CheckReturnValue
public fun <T> adapter(type: Type): JsonAdapter<T> = adapter(type, NO_ANNOTATIONS) 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 { public class Builder {
internal val factories = mutableListOf<JsonAdapter.Factory>() internal val factories = mutableListOf<JsonAdapter.Factory>()
internal var lastOffset = 0 internal var lastOffset = 0
internal var registry: AdapterRegistry? = null
@CheckReturnValue @CheckReturnValue
@ExperimentalStdlibApi @ExperimentalStdlibApi
@@ -239,10 +242,42 @@ public class Moshi internal constructor(builder: Builder) {
addLast(AdapterMethodsFactory(adapter)) addLast(AdapterMethodsFactory(adapter))
} }
public fun setRegistry(adapterRegistry: AdapterRegistry): Builder = apply {
this.registry = adapterRegistry
}
@CheckReturnValue @CheckReturnValue
public fun build(): Moshi = Moshi(this) 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. * A possibly-reentrant chain of lookups for JSON adapters.
* *

View File

@@ -378,12 +378,24 @@ public fun Moshi.generatedAdapter(
if (jsonClass == null || !jsonClass.generateAdapter) { if (jsonClass == null || !jsonClass.generateAdapter) {
return null return null
} }
val adapterClassName = Types.generatedJsonAdapterName(rawType.name)
val adapters = registry?.adapters
val rawAdapterClassName = Types.generatedJsonAdapterName(rawType.name)
var possiblyFoundAdapter: Class<out JsonAdapter<*>>? = null var possiblyFoundAdapter: Class<out JsonAdapter<*>>? = null
return try { return try {
@Suppress("UNCHECKED_CAST") @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 possiblyFoundAdapter = adapterClass
var constructor: Constructor<out JsonAdapter<*>> var constructor: Constructor<out JsonAdapter<*>>
var args: Array<Any> var args: Array<Any>
if (type is ParameterizedType) { if (type is ParameterizedType) {