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-32567
pull/32585/head
Stephane Nicoll 2 years ago
parent ff6acbe972
commit e94a1f7988

@ -48,6 +48,7 @@ import org.springframework.boot.autoconfigure.jackson.JacksonProperties.Construc
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.boot.jackson.JsonComponentModule;
import org.springframework.boot.jackson.JsonMixinModule; import org.springframework.boot.jackson.JsonMixinModule;
import org.springframework.boot.jackson.JsonMixinModuleEntries;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -95,11 +96,23 @@ public class JacksonAutoConfiguration {
return new JsonComponentModule(); return new JsonComponentModule();
} }
@Bean @Configuration(proxyBeanMethods = false)
public JsonMixinModule jsonMixinModule(ApplicationContext context) { static class JacksonMixinConfiguration {
List<String> packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context)
: Collections.emptyList(); @Bean
return new JsonMixinModule(context, packages); static JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext context) {
List<String> packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context)
: Collections.emptyList();
return JsonMixinModuleEntries.scan(context, packages);
}
@Bean
JsonMixinModule jsonMixinModule(ApplicationContext context, JsonMixinModuleEntries entries) {
JsonMixinModule jsonMixinModule = new JsonMixinModule();
jsonMixinModule.registerEntries(entries, context.getClassLoader());
return jsonMixinModule;
}
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)

@ -54,7 +54,9 @@ import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.jackson.JsonComponent; import org.springframework.boot.jackson.JsonComponent;
import org.springframework.boot.jackson.JsonMixin;
import org.springframework.boot.jackson.JsonMixinModule; import org.springframework.boot.jackson.JsonMixinModule;
import org.springframework.boot.jackson.JsonMixinModuleEntries;
import org.springframework.boot.jackson.JsonObjectSerializer; import org.springframework.boot.jackson.JsonObjectSerializer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -65,6 +67,7 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -96,10 +99,10 @@ class JacksonAutoConfigurationTests {
@Test @Test
void jsonMixinModuleShouldBeAutoConfiguredWithBasePackages() { void jsonMixinModuleShouldBeAutoConfiguredWithBasePackages() {
this.contextRunner.withUserConfiguration(MixinConfiguration.class).run((context) -> { this.contextRunner.withUserConfiguration(MixinConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(JsonMixinModule.class); assertThat(context).hasSingleBean(JsonMixinModule.class).hasSingleBean(JsonMixinModuleEntries.class);
JsonMixinModule module = context.getBean(JsonMixinModule.class); JsonMixinModuleEntries moduleEntries = context.getBean(JsonMixinModuleEntries.class);
assertThat(module).extracting("basePackages", InstanceOfAssertFactories.list(String.class)) assertThat(moduleEntries).extracting("entries", InstanceOfAssertFactories.MAP)
.containsExactly(MixinConfiguration.class.getPackage().getName()); .contains(entry(Person.class, EmptyMixin.class));
}); });
} }
@ -644,6 +647,11 @@ class JacksonAutoConfigurationTests {
} }
@JsonMixin(type = Person.class)
static class EmptyMixin {
}
@AutoConfigurationPackage @AutoConfigurationPackage
static class MixinConfiguration { static class MixinConfiguration {

@ -21,18 +21,8 @@ import java.util.Collection;
import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext; 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.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/** /**
* Spring Bean and Jackson {@link Module} to find and * Spring Bean and Jackson {@link Module} to find and
@ -40,62 +30,35 @@ import org.springframework.util.StringUtils;
* {@link JsonMixin @JsonMixin}-annotated classes. * {@link JsonMixin @JsonMixin}-annotated classes.
* *
* @author Guirong Hu * @author Guirong Hu
* @author Stephane Nicoll
* @since 2.7.0 * @since 2.7.0
* @see JsonMixin * @see JsonMixin
*/ */
public class JsonMixinModule extends SimpleModule implements InitializingBean { public class JsonMixinModule extends SimpleModule {
private final ApplicationContext context; public JsonMixinModule() {
}
private final Collection<String> basePackages;
/** /**
* Create a new {@link JsonMixinModule} instance. * Create a new {@link JsonMixinModule} instance.
* @param context the source application context * @param context the source application context
* @param basePackages the packages to check for annotated classes * @param basePackages the packages to check for annotated classes
* @deprecated since 3.0.0 in favor of
* {@link #registerEntries(JsonMixinModuleEntries, ClassLoader)}
*/ */
@Deprecated(since = "3.0.0", forRemoval = true)
public JsonMixinModule(ApplicationContext context, Collection<String> basePackages) { public JsonMixinModule(ApplicationContext context, Collection<String> basePackages) {
Assert.notNull(context, "Context must not be null"); Assert.notNull(context, "Context must not be null");
this.context = context; registerEntries(JsonMixinModuleEntries.scan(context, basePackages), context.getClassLoader());
this.basePackages = basePackages;
}
@Override
public void afterPropertiesSet() throws Exception {
if (ObjectUtils.isEmpty(this.basePackages)) {
return;
}
JsonMixinComponentScanner scanner = new JsonMixinComponentScanner();
scanner.setEnvironment(this.context.getEnvironment());
scanner.setResourceLoader(this.context);
for (String basePackage : this.basePackages) {
if (StringUtils.hasText(basePackage)) {
for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) {
addJsonMixin(ClassUtils.forName(candidate.getBeanClassName(), this.context.getClassLoader()));
}
}
}
}
private void addJsonMixin(Class<?> mixinClass) {
MergedAnnotation<JsonMixin> annotation = MergedAnnotations
.from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(JsonMixin.class);
for (Class<?> targetType : annotation.getClassArray("type")) {
setMixInAnnotation(targetType, mixinClass);
}
} }
static class JsonMixinComponentScanner extends ClassPathScanningCandidateComponentProvider { /**
* Register the specified {@link JsonMixinModuleEntries entries}.
JsonMixinComponentScanner() { * @param entries the entries to register to this instance
addIncludeFilter(new AnnotationTypeFilter(JsonMixin.class)); * @param classLoader the classloader to use
} */
public void registerEntries(JsonMixinModuleEntries entries, ClassLoader classLoader) {
@Override entries.doWithEntry(classLoader, this::setMixInAnnotation);
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return true;
}
} }
} }

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

@ -13,4 +13,5 @@ org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor,\
org.springframework.boot.jackson.JsonMixinModuleEntriesBeanRegistrationAotProcessor

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

@ -52,6 +52,8 @@ class JsonMixinModuleTests {
} }
@Test @Test
@Deprecated(since = "3.0.0", forRemoval = true)
@SuppressWarnings("removal")
void createWhenContextIsNullShouldThrowException() { void createWhenContextIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new JsonMixinModule(null, Collections.emptyList())) assertThatIllegalArgumentException().isThrownBy(() -> new JsonMixinModule(null, Collections.emptyList()))
.withMessageContaining("Context must not be null"); .withMessageContaining("Context must not be null");
@ -89,12 +91,20 @@ class JsonMixinModuleTests {
private void load(Class<?>... basePackageClasses) { private void load(Class<?>... basePackageClasses) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
List<String> basePackages = Arrays.stream(basePackageClasses).map(ClassUtils::getPackageName).toList(); context.registerBean(JsonMixinModule.class, () -> createJsonMixinModule(context, basePackageClasses));
context.registerBean(JsonMixinModule.class, () -> new JsonMixinModule(context, basePackages));
context.refresh(); context.refresh();
this.context = context; this.context = context;
} }
private JsonMixinModule createJsonMixinModule(AnnotationConfigApplicationContext context,
Class<?>... basePackageClasses) {
List<String> basePackages = Arrays.stream(basePackageClasses).map(ClassUtils::getPackageName).toList();
JsonMixinModuleEntries entries = JsonMixinModuleEntries.scan(context, basePackages);
JsonMixinModule jsonMixinModule = new JsonMixinModule();
jsonMixinModule.registerEntries(entries, context.getClassLoader());
return jsonMixinModule;
}
private void assertMixIn(Module module, Name value, String expectedJson) throws Exception { private void assertMixIn(Module module, Name value, String expectedJson) throws Exception {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module); mapper.registerModule(module);

@ -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…
Cancel
Save