Browse Source
This commit adds an AOT contribution that replaces the scanning of @JsonMixin by a mapping in generated code. This makes sure that such components are found in a native image. Closes gh-32567pull/32585/head
9 changed files with 503 additions and 64 deletions
@ -0,0 +1,164 @@
@@ -0,0 +1,164 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or authors. |
||||
* |
||||
* 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 org.springframework.boot.jackson; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import java.util.function.BiConsumer; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; |
||||
import org.springframework.beans.factory.config.BeanDefinition; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; |
||||
import org.springframework.core.annotation.MergedAnnotation; |
||||
import org.springframework.core.annotation.MergedAnnotations; |
||||
import org.springframework.core.type.filter.AnnotationTypeFilter; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Provide the mapping of json mixin class to consider. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @since 3.0.0 |
||||
*/ |
||||
public final class JsonMixinModuleEntries { |
||||
|
||||
private final Map<Object, Object> entries; |
||||
|
||||
private JsonMixinModuleEntries(Builder builder) { |
||||
this.entries = new LinkedHashMap<>(builder.entries); |
||||
} |
||||
|
||||
/** |
||||
* Create an instance using the specified {@link Builder}. |
||||
* @param mixins a consumer of the builder |
||||
* @return an instance with the state of the customized builder. |
||||
*/ |
||||
public static JsonMixinModuleEntries create(Consumer<Builder> mixins) { |
||||
Builder builder = new Builder(); |
||||
mixins.accept(builder); |
||||
return builder.build(); |
||||
} |
||||
|
||||
/** |
||||
* Scan the classpath for {@link JsonMixin @JsonMixin} in the specified |
||||
* {@code basePackages}. |
||||
* @param context the application context to use |
||||
* @param basePackages the base packages to consider |
||||
* @return an instance with the result of the scanning |
||||
*/ |
||||
public static JsonMixinModuleEntries scan(ApplicationContext context, Collection<String> basePackages) { |
||||
return JsonMixinModuleEntries.create((builder) -> { |
||||
if (ObjectUtils.isEmpty(basePackages)) { |
||||
return; |
||||
} |
||||
JsonMixinComponentScanner scanner = new JsonMixinComponentScanner(); |
||||
scanner.setEnvironment(context.getEnvironment()); |
||||
scanner.setResourceLoader(context); |
||||
for (String basePackage : basePackages) { |
||||
if (StringUtils.hasText(basePackage)) { |
||||
for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) { |
||||
Class<?> mixinClass = ClassUtils.resolveClassName(candidate.getBeanClassName(), |
||||
context.getClassLoader()); |
||||
registerMixinClass(builder, mixinClass); |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private static void registerMixinClass(Builder builder, Class<?> mixinClass) { |
||||
MergedAnnotation<JsonMixin> annotation = MergedAnnotations |
||||
.from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(JsonMixin.class); |
||||
for (Class<?> targetType : annotation.getClassArray("type")) { |
||||
builder.and(targetType, mixinClass); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Perform an action on each entry defined by this instance. If a class needs to be |
||||
* resolved from its class name, the specified {@link ClassLoader} is used. |
||||
* @param classLoader the classloader to use to resolve class name if necessary |
||||
* @param action the action to invoke on each type to mixin class entry |
||||
*/ |
||||
public void doWithEntry(ClassLoader classLoader, BiConsumer<Class<?>, Class<?>> action) { |
||||
this.entries.forEach((type, mixin) -> action.accept(resolveClassNameIfNecessary(type, classLoader), |
||||
resolveClassNameIfNecessary(mixin, classLoader))); |
||||
} |
||||
|
||||
private Class<?> resolveClassNameIfNecessary(Object type, ClassLoader classLoader) { |
||||
return (type instanceof Class<?> clazz) ? clazz : ClassUtils.resolveClassName((String) type, classLoader); |
||||
} |
||||
|
||||
/** |
||||
* Builder for {@link JsonMixinModuleEntries}. |
||||
*/ |
||||
public static class Builder { |
||||
|
||||
private final Map<Object, Object> entries; |
||||
|
||||
Builder() { |
||||
this.entries = new LinkedHashMap<>(); |
||||
} |
||||
|
||||
/** |
||||
* Add a mapping for the specified class names. |
||||
* @param typeClassName the type class name |
||||
* @param mixinClassName the mixin class name |
||||
* @return {@code this}, to facilitate method chaining |
||||
*/ |
||||
public Builder and(String typeClassName, String mixinClassName) { |
||||
this.entries.put(typeClassName, mixinClassName); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Add a mapping for the specified classes. |
||||
* @param type the type class
|
||||
* @param mixinClass the mixin class
|
||||
* @return {@code this}, to facilitate method chaining |
||||
*/ |
||||
public Builder and(Class<?> type, Class<?> mixinClass) { |
||||
this.entries.put(type, mixinClass); |
||||
return this; |
||||
} |
||||
|
||||
JsonMixinModuleEntries build() { |
||||
return new JsonMixinModuleEntries(this); |
||||
} |
||||
|
||||
} |
||||
|
||||
static class JsonMixinComponentScanner extends ClassPathScanningCandidateComponentProvider { |
||||
|
||||
JsonMixinComponentScanner() { |
||||
addIncludeFilter(new AnnotationTypeFilter(JsonMixin.class)); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { |
||||
return true; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or authors. |
||||
* |
||||
* 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 org.springframework.boot.jackson; |
||||
|
||||
import java.lang.reflect.Executable; |
||||
import java.util.LinkedHashSet; |
||||
import java.util.Set; |
||||
|
||||
import javax.lang.model.element.Modifier; |
||||
|
||||
import org.springframework.aot.generate.AccessControl; |
||||
import org.springframework.aot.generate.GeneratedMethod; |
||||
import org.springframework.aot.generate.GenerationContext; |
||||
import org.springframework.aot.hint.BindingReflectionHintsRegistrar; |
||||
import org.springframework.aot.hint.RuntimeHints; |
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; |
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; |
||||
import org.springframework.beans.factory.aot.BeanRegistrationCode; |
||||
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; |
||||
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; |
||||
import org.springframework.beans.factory.support.RegisteredBean; |
||||
import org.springframework.javapoet.CodeBlock; |
||||
|
||||
/** |
||||
* {@link BeanRegistrationAotProcessor} that replaces any {@link JsonMixinModuleEntries} |
||||
* by an hard-coded equivalent. This has the effect of disabling scanning at runtime. |
||||
* |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
class JsonMixinModuleEntriesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { |
||||
|
||||
@Override |
||||
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { |
||||
if (registeredBean.getBeanClass().equals(JsonMixinModuleEntries.class)) { |
||||
return BeanRegistrationAotContribution |
||||
.withCustomCodeFragments((codeFragments) -> new AotContribution(codeFragments, registeredBean)); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
static class AotContribution extends BeanRegistrationCodeFragmentsDecorator { |
||||
|
||||
private final RegisteredBean registeredBean; |
||||
|
||||
private final ClassLoader classLoader; |
||||
|
||||
AotContribution(BeanRegistrationCodeFragments delegate, RegisteredBean registeredBean) { |
||||
super(delegate); |
||||
this.registeredBean = registeredBean; |
||||
this.classLoader = registeredBean.getBeanFactory().getBeanClassLoader(); |
||||
} |
||||
|
||||
@Override |
||||
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, |
||||
BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, |
||||
boolean allowDirectSupplierShortcut) { |
||||
JsonMixinModuleEntries entries = this.registeredBean.getBeanFactory() |
||||
.getBean(this.registeredBean.getBeanName(), JsonMixinModuleEntries.class); |
||||
contributeHints(generationContext.getRuntimeHints(), entries); |
||||
GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("getInstance", (method) -> { |
||||
Class<?> beanType = JsonMixinModuleEntries.class; |
||||
method.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName()); |
||||
method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); |
||||
method.returns(beanType); |
||||
CodeBlock.Builder code = CodeBlock.builder(); |
||||
code.add("return $T.create(", JsonMixinModuleEntries.class).beginControlFlow("(mixins) ->"); |
||||
entries.doWithEntry(this.classLoader, (type, mixin) -> addEntryCode(code, type, mixin)); |
||||
code.endControlFlow(")"); |
||||
method.addCode(code.build()); |
||||
}); |
||||
return generatedMethod.toMethodReference().toCodeBlock(); |
||||
} |
||||
|
||||
private void addEntryCode(CodeBlock.Builder code, Class<?> type, Class<?> mixin) { |
||||
AccessControl accessForTypes = AccessControl.lowest(AccessControl.forClass(type), |
||||
AccessControl.forClass(mixin)); |
||||
if (accessForTypes.isPublic()) { |
||||
code.addStatement("$L.and($T.class, $T.class)", "mixins", type, mixin); |
||||
} |
||||
else { |
||||
code.addStatement("$L.and($S, $S)", "mixins", type.getName(), mixin.getName()); |
||||
} |
||||
} |
||||
|
||||
private void contributeHints(RuntimeHints runtimeHints, JsonMixinModuleEntries entries) { |
||||
Set<Class<?>> mixins = new LinkedHashSet<>(); |
||||
entries.doWithEntry(this.classLoader, (type, mixin) -> mixins.add(type)); |
||||
new BindingReflectionHintsRegistrar().registerReflectionHints(runtimeHints.reflection(), |
||||
mixins.toArray(Class<?>[]::new)); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or authors. |
||||
* |
||||
* 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 org.springframework.boot.jackson; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
import java.util.function.BiConsumer; |
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.aot.hint.RuntimeHints; |
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; |
||||
import org.springframework.aot.test.generate.TestGenerationContext; |
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder; |
||||
import org.springframework.boot.jackson.scan.a.RenameMixInClass; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ApplicationContextInitializer; |
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.aot.ApplicationContextAotGenerator; |
||||
import org.springframework.context.support.GenericApplicationContext; |
||||
import org.springframework.core.test.tools.CompileWithForkedClassLoader; |
||||
import org.springframework.core.test.tools.Compiled; |
||||
import org.springframework.core.test.tools.TestCompiler; |
||||
import org.springframework.javapoet.ClassName; |
||||
import org.springframework.util.ClassUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.entry; |
||||
|
||||
/** |
||||
* Tests for {@link JsonMixinModuleEntriesBeanRegistrationAotProcessor}. |
||||
* |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
@CompileWithForkedClassLoader |
||||
class JsonMixinModuleEntriesBeanRegistrationAotProcessorTests { |
||||
|
||||
private final TestGenerationContext generationContext = new TestGenerationContext(); |
||||
|
||||
private final GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); |
||||
|
||||
@Test |
||||
void processAheadOfTimeShouldRegisterBindingHintsForMixins() { |
||||
registerEntries(RenameMixInClass.class); |
||||
processAheadOfTime(); |
||||
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); |
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(Name.class, "getName")).accepts(runtimeHints); |
||||
assertThat(RuntimeHintsPredicates.reflection().onMethod(NameAndAge.class, "getAge")).accepts(runtimeHints); |
||||
} |
||||
|
||||
@Test |
||||
void processAheadOfTimeWhenPublicClassShouldRegisterClass() { |
||||
registerEntries(RenameMixInClass.class); |
||||
compile((freshContext, compiled) -> { |
||||
assertThat(freshContext.getBean(TestConfiguration.class).scanningInvoked).isFalse(); |
||||
JsonMixinModuleEntries jsonMixinModuleEntries = freshContext.getBean(JsonMixinModuleEntries.class); |
||||
assertThat(jsonMixinModuleEntries).extracting("entries", InstanceOfAssertFactories.MAP).containsExactly( |
||||
entry(Name.class, RenameMixInClass.class), entry(NameAndAge.class, RenameMixInClass.class)); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void processAheadOfTimeWhenNonAccessibleClassShouldRegisterClassName() { |
||||
Class<?> privateMixinClass = ClassUtils |
||||
.resolveClassName("org.springframework.boot.jackson.scan.e.PrivateMixInClass", null); |
||||
registerEntries(privateMixinClass); |
||||
compile((freshContext, compiled) -> { |
||||
assertThat(freshContext.getBean(TestConfiguration.class).scanningInvoked).isFalse(); |
||||
JsonMixinModuleEntries jsonMixinModuleEntries = freshContext.getBean(JsonMixinModuleEntries.class); |
||||
assertThat(jsonMixinModuleEntries).extracting("entries", InstanceOfAssertFactories.MAP).containsExactly( |
||||
entry(Name.class.getName(), privateMixinClass.getName()), |
||||
entry(NameAndAge.class.getName(), privateMixinClass.getName())); |
||||
}); |
||||
} |
||||
|
||||
private ClassName processAheadOfTime() { |
||||
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext, |
||||
this.generationContext); |
||||
this.generationContext.writeGeneratedContent(); |
||||
return className; |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private void compile(BiConsumer<GenericApplicationContext, Compiled> result) { |
||||
ClassName className = processAheadOfTime(); |
||||
TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> { |
||||
GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); |
||||
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled |
||||
.getInstance(ApplicationContextInitializer.class, className.toString()); |
||||
initializer.initialize(freshApplicationContext); |
||||
freshApplicationContext.refresh(); |
||||
result.accept(freshApplicationContext, compiled); |
||||
}); |
||||
} |
||||
|
||||
private void registerEntries(Class<?>... basePackageClasses) { |
||||
List<String> packageNames = Arrays.stream(basePackageClasses).map(Class::getPackageName).toList(); |
||||
this.applicationContext.registerBeanDefinition("configuration", BeanDefinitionBuilder |
||||
.rootBeanDefinition(TestConfiguration.class).addConstructorArgValue(packageNames).getBeanDefinition()); |
||||
} |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
static class TestConfiguration { |
||||
|
||||
public boolean scanningInvoked; |
||||
|
||||
private final Collection<String> packageNames; |
||||
|
||||
TestConfiguration(Collection<String> packageNames) { |
||||
this.packageNames = packageNames; |
||||
} |
||||
|
||||
@Bean |
||||
JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext applicationContext) { |
||||
this.scanningInvoked = true; |
||||
return JsonMixinModuleEntries.scan(applicationContext, this.packageNames); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or authors. |
||||
* |
||||
* 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 org.springframework.boot.jackson.scan.e; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
||||
import org.springframework.boot.jackson.JsonMixin; |
||||
import org.springframework.boot.jackson.Name; |
||||
import org.springframework.boot.jackson.NameAndAge; |
||||
|
||||
@JsonMixin(type = { Name.class, NameAndAge.class }) |
||||
class PrivateMixInClass { |
||||
|
||||
@JsonProperty("username") |
||||
String getName() { |
||||
return null; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue