From df417bf317ed1c64309bc3e4e210117c1e7a8bb1 Mon Sep 17 00:00:00 2001 From: Guirong Hu Date: Thu, 10 Mar 2022 11:37:16 +0800 Subject: [PATCH 1/2] Simplify registration of Jackson mixin types See gh-30152 --- .../jackson/JacksonAutoConfiguration.java | 12 ++ .../JacksonAutoConfigurationTests.java | 8 +- .../boot/jackson/JsonMixin.java | 59 ++++++ .../boot/jackson/JsonMixinModule.java | 107 ++++++++++ .../boot/jackson/JsonMixinScan.java | 75 +++++++ .../boot/jackson/JsonMixinScanPackages.java | 185 ++++++++++++++++++ .../boot/jackson/JsonMixinModuleTests.java | 107 ++++++++++ .../jackson/JsonMixinScanPackagesTests.java | 182 +++++++++++++++++ .../boot/jackson/scan/a/RenameMixInClass.java | 33 ++++ .../scan/b/RenameMixInAbstractClass.java | 30 +++ .../jackson/scan/c/RenameMixInInterface.java | 30 +++ .../boot/jackson/scan/d/EmptyMixInClass.java | 26 +++ 12 files changed, 853 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixin.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/a/RenameMixInClass.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/b/RenameMixInAbstractClass.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/c/RenameMixInInterface.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/d/EmptyMixInClass.java 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 dd1d938959..d8a3b1e93a 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 @@ -41,11 +41,14 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jackson.JacksonProperties.ConstructorDetectorStrategy; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jackson.JsonComponentModule; +import org.springframework.boot.jackson.JsonMixinModule; +import org.springframework.boot.jackson.JsonMixinScanPackages; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -93,6 +96,15 @@ public class JacksonAutoConfiguration { return new JsonComponentModule(); } + @Bean + public JsonMixinModule jsonMixinModule(ApplicationContext context) { + List packages = JsonMixinScanPackages.get(context).getPackageNames(); + if (packages.isEmpty() && AutoConfigurationPackages.has(context)) { + packages = AutoConfigurationPackages.get(context); + } + return new JsonMixinModule(context, packages); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Jackson2ObjectMapperBuilder.class) static class JacksonObjectMapperConfiguration { 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 c95797bf38..1c79ba9829 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. @@ -52,6 +52,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.jackson.JsonComponent; +import org.springframework.boot.jackson.JsonMixinModule; import org.springframework.boot.jackson.JsonObjectSerializer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -90,6 +91,11 @@ class JacksonAutoConfigurationTests { }); } + @Test + void jsonMixinModuleShouldBeAutoconfigured() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(JsonMixinModule.class)); + } + @Test void noCustomDateFormat() { this.contextRunner.run((context) -> { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixin.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixin.java new file mode 100644 index 0000000000..c1dcbe892c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixin.java @@ -0,0 +1,59 @@ +/* + * 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.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Provides a mixin class implementation that registers with Jackson when using + * {@link JsonMixinModule}. + * + * @author Guirong Hu + * @see JsonMixinModule + * @since 2.7.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JsonMixin { + + /** + * Alias for the {@link #type()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @JsonMixin(MyType.class)} instead of + * {@code @JsonMixin(type=MyType.class)}. + * @return the mixed-in classes + * @since 2.7.0 + */ + @AliasFor("type") + Class[] value() default {}; + + /** + * The types that are handled by the provided mix-in class. {@link #value()} is an + * alias for (and mutually exclusive with) this attribute. + * @return the mixed-in classes + * @since 2.7.0 + */ + @AliasFor("value") + Class[] type() default {}; + +} 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 new file mode 100644 index 0000000000..de70dd907f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java @@ -0,0 +1,107 @@ +/* + * 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.io.IOException; +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.classreading.MetadataReader; +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 register {@link JsonMixin @JsonMixin} + * annotated beans. + * + * @author Guirong Hu + * @since 2.7.0 + * @see JsonMixin + */ +public class JsonMixinModule extends SimpleModule implements InitializingBean { + + private final ApplicationContext context; + + private final Collection basePackages; + + /** + * Create a new {@link JsonMixinModule} instance. + * @param context the source application context + * @param basePackages the packages to check for annotated classes + */ + 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); + Class[] targetTypes = annotation.getClassArray("type"); + if (ObjectUtils.isEmpty(targetTypes)) { + return; + } + for (Class targetType : targetTypes) { + setMixInAnnotation(targetType, mixinClass); + } + } + + static class JsonMixinComponentScanner extends ClassPathScanningCandidateComponentProvider { + + @Override + protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { + return true; + } + + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return beanDefinition.getMetadata().hasAnnotation(JsonMixin.class.getName()); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java new file mode 100644 index 0000000000..86f8835e98 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java @@ -0,0 +1,75 @@ +/* + * 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.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AliasFor; + +/** + * Configures the base packages used by auto-configuration when scanning for mix-in + * classes. One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias + * {@link #value()} may be specified to define specific packages to scan. If specific + * packages are not defined scanning will occur from the package of the class with this + * annotation. + * + * @author Guirong Hu + * @since 2.7.0 + * @see JsonMixinScanPackages + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(JsonMixinScanPackages.Registrar.class) +public @interface JsonMixinScan { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @JsonMixinScan("org.my.pkg")} instead of + * {@code @JsonMixinScan(basePackages="org.my.pkg")}. + * @return the base packages to scan + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Base packages to scan for mix-in classes. {@link #value()} is an alias for (and + * mutually exclusive with) this attribute. + *

+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based + * package names. + * @return the base packages to scan + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for mix-in classes. The package of each class specified will be scanned. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * @return classes from the base packages to scan + */ + Class[] basePackageClasses() default {}; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java new file mode 100644 index 0000000000..144c0c0543 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java @@ -0,0 +1,185 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Class for storing {@link JsonMixinScan @JsonMixinScan} specified packages for reference + * later. + * + * @author Guirong Hu + * @since 2.7.0 + * @see JsonMixinScan + * @see JsonMixinModule + */ +public class JsonMixinScanPackages { + + private static final String BEAN = JsonMixinScanPackages.class.getName(); + + private static final JsonMixinScanPackages NONE = new JsonMixinScanPackages(); + + private final List packageNames; + + JsonMixinScanPackages(String... packageNames) { + List packages = new ArrayList<>(); + for (String name : packageNames) { + if (StringUtils.hasText(name)) { + packages.add(name); + } + } + this.packageNames = Collections.unmodifiableList(packages); + } + + /** + * Return the package names specified from all {@link JsonMixinScan @JsonMixinScan} + * annotations. + * @return the mix-in classes scan package names + */ + public List getPackageNames() { + return this.packageNames; + } + + /** + * Return the {@link JsonMixinScanPackages} for the given bean factory. + * @param beanFactory the source bean factory + * @return the {@link JsonMixinScanPackages} for the bean factory (never {@code null}) + */ + public static JsonMixinScanPackages get(BeanFactory beanFactory) { + // Currently, we only store a single base package, but we return a list to + // allow this to change in the future if needed + try { + return beanFactory.getBean(BEAN, JsonMixinScanPackages.class); + } + catch (NoSuchBeanDefinitionException ex) { + return NONE; + } + } + + /** + * Register the specified mix-in classes scan packages with the system. + * @param registry the source registry + * @param packageNames the package names to register + */ + public static void register(BeanDefinitionRegistry registry, String... packageNames) { + Assert.notNull(registry, "Registry must not be null"); + Assert.notNull(packageNames, "PackageNames must not be null"); + register(registry, Arrays.asList(packageNames)); + } + + /** + * Register the specified mix-in classes scan packages with the system. + * @param registry the source registry + * @param packageNames the package names to register + */ + public static void register(BeanDefinitionRegistry registry, Collection packageNames) { + Assert.notNull(registry, "Registry must not be null"); + Assert.notNull(packageNames, "PackageNames must not be null"); + if (registry.containsBeanDefinition(BEAN)) { + JsonMixinScanPackagesBeanDefinition beanDefinition = (JsonMixinScanPackagesBeanDefinition) registry + .getBeanDefinition(BEAN); + beanDefinition.addPackageNames(packageNames); + } + else { + registry.registerBeanDefinition(BEAN, new JsonMixinScanPackagesBeanDefinition(packageNames)); + } + } + + /** + * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing + * configuration. + */ + static class Registrar implements ImportBeanDefinitionRegistrar { + + private final Environment environment; + + Registrar(Environment environment) { + this.environment = environment; + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + register(registry, getPackagesToScan(metadata)); + } + + private Set getPackagesToScan(AnnotationMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes + .fromMap(metadata.getAnnotationAttributes(JsonMixinScan.class.getName())); + Set packagesToScan = new LinkedHashSet<>(); + for (String basePackage : attributes.getStringArray("basePackages")) { + String[] tokenized = StringUtils.tokenizeToStringArray( + this.environment.resolvePlaceholders(basePackage), + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + Collections.addAll(packagesToScan, tokenized); + } + for (Class basePackageClass : attributes.getClassArray("basePackageClasses")) { + packagesToScan.add(this.environment.resolvePlaceholders(ClassUtils.getPackageName(basePackageClass))); + } + if (packagesToScan.isEmpty()) { + String packageName = ClassUtils.getPackageName(metadata.getClassName()); + Assert.state(StringUtils.hasLength(packageName), + "@JsonMixinScan cannot be used with the default package"); + return Collections.singleton(packageName); + } + return packagesToScan; + } + + } + + static class JsonMixinScanPackagesBeanDefinition extends GenericBeanDefinition { + + private final Set packageNames = new LinkedHashSet<>(); + + JsonMixinScanPackagesBeanDefinition(Collection packageNames) { + setBeanClass(JsonMixinScanPackages.class); + setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + addPackageNames(packageNames); + } + + @Override + public Supplier getInstanceSupplier() { + return () -> new JsonMixinScanPackages(StringUtils.toStringArray(this.packageNames)); + } + + private void addPackageNames(Collection additionalPackageNames) { + this.packageNames.addAll(additionalPackageNames); + } + + } + +} 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 new file mode 100644 index 0000000000..07addfaa78 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java @@ -0,0 +1,107 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.jackson.scan.a.RenameMixInClass; +import org.springframework.boot.jackson.scan.b.RenameMixInAbstractClass; +import org.springframework.boot.jackson.scan.c.RenameMixInInterface; +import org.springframework.boot.jackson.scan.d.EmptyMixInClass; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link JsonMixinModule}. + * + * @author Guirong Hu + */ +public class JsonMixinModuleTests { + + private AnnotationConfigApplicationContext context; + + @AfterEach + void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + void createWhenContextIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new JsonMixinModule(null, Collections.emptyList())) + .withMessageContaining("Context must not be null"); + } + + @Test + void jsonWithModuleWithRenameMixInClassShouldBeMixedIn() throws Exception { + load(RenameMixInClass.class); + JsonMixinModule module = this.context.getBean(JsonMixinModule.class); + assertMixIn(module, new Name("spring"), "{\"username\":\"spring\"}"); + assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}"); + } + + @Test + void jsonWithModuleWithEmptyMixInClassShouldNotBeMixedIn() throws Exception { + load(EmptyMixInClass.class); + JsonMixinModule module = this.context.getBean(JsonMixinModule.class); + assertMixIn(module, new Name("spring"), "{\"name\":\"spring\"}"); + assertMixIn(module, new NameAndAge("spring", 100), "{\"name\":\"spring\",\"age\":100}"); + } + + @Test + void jsonWithModuleWithRenameMixInAbstractClassShouldBeMixedIn() throws Exception { + load(RenameMixInAbstractClass.class); + JsonMixinModule module = this.context.getBean(JsonMixinModule.class); + assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}"); + } + + @Test + void jsonWithModuleWithRenameMixInInterfaceShouldBeMixedIn() throws Exception { + load(RenameMixInInterface.class); + JsonMixinModule module = this.context.getBean(JsonMixinModule.class); + assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}"); + } + + private void load(Class... basePackageClasses) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + List basePackages = Arrays.stream(basePackageClasses).map(ClassUtils::getPackageName) + .collect(Collectors.toList()); + context.registerBean(JsonMixinModule.class, () -> new JsonMixinModule(context, basePackages)); + context.refresh(); + this.context = context; + } + + private void assertMixIn(Module module, Name value, String expectedJson) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + String json = mapper.writeValueAsString(value); + assertThat(json).isEqualToIgnoringWhitespace(expectedJson); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java new file mode 100644 index 0000000000..179c4fd128 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java @@ -0,0 +1,182 @@ +/* + * 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.Collections; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationConfigurationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link JsonMixinScanPackages}. + * + * @author Guirong Hu + */ +class JsonMixinScanPackagesTests { + + private AnnotationConfigApplicationContext context; + + @AfterEach + void cleanup() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + void getWhenNoneRegisteredShouldReturnNone() { + this.context = new AnnotationConfigApplicationContext(); + this.context.refresh(); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages).isNotNull(); + assertThat(packages.getPackageNames()).isEmpty(); + } + + @Test + void getShouldReturnRegisterPackages() { + this.context = new AnnotationConfigApplicationContext(); + JsonMixinScanPackages.register(this.context, "a", "b"); + JsonMixinScanPackages.register(this.context, "b", "c"); + this.context.refresh(); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages.getPackageNames()).containsExactly("a", "b", "c"); + } + + @Test + void registerFromArrayWhenRegistryIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> JsonMixinScanPackages.register(null)) + .withMessageContaining("Registry must not be null"); + + } + + @Test + void registerFromArrayWhenPackageNamesIsNullShouldThrowException() { + this.context = new AnnotationConfigApplicationContext(); + assertThatIllegalArgumentException() + .isThrownBy(() -> JsonMixinScanPackages.register(this.context, (String[]) null)) + .withMessageContaining("PackageNames must not be null"); + } + + @Test + void registerFromCollectionWhenRegistryIsNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> JsonMixinScanPackages.register(null, Collections.emptyList())) + .withMessageContaining("Registry must not be null"); + } + + @Test + void registerFromCollectionWhenPackageNamesIsNullShouldThrowException() { + this.context = new AnnotationConfigApplicationContext(); + assertThatIllegalArgumentException() + .isThrownBy(() -> JsonMixinScanPackages.register(this.context, (Collection) null)) + .withMessageContaining("PackageNames must not be null"); + } + + @Test + void jsonMixinScanAnnotationWhenHasValueAttributeShouldSetupPackages() { + this.context = new AnnotationConfigApplicationContext(JsonMixinScanValueConfig.class); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages.getPackageNames()).containsExactly("a"); + } + + @Test + void jsonMixinScanAnnotationWhenHasValueAttributeShouldSetupPackagesAsm() { + this.context = new AnnotationConfigApplicationContext(); + this.context.registerBeanDefinition("jsonMixinScanValueConfig", + new RootBeanDefinition(JsonMixinScanValueConfig.class.getName())); + this.context.refresh(); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages.getPackageNames()).containsExactly("a"); + } + + @Test + void jsonMixinScanAnnotationWhenHasBasePackagesAttributeShouldSetupPackages() { + this.context = new AnnotationConfigApplicationContext(JsonMixinScanBasePackagesConfig.class); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages.getPackageNames()).containsExactly("b"); + } + + @Test + void jsonMixinScanAnnotationWhenHasValueAndBasePackagesAttributeShouldThrow() { + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> this.context = new AnnotationConfigApplicationContext( + JsonMixinScanValueAndBasePackagesConfig.class)); + } + + @Test + void jsonMixinScanAnnotationWhenHasBasePackageClassesAttributeShouldSetupPackages() { + this.context = new AnnotationConfigApplicationContext(JsonMixinScanBasePackageClassesConfig.class); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages.getPackageNames()).containsExactly(getClass().getPackage().getName()); + } + + @Test + void jsonMixinScanAnnotationWhenNoAttributesShouldSetupPackages() { + this.context = new AnnotationConfigApplicationContext(JsonMixinScanNoAttributesConfig.class); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages.getPackageNames()).containsExactly(getClass().getPackage().getName()); + } + + @Test + void jsonMixinScanAnnotationWhenLoadingFromMultipleConfigsShouldCombinePackages() { + this.context = new AnnotationConfigApplicationContext(JsonMixinScanValueConfig.class, + JsonMixinScanBasePackagesConfig.class); + JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); + assertThat(packages.getPackageNames()).containsExactly("a", "b"); + } + + @Configuration(proxyBeanMethods = false) + @JsonMixinScan("a") + static class JsonMixinScanValueConfig { + + } + + @Configuration(proxyBeanMethods = false) + @JsonMixinScan(basePackages = "b") + static class JsonMixinScanBasePackagesConfig { + + } + + @Configuration(proxyBeanMethods = false) + @JsonMixinScan(value = "a", basePackages = "b") + static class JsonMixinScanValueAndBasePackagesConfig { + + } + + @Configuration(proxyBeanMethods = false) + @JsonMixinScan(basePackageClasses = JsonMixinScanPackagesTests.class) + static class JsonMixinScanBasePackageClassesConfig { + + } + + @Configuration(proxyBeanMethods = false) + @JsonMixinScan + static class JsonMixinScanNoAttributesConfig { + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/a/RenameMixInClass.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/a/RenameMixInClass.java new file mode 100644 index 0000000000..15d401f598 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/a/RenameMixInClass.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.a; + +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 }) +public class RenameMixInClass { + + @JsonProperty("username") + String getName() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/b/RenameMixInAbstractClass.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/b/RenameMixInAbstractClass.java new file mode 100644 index 0000000000..0ee9bf1913 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/b/RenameMixInAbstractClass.java @@ -0,0 +1,30 @@ +/* + * 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.b; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.boot.jackson.JsonMixin; +import org.springframework.boot.jackson.NameAndAge; + +@JsonMixin(type = NameAndAge.class) +public abstract class RenameMixInAbstractClass { + + @JsonProperty("username") + abstract String getName(); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/c/RenameMixInInterface.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/c/RenameMixInInterface.java new file mode 100644 index 0000000000..cfff37fe32 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/c/RenameMixInInterface.java @@ -0,0 +1,30 @@ +/* + * 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.c; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.boot.jackson.JsonMixin; +import org.springframework.boot.jackson.NameAndAge; + +@JsonMixin(type = NameAndAge.class) +public interface RenameMixInInterface { + + @JsonProperty("username") + String getName(); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/d/EmptyMixInClass.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/d/EmptyMixInClass.java new file mode 100644 index 0000000000..358f9cb378 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/scan/d/EmptyMixInClass.java @@ -0,0 +1,26 @@ +/* + * 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.d; + +import org.springframework.boot.jackson.JsonMixin; +import org.springframework.boot.jackson.Name; +import org.springframework.boot.jackson.NameAndAge; + +@JsonMixin(type = { Name.class, NameAndAge.class }) +public class EmptyMixInClass { + +} From 685d2d4391196ab4791744548dec62191fc0b63e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 18 Mar 2022 15:56:00 +0000 Subject: [PATCH 2/2] Polish See gh-30152 --- .../jackson/JacksonAutoConfiguration.java | 7 +- .../JacksonAutoConfigurationTests.java | 16 +- .../src/docs/asciidoc/features/json.adoc | 8 + .../boot/jackson/JsonMixinModule.java | 22 +-- .../boot/jackson/JsonMixinScan.java | 75 ------- .../boot/jackson/JsonMixinScanPackages.java | 185 ------------------ .../boot/jackson/JsonMixinModuleTests.java | 2 +- .../jackson/JsonMixinScanPackagesTests.java | 182 ----------------- 8 files changed, 33 insertions(+), 464 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java 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 d8a3b1e93a..90b035efe9 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,7 +48,6 @@ 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.JsonMixinScanPackages; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -98,10 +97,8 @@ public class JacksonAutoConfiguration { @Bean public JsonMixinModule jsonMixinModule(ApplicationContext context) { - List packages = JsonMixinScanPackages.get(context).getPackageNames(); - if (packages.isEmpty() && AutoConfigurationPackages.has(context)) { - packages = AutoConfigurationPackages.get(context); - } + List packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context) + : Collections.emptyList(); return new JsonMixinModule(context, packages); } 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 1c79ba9829..894583bb14 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 @@ -47,8 +47,10 @@ import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.util.StdDateFormat; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +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; @@ -92,8 +94,13 @@ class JacksonAutoConfigurationTests { } @Test - void jsonMixinModuleShouldBeAutoconfigured() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(JsonMixinModule.class)); + 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()); + }); } @Test @@ -637,4 +644,9 @@ class JacksonAutoConfigurationTests { } + @AutoConfigurationPackage + static class MixinConfiguration { + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/json.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/json.adoc index f11989c0a8..62a392628f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/json.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/json.adoc @@ -40,6 +40,14 @@ include::code:object/MyJsonComponent[] +[[features.json.jackson.mixins]] +==== Mixins +Jackson has support for mixins that can be used to mix additional annotations into those already declared on a target class. +Spring Boot's Jackson auto-configuration will scan your application's packages for classes annotated with `@JsonMixin` and register them with the auto-configured `ObjectMapper`. +The registration is performed by Spring Boot's `JsonMixinModule`. + + + [[features.json.gson]] === Gson Auto-configuration for Gson is provided. 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 de70dd907f..2285683384 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 @@ -16,7 +16,6 @@ package org.springframework.boot.jackson; -import java.io.IOException; import java.util.Collection; import com.fasterxml.jackson.databind.Module; @@ -29,15 +28,16 @@ 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.classreading.MetadataReader; +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 register {@link JsonMixin @JsonMixin} - * annotated beans. + * Spring Bean and Jackson {@link Module} to find and + * {@link SimpleModule#setMixInAnnotation(Class, Class) register} + * {@link JsonMixin @JsonMixin}-annotated classes. * * @author Guirong Hu * @since 2.7.0 @@ -68,7 +68,6 @@ public class JsonMixinModule extends SimpleModule implements InitializingBean { 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)) { @@ -81,25 +80,20 @@ public class JsonMixinModule extends SimpleModule implements InitializingBean { private void addJsonMixin(Class mixinClass) { MergedAnnotation annotation = MergedAnnotations .from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(JsonMixin.class); - Class[] targetTypes = annotation.getClassArray("type"); - if (ObjectUtils.isEmpty(targetTypes)) { - return; - } - for (Class targetType : targetTypes) { + for (Class targetType : annotation.getClassArray("type")) { setMixInAnnotation(targetType, mixinClass); } } static class JsonMixinComponentScanner extends ClassPathScanningCandidateComponentProvider { - @Override - protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { - return true; + JsonMixinComponentScanner() { + addIncludeFilter(new AnnotationTypeFilter(JsonMixin.class)); } @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { - return beanDefinition.getMetadata().hasAnnotation(JsonMixin.class.getName()); + return true; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java deleted file mode 100644 index 86f8835e98..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScan.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.context.annotation.Import; -import org.springframework.core.annotation.AliasFor; - -/** - * Configures the base packages used by auto-configuration when scanning for mix-in - * classes. One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias - * {@link #value()} may be specified to define specific packages to scan. If specific - * packages are not defined scanning will occur from the package of the class with this - * annotation. - * - * @author Guirong Hu - * @since 2.7.0 - * @see JsonMixinScanPackages - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Import(JsonMixinScanPackages.Registrar.class) -public @interface JsonMixinScan { - - /** - * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation - * declarations e.g.: {@code @JsonMixinScan("org.my.pkg")} instead of - * {@code @JsonMixinScan(basePackages="org.my.pkg")}. - * @return the base packages to scan - */ - @AliasFor("basePackages") - String[] value() default {}; - - /** - * Base packages to scan for mix-in classes. {@link #value()} is an alias for (and - * mutually exclusive with) this attribute. - *

- * Use {@link #basePackageClasses()} for a type-safe alternative to String-based - * package names. - * @return the base packages to scan - */ - @AliasFor("value") - String[] basePackages() default {}; - - /** - * Type-safe alternative to {@link #basePackages()} for specifying the packages to - * scan for mix-in classes. The package of each class specified will be scanned. - *

- * Consider creating a special no-op marker class or interface in each package that - * serves no purpose other than being referenced by this attribute. - * @return classes from the base packages to scan - */ - Class[] basePackageClasses() default {}; - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java deleted file mode 100644 index 144c0c0543..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinScanPackages.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Supplier; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.env.Environment; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; - -/** - * Class for storing {@link JsonMixinScan @JsonMixinScan} specified packages for reference - * later. - * - * @author Guirong Hu - * @since 2.7.0 - * @see JsonMixinScan - * @see JsonMixinModule - */ -public class JsonMixinScanPackages { - - private static final String BEAN = JsonMixinScanPackages.class.getName(); - - private static final JsonMixinScanPackages NONE = new JsonMixinScanPackages(); - - private final List packageNames; - - JsonMixinScanPackages(String... packageNames) { - List packages = new ArrayList<>(); - for (String name : packageNames) { - if (StringUtils.hasText(name)) { - packages.add(name); - } - } - this.packageNames = Collections.unmodifiableList(packages); - } - - /** - * Return the package names specified from all {@link JsonMixinScan @JsonMixinScan} - * annotations. - * @return the mix-in classes scan package names - */ - public List getPackageNames() { - return this.packageNames; - } - - /** - * Return the {@link JsonMixinScanPackages} for the given bean factory. - * @param beanFactory the source bean factory - * @return the {@link JsonMixinScanPackages} for the bean factory (never {@code null}) - */ - public static JsonMixinScanPackages get(BeanFactory beanFactory) { - // Currently, we only store a single base package, but we return a list to - // allow this to change in the future if needed - try { - return beanFactory.getBean(BEAN, JsonMixinScanPackages.class); - } - catch (NoSuchBeanDefinitionException ex) { - return NONE; - } - } - - /** - * Register the specified mix-in classes scan packages with the system. - * @param registry the source registry - * @param packageNames the package names to register - */ - public static void register(BeanDefinitionRegistry registry, String... packageNames) { - Assert.notNull(registry, "Registry must not be null"); - Assert.notNull(packageNames, "PackageNames must not be null"); - register(registry, Arrays.asList(packageNames)); - } - - /** - * Register the specified mix-in classes scan packages with the system. - * @param registry the source registry - * @param packageNames the package names to register - */ - public static void register(BeanDefinitionRegistry registry, Collection packageNames) { - Assert.notNull(registry, "Registry must not be null"); - Assert.notNull(packageNames, "PackageNames must not be null"); - if (registry.containsBeanDefinition(BEAN)) { - JsonMixinScanPackagesBeanDefinition beanDefinition = (JsonMixinScanPackagesBeanDefinition) registry - .getBeanDefinition(BEAN); - beanDefinition.addPackageNames(packageNames); - } - else { - registry.registerBeanDefinition(BEAN, new JsonMixinScanPackagesBeanDefinition(packageNames)); - } - } - - /** - * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing - * configuration. - */ - static class Registrar implements ImportBeanDefinitionRegistrar { - - private final Environment environment; - - Registrar(Environment environment) { - this.environment = environment; - } - - @Override - public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - register(registry, getPackagesToScan(metadata)); - } - - private Set getPackagesToScan(AnnotationMetadata metadata) { - AnnotationAttributes attributes = AnnotationAttributes - .fromMap(metadata.getAnnotationAttributes(JsonMixinScan.class.getName())); - Set packagesToScan = new LinkedHashSet<>(); - for (String basePackage : attributes.getStringArray("basePackages")) { - String[] tokenized = StringUtils.tokenizeToStringArray( - this.environment.resolvePlaceholders(basePackage), - ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); - Collections.addAll(packagesToScan, tokenized); - } - for (Class basePackageClass : attributes.getClassArray("basePackageClasses")) { - packagesToScan.add(this.environment.resolvePlaceholders(ClassUtils.getPackageName(basePackageClass))); - } - if (packagesToScan.isEmpty()) { - String packageName = ClassUtils.getPackageName(metadata.getClassName()); - Assert.state(StringUtils.hasLength(packageName), - "@JsonMixinScan cannot be used with the default package"); - return Collections.singleton(packageName); - } - return packagesToScan; - } - - } - - static class JsonMixinScanPackagesBeanDefinition extends GenericBeanDefinition { - - private final Set packageNames = new LinkedHashSet<>(); - - JsonMixinScanPackagesBeanDefinition(Collection packageNames) { - setBeanClass(JsonMixinScanPackages.class); - setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - addPackageNames(packageNames); - } - - @Override - public Supplier getInstanceSupplier() { - return () -> new JsonMixinScanPackages(StringUtils.toStringArray(this.packageNames)); - } - - private void addPackageNames(Collection additionalPackageNames) { - this.packageNames.addAll(additionalPackageNames); - } - - } - -} 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 07addfaa78..b6a1430e3d 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 @@ -41,7 +41,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * * @author Guirong Hu */ -public class JsonMixinModuleTests { +class JsonMixinModuleTests { private AnnotationConfigApplicationContext context; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java deleted file mode 100644 index 179c4fd128..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinScanPackagesTests.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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.Collections; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.AnnotationConfigurationException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link JsonMixinScanPackages}. - * - * @author Guirong Hu - */ -class JsonMixinScanPackagesTests { - - private AnnotationConfigApplicationContext context; - - @AfterEach - void cleanup() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - void getWhenNoneRegisteredShouldReturnNone() { - this.context = new AnnotationConfigApplicationContext(); - this.context.refresh(); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages).isNotNull(); - assertThat(packages.getPackageNames()).isEmpty(); - } - - @Test - void getShouldReturnRegisterPackages() { - this.context = new AnnotationConfigApplicationContext(); - JsonMixinScanPackages.register(this.context, "a", "b"); - JsonMixinScanPackages.register(this.context, "b", "c"); - this.context.refresh(); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages.getPackageNames()).containsExactly("a", "b", "c"); - } - - @Test - void registerFromArrayWhenRegistryIsNullShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy(() -> JsonMixinScanPackages.register(null)) - .withMessageContaining("Registry must not be null"); - - } - - @Test - void registerFromArrayWhenPackageNamesIsNullShouldThrowException() { - this.context = new AnnotationConfigApplicationContext(); - assertThatIllegalArgumentException() - .isThrownBy(() -> JsonMixinScanPackages.register(this.context, (String[]) null)) - .withMessageContaining("PackageNames must not be null"); - } - - @Test - void registerFromCollectionWhenRegistryIsNullShouldThrowException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> JsonMixinScanPackages.register(null, Collections.emptyList())) - .withMessageContaining("Registry must not be null"); - } - - @Test - void registerFromCollectionWhenPackageNamesIsNullShouldThrowException() { - this.context = new AnnotationConfigApplicationContext(); - assertThatIllegalArgumentException() - .isThrownBy(() -> JsonMixinScanPackages.register(this.context, (Collection) null)) - .withMessageContaining("PackageNames must not be null"); - } - - @Test - void jsonMixinScanAnnotationWhenHasValueAttributeShouldSetupPackages() { - this.context = new AnnotationConfigApplicationContext(JsonMixinScanValueConfig.class); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages.getPackageNames()).containsExactly("a"); - } - - @Test - void jsonMixinScanAnnotationWhenHasValueAttributeShouldSetupPackagesAsm() { - this.context = new AnnotationConfigApplicationContext(); - this.context.registerBeanDefinition("jsonMixinScanValueConfig", - new RootBeanDefinition(JsonMixinScanValueConfig.class.getName())); - this.context.refresh(); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages.getPackageNames()).containsExactly("a"); - } - - @Test - void jsonMixinScanAnnotationWhenHasBasePackagesAttributeShouldSetupPackages() { - this.context = new AnnotationConfigApplicationContext(JsonMixinScanBasePackagesConfig.class); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages.getPackageNames()).containsExactly("b"); - } - - @Test - void jsonMixinScanAnnotationWhenHasValueAndBasePackagesAttributeShouldThrow() { - assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> this.context = new AnnotationConfigApplicationContext( - JsonMixinScanValueAndBasePackagesConfig.class)); - } - - @Test - void jsonMixinScanAnnotationWhenHasBasePackageClassesAttributeShouldSetupPackages() { - this.context = new AnnotationConfigApplicationContext(JsonMixinScanBasePackageClassesConfig.class); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages.getPackageNames()).containsExactly(getClass().getPackage().getName()); - } - - @Test - void jsonMixinScanAnnotationWhenNoAttributesShouldSetupPackages() { - this.context = new AnnotationConfigApplicationContext(JsonMixinScanNoAttributesConfig.class); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages.getPackageNames()).containsExactly(getClass().getPackage().getName()); - } - - @Test - void jsonMixinScanAnnotationWhenLoadingFromMultipleConfigsShouldCombinePackages() { - this.context = new AnnotationConfigApplicationContext(JsonMixinScanValueConfig.class, - JsonMixinScanBasePackagesConfig.class); - JsonMixinScanPackages packages = JsonMixinScanPackages.get(this.context); - assertThat(packages.getPackageNames()).containsExactly("a", "b"); - } - - @Configuration(proxyBeanMethods = false) - @JsonMixinScan("a") - static class JsonMixinScanValueConfig { - - } - - @Configuration(proxyBeanMethods = false) - @JsonMixinScan(basePackages = "b") - static class JsonMixinScanBasePackagesConfig { - - } - - @Configuration(proxyBeanMethods = false) - @JsonMixinScan(value = "a", basePackages = "b") - static class JsonMixinScanValueAndBasePackagesConfig { - - } - - @Configuration(proxyBeanMethods = false) - @JsonMixinScan(basePackageClasses = JsonMixinScanPackagesTests.class) - static class JsonMixinScanBasePackageClassesConfig { - - } - - @Configuration(proxyBeanMethods = false) - @JsonMixinScan - static class JsonMixinScanNoAttributesConfig { - - } - -}