Skip to content

Add symbol processor for configuration metadata that shares the implementation of the annotation processor - #51930

Open
salatmaster wants to merge 4 commits into
spring-projects:mainfrom
salatmaster:gh-28046-ksp-shared-processor
Open

salatmaster wants to merge 4 commits into
spring-projects:mainfrom
salatmaster:gh-28046-ksp-shared-processor

Conversation

@salatmaster

@salatmaster salatmaster commented Oct 1, 2026 •

Copy link
Copy Markdown

This is a second take on KSP support for the configuration processor. It replaces #51375, which was declined because it duplicated the logic of the annotation processor in Kotlin, and it follows the direction suggested there: a thin layer that deals with the Kotlin specifics and delegates to an implementation written in Java that is shared with the annotation processor.

See gh-28046

Approach

The first commit introduces a small language-neutral model in spring-boot-configuration-processor (org.springframework.boot.configurationprocessor.model): TypeDeclaration, MethodDeclaration, VariableDeclaration, TypeReference and AnnotationReference, plus ProcessingContext and ProcessingRound for what the processing tool provides (type lookup, messages, options, resources, annotated declarations).

Everything that decides what ends up in the metadata now works with that model only: PropertyDescriptorResolver, the PropertyDescriptor hierarchy, TypeElementMembers, MetadataGenerationEnvironment, MetadataCollectors and MetadataStore. The orchestration that was in ConfigurationMetadataAnnotationProcessor has moved to ConfigurationMetadataGenerator. The annotation processor is left with adapting javax.lang.model to the model (package-private Java* classes, with TypeUtils and the field values parser behind them) and delegating to the generator. Most of the diff in the existing classes is the substitution of javax.lang.model types with their model counterparts.

The second commit adds spring-boot-configuration-symbol-processor. It only adapts KSP symbols to the model and calls ConfigurationMetadataGenerator; it has no knowledge of what configuration metadata is. A Kotlin class is exposed the way Java sees it: a property is a field with a getter and, when it is mutable, a setter. That keeps the binding rules in one place and makes the output the same as what the annotation processor produces through kapt. What is left in Kotlin is about 400 lines of code: the adapters, the mapping of Kotlin type names to their JVM names, KDoc lookup and nullability.

The annotation processor is unchanged in behavior

  • The existing 259 tests of the module pass. The *MetadataGenerationTests are untouched. The unit tests that build descriptors from javax.lang.model elements now build them from the model, with the same expectations.
  • I compared the metadata generated for every module of Spring Boot before and after the change. The 108 files are identical, apart from spring-boot-session where SessionsEndpoint and ReactiveSessionsEndpoint share the sessions id and the type that the group is attributed to depends on the order in which the two are processed. main gives either result from one build to the next, and both versions give the same result on a clean output directory.

The symbol processor

  • 25 tests that run KSP itself over Kotlin (and Java) sources.
  • With a real Gradle build (KSP plugin 2.3.0, Kotlin 2.3.10), I built the same Kotlin sources once with kapt and the annotation processor and once with the symbol processor. The sample covers constructor and JavaBean binding, @DefaultValue, @Name, nested groups, generics inherited from a superclass, deprecations, an endpoint and a @Bean method. Leaving aside the additional metadata, whose location I had only configured for the KSP build, the two files differ in two places only, both in favor of KSP: the properties of a data class whose constructor parameters all have defaults are described (kapt sees several constructors and describes none), and descriptions are picked up from the @property tags of the class KDoc.
  • Because the logic is shared, things that Add symbol processor for configuration metadata #51375 did not handle now work without any Kotlin code written for them: meta-annotated endpoints, the description cache, and the Java sources of a module that KSP processes.

Limitations

These come from KSP rather than from the design, and are documented in the new appendix section:

  • KSP does not expose initializers or the default values of parameters ([feature request] Retrieve property default value for compile time constants google/ksp#1868), so a default can only be described with @DefaultValue or additional metadata. A parameter that has a Kotlin default gets no default value rather than a wrong one.
  • KSP gives no access to the resources of the module or of its classpath. The location of additional-spring-configuration-metadata.json has to be passed with the existing additionalMetadataLocations option, and the metadata of a @ConfigurationPropertiesSource type of another module cannot be reused.
  • A module has to apply either processor, not both, as they write the same file.

Things I would like your opinion on

  • The model, ProcessingContext, ProcessingRound and ConfigurationMetadataGenerator are public because the symbol processor is a separate artifact. If you would rather not have that API, the KSP adapter could live in spring-boot-configuration-processor itself, at the cost of that jar containing Kotlin classes and compiling against the KSP API.
  • An alternative that I considered and rejected was to implement javax.lang.model on top of KSP so that the annotation processor runs unmodified. It leaves the existing code alone, but a partial implementation of such a large API fails at runtime when the processor starts using a new method, whereas adding a method to the model breaks the compilation of both adapters.
  • Naming (model, Declaration, the Java* prefix of the adapters) is easy to change.

I'm aware that this touches most of the annotation processor, so I'm happy to split it differently or to rework it.

Configuration metadata could previously only be generated from the
javax.lang.model API, which prevented the code that generates it from
being used by anything other than a Java annotation processor.

This commit introduces a small model of the declarations that the
processor inspects, made of types, methods, variables, type references
and annotations. The code that resolves the properties of a type and
that collects, merges and writes the metadata now works with that model
only and has moved from ConfigurationMetadataAnnotationProcessor to
ConfigurationMetadataGenerator. The annotation processor adapts the
javax.lang.model API to the model and delegates to the generator.

The metadata that the annotation processor generates is unchanged.

See spring-projectsgh-28046

Signed-off-by: Areg Iazychian <abstractcoderx@gmail.com>
Add spring-boot-configuration-symbol-processor, a Kotlin Symbol
Processing (KSP) processor that writes the configuration metadata of
Kotlin types without having to rely on kapt.

The processor is a thin layer that adapts the symbols of KSP to the
model of the configuration processor and delegates to the
ConfigurationMetadataGenerator that the annotation processor uses. A
Kotlin class is exposed the way that Java sees it, with each property
being a field with a getter and, when it is mutable, a setter, so that
the same rules apply to both languages and the generated metadata is
the same as when the annotation processor runs through kapt.

KSP does not expose initializers, nor does it give access to the
resources of the module and of its classpath. As a result, default
values can only be described with @DefaultValue, the location of
additional metadata has to be provided with a processor option, and
the metadata of a @ConfigurationPropertiesSource type of another module
cannot be reused.

See spring-projectsgh-28046

Signed-off-by: Areg Iazychian <abstractcoderx@gmail.com>
Add the symbol processor to spring-boot-dependencies so that a project
can declare it without having to specify its version.

See spring-projectsgh-28046

Signed-off-by: Areg Iazychian <abstractcoderx@gmail.com>
Describe how to apply the symbol processor, how Kotlin types are
described, how to contribute additional metadata, and its limitations.
Note that a module has to apply either the annotation processor or the
symbol processor, as both write the same metadata file.

See spring-projectsgh-28046

Signed-off-by: Areg Iazychian <abstractcoderx@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants