See gh-30152
pull/30472/head
Andy Wilkinson 3 years ago
parent df417bf317
commit 685d2d4391

@ -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<String> packages = JsonMixinScanPackages.get(context).getPackageNames();
if (packages.isEmpty() && AutoConfigurationPackages.has(context)) {
packages = AutoConfigurationPackages.get(context);
}
List<String> packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context)
: Collections.emptyList();
return new JsonMixinModule(context, packages);
}

@ -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 {
}
}

@ -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.

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

@ -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.
* <p>
* 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.
* <p>
* 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 {};
}

@ -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<String> packageNames;
JsonMixinScanPackages(String... packageNames) {
List<String> 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<String> 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<String> 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<String> getPackagesToScan(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(JsonMixinScan.class.getName()));
Set<String> 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<String> packageNames = new LinkedHashSet<>();
JsonMixinScanPackagesBeanDefinition(Collection<String> 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<String> additionalPackageNames) {
this.packageNames.addAll(additionalPackageNames);
}
}
}

@ -41,7 +41,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Guirong Hu
*/
public class JsonMixinModuleTests {
class JsonMixinModuleTests {
private AnnotationConfigApplicationContext context;

@ -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<String>) 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 {
}
}
Loading…
Cancel
Save