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"
/**
* 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

View File

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

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

View File

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

View File

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