Replace outcome of JsonMixins scanning in AOT optimized contexts
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
parent
ff6acbe972
commit
e94a1f7988
@ -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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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