diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index ca87ade02c..99f0f7e1df 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -48,6 +48,7 @@ import org.springframework.boot.autoconfigure.jackson.JacksonProperties.Construc import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.boot.jackson.JsonMixinModule; +import org.springframework.boot.jackson.JsonMixinModuleEntries; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -95,11 +96,23 @@ public class JacksonAutoConfiguration { return new JsonComponentModule(); } - @Bean - public JsonMixinModule jsonMixinModule(ApplicationContext context) { - List packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context) - : Collections.emptyList(); - return new JsonMixinModule(context, packages); + @Configuration(proxyBeanMethods = false) + static class JacksonMixinConfiguration { + + @Bean + static JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext context) { + List 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) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 894583bb14..75831041be 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -54,7 +54,9 @@ import org.springframework.boot.autoconfigure.AutoConfigurationPackage; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.jackson.JsonComponent; +import org.springframework.boot.jackson.JsonMixin; import org.springframework.boot.jackson.JsonMixinModule; +import org.springframework.boot.jackson.JsonMixinModuleEntries; import org.springframework.boot.jackson.JsonObjectSerializer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; 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.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; /** @@ -96,10 +99,10 @@ class JacksonAutoConfigurationTests { @Test void jsonMixinModuleShouldBeAutoConfiguredWithBasePackages() { this.contextRunner.withUserConfiguration(MixinConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(JsonMixinModule.class); - JsonMixinModule module = context.getBean(JsonMixinModule.class); - assertThat(module).extracting("basePackages", InstanceOfAssertFactories.list(String.class)) - .containsExactly(MixinConfiguration.class.getPackage().getName()); + assertThat(context).hasSingleBean(JsonMixinModule.class).hasSingleBean(JsonMixinModuleEntries.class); + JsonMixinModuleEntries moduleEntries = context.getBean(JsonMixinModuleEntries.class); + assertThat(moduleEntries).extracting("entries", InstanceOfAssertFactories.MAP) + .contains(entry(Person.class, EmptyMixin.class)); }); } @@ -644,6 +647,11 @@ class JacksonAutoConfigurationTests { } + @JsonMixin(type = Person.class) + static class EmptyMixin { + + } + @AutoConfigurationPackage static class MixinConfiguration { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java index 2285683384..21ba8a633d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java @@ -21,18 +21,8 @@ import java.util.Collection; import com.fasterxml.jackson.databind.Module; 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.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.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** * Spring Bean and Jackson {@link Module} to find and @@ -40,62 +30,35 @@ import org.springframework.util.StringUtils; * {@link JsonMixin @JsonMixin}-annotated classes. * * @author Guirong Hu + * @author Stephane Nicoll * @since 2.7.0 * @see JsonMixin */ -public class JsonMixinModule extends SimpleModule implements InitializingBean { +public class JsonMixinModule extends SimpleModule { - private final ApplicationContext context; - - private final Collection basePackages; + public JsonMixinModule() { + } /** * Create a new {@link JsonMixinModule} instance. * @param context the source application context * @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 basePackages) { Assert.notNull(context, "Context must not be null"); - this.context = context; - 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 annotation = MergedAnnotations - .from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(JsonMixin.class); - for (Class targetType : annotation.getClassArray("type")) { - setMixInAnnotation(targetType, mixinClass); - } + registerEntries(JsonMixinModuleEntries.scan(context, basePackages), context.getClassLoader()); } - static class JsonMixinComponentScanner extends ClassPathScanningCandidateComponentProvider { - - JsonMixinComponentScanner() { - addIncludeFilter(new AnnotationTypeFilter(JsonMixin.class)); - } - - @Override - protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { - return true; - } - + /** + * Register the specified {@link JsonMixinModuleEntries entries}. + * @param entries the entries to register to this instance + * @param classLoader the classloader to use + */ + public void registerEntries(JsonMixinModuleEntries entries, ClassLoader classLoader) { + entries.doWithEntry(classLoader, this::setMixInAnnotation); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntries.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntries.java new file mode 100644 index 0000000000..8c044c4a78 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntries.java @@ -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 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 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 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 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> 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 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; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessor.java new file mode 100644 index 0000000000..6fa0d2e499 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessor.java @@ -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> mixins = new LinkedHashSet<>(); + entries.doWithEntry(this.classLoader, (type, mixin) -> mixins.add(type)); + new BindingReflectionHintsRegistrar().registerReflectionHints(runtimeHints.reflection(), + mixins.toArray(Class[]::new)); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories index d60b07ea90..24d607cf90 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories @@ -13,4 +13,5 @@ org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ -org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor +org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor,\ +org.springframework.boot.jackson.JsonMixinModuleEntriesBeanRegistrationAotProcessor diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessorTests.java new file mode 100644 index 0000000000..7188944221 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessorTests.java @@ -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 result) { + ClassName className = processAheadOfTime(); + TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> { + GenericApplicationContext freshApplicationContext = new GenericApplicationContext(); + ApplicationContextInitializer initializer = compiled + .getInstance(ApplicationContextInitializer.class, className.toString()); + initializer.initialize(freshApplicationContext); + freshApplicationContext.refresh(); + result.accept(freshApplicationContext, compiled); + }); + } + + private void registerEntries(Class... basePackageClasses) { + List 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 packageNames; + + TestConfiguration(Collection packageNames) { + this.packageNames = packageNames; + } + + @Bean + JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext applicationContext) { + this.scanningInvoked = true; + return JsonMixinModuleEntries.scan(applicationContext, this.packageNames); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java index da68e5fe34..a7a62ac737 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java @@ -52,6 +52,8 @@ class JsonMixinModuleTests { } @Test + @Deprecated(since = "3.0.0", forRemoval = true) + @SuppressWarnings("removal") void createWhenContextIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new JsonMixinModule(null, Collections.emptyList())) .withMessageContaining("Context must not be null"); @@ -89,12 +91,20 @@ class JsonMixinModuleTests { private void load(Class... basePackageClasses) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - List basePackages = Arrays.stream(basePackageClasses).map(ClassUtils::getPackageName).toList(); - context.registerBean(JsonMixinModule.class, () -> new JsonMixinModule(context, basePackages)); + context.registerBean(JsonMixinModule.class, () -> createJsonMixinModule(context, basePackageClasses)); context.refresh(); this.context = context; } + private JsonMixinModule createJsonMixinModule(AnnotationConfigApplicationContext context, + Class... basePackageClasses) { + List 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 { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(module); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/e/PrivateMixInClass.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/e/PrivateMixInClass.java new file mode 100644 index 0000000000..a44e1d873f --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/e/PrivateMixInClass.java @@ -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; + } + +}