Skip to content

Commit e471415

Browse files
committed
Simplify registration of Jackson mixin types
Closes gh-25920
1 parent 6f8ce3d commit e471415

File tree

5 files changed

+279
-1
lines changed

5 files changed

+279
-1
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.boot.autoconfigure.jackson.JacksonProperties.ConstructorDetectorStrategy;
4747
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4848
import org.springframework.boot.jackson.JsonComponentModule;
49+
import org.springframework.boot.jackson.JsonMixinModule;
4950
import org.springframework.context.ApplicationContext;
5051
import org.springframework.context.annotation.Bean;
5152
import org.springframework.context.annotation.Configuration;
@@ -93,6 +94,11 @@ public JsonComponentModule jsonComponentModule() {
9394
return new JsonComponentModule();
9495
}
9596

97+
@Bean
98+
public JsonMixinModule jsonMixinModule() {
99+
return new JsonMixinModule();
100+
}
101+
96102
@Configuration(proxyBeanMethods = false)
97103
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
98104
static class JacksonObjectMapperConfiguration {

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -52,6 +52,7 @@
5252
import org.springframework.boot.autoconfigure.AutoConfigurations;
5353
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
5454
import org.springframework.boot.jackson.JsonComponent;
55+
import org.springframework.boot.jackson.JsonMixinModule;
5556
import org.springframework.boot.jackson.JsonObjectSerializer;
5657
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
5758
import org.springframework.context.annotation.Bean;
@@ -90,6 +91,11 @@ void doubleModuleRegistration() {
9091
});
9192
}
9293

94+
@Test
95+
void jsonMixinModuleShouldBeAutoconfigured() {
96+
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(JsonMixinModule.class));
97+
}
98+
9399
@Test
94100
void noCustomDateFormat() {
95101
this.contextRunner.run((context) -> {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.jackson;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
26+
import org.springframework.context.annotation.Scope;
27+
import org.springframework.core.annotation.AliasFor;
28+
import org.springframework.stereotype.Component;
29+
30+
/**
31+
* {@link Component @Component} that provides mix-in class implementations to be
32+
* registered with Jackson when {@link JsonMixinModule} is in use.
33+
*
34+
* @author Guirong Hu
35+
* @see JsonMixinModule
36+
* @since 2.7.0
37+
*/
38+
@Target(ElementType.TYPE)
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Documented
41+
@Component
42+
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
43+
public @interface JsonMixin {
44+
45+
/**
46+
* Explicitly specify the name of the Spring bean definition associated with the
47+
* {@code @JsonMixin} class. If left unspecified (the common case), a bean name will
48+
* be automatically generated.
49+
* @return the explicit component name, if any (or empty String otherwise)
50+
*/
51+
@AliasFor(annotation = Component.class)
52+
String value() default "";
53+
54+
/**
55+
* The types that are handled by the provided mix-in class.
56+
* @return the types that should be mixed-in by the component
57+
* @since 2.7.0
58+
*/
59+
Class<?>[] type() default {};
60+
61+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.jackson;
18+
19+
import com.fasterxml.jackson.databind.Module;
20+
import com.fasterxml.jackson.databind.module.SimpleModule;
21+
22+
import org.springframework.beans.BeansException;
23+
import org.springframework.beans.factory.BeanFactory;
24+
import org.springframework.beans.factory.BeanFactoryAware;
25+
import org.springframework.beans.factory.HierarchicalBeanFactory;
26+
import org.springframework.beans.factory.InitializingBean;
27+
import org.springframework.beans.factory.ListableBeanFactory;
28+
import org.springframework.core.annotation.MergedAnnotation;
29+
import org.springframework.core.annotation.MergedAnnotations;
30+
import org.springframework.util.ObjectUtils;
31+
32+
/**
33+
* Spring Bean and Jackson {@link Module} to register {@link JsonMixin @JsonMixin}
34+
* annotated beans.
35+
*
36+
* @author Guirong Hu
37+
* @since 2.7.0
38+
* @see JsonMixin
39+
*/
40+
public class JsonMixinModule extends SimpleModule implements BeanFactoryAware, InitializingBean {
41+
42+
private BeanFactory beanFactory;
43+
44+
@Override
45+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
46+
this.beanFactory = beanFactory;
47+
}
48+
49+
@Override
50+
public void afterPropertiesSet() throws Exception {
51+
BeanFactory beanFactory = this.beanFactory;
52+
while (beanFactory != null) {
53+
if (beanFactory instanceof ListableBeanFactory) {
54+
addJsonMixinBeans((ListableBeanFactory) beanFactory);
55+
}
56+
beanFactory = (beanFactory instanceof HierarchicalBeanFactory)
57+
? ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory() : null;
58+
}
59+
}
60+
61+
private void addJsonMixinBeans(ListableBeanFactory beanFactory) {
62+
String[] names = beanFactory.getBeanNamesForAnnotation(JsonMixin.class);
63+
if (ObjectUtils.isEmpty(names)) {
64+
return;
65+
}
66+
for (String name : names) {
67+
Class<?> mixinClass = beanFactory.getType(name);
68+
addJsonMixinBean(mixinClass);
69+
}
70+
}
71+
72+
private void addJsonMixinBean(Class<?> mixinClass) {
73+
MergedAnnotation<JsonMixin> annotation = MergedAnnotations
74+
.from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(JsonMixin.class);
75+
Class<?>[] targetTypes = annotation.getClassArray("type");
76+
if (ObjectUtils.isEmpty(targetTypes)) {
77+
return;
78+
}
79+
for (Class<?> targetType : targetTypes) {
80+
setMixInAnnotation(targetType, mixinClass);
81+
}
82+
}
83+
84+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.jackson;
18+
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import com.fasterxml.jackson.databind.Module;
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Tests for {@link JsonMixinModule}.
31+
*
32+
* @author Guirong Hu
33+
*/
34+
public class JsonMixinModuleTests {
35+
36+
private AnnotationConfigApplicationContext context;
37+
38+
@AfterEach
39+
void closeContext() {
40+
if (this.context != null) {
41+
this.context.close();
42+
}
43+
}
44+
45+
@Test
46+
void jsonWithModuleWithRenameMixInClassShouldBeMixedIn() throws Exception {
47+
load(RenameMixInClass.class);
48+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
49+
assertMixIn(module, new Name("spring"), "{\"username\":\"spring\"}");
50+
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
51+
}
52+
53+
@Test
54+
void jsonWithModuleWithEmptyMixInClassShouldNotBeMixedIn() throws Exception {
55+
load(EmptyMixInClass.class);
56+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
57+
assertMixIn(module, new Name("spring"), "{\"name\":\"spring\"}");
58+
assertMixIn(module, new NameAndAge("spring", 100), "{\"name\":\"spring\",\"age\":100}");
59+
}
60+
61+
@Test
62+
void jsonWithModuleWithRenameMixInAbstractClassShouldBeMixedIn() throws Exception {
63+
load(RenameMixInAbstractClass.class);
64+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
65+
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
66+
}
67+
68+
@Test
69+
void jsonWithModuleWithRenameMixInInterfaceShouldBeMixedIn() throws Exception {
70+
load(RenameMixInInterface.class);
71+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
72+
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
73+
}
74+
75+
private void load(Class<?>... configs) {
76+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
77+
context.register(configs);
78+
context.register(JsonMixinModule.class);
79+
context.refresh();
80+
this.context = context;
81+
}
82+
83+
private void assertMixIn(Module module, Name value, String expectedJson) throws Exception {
84+
ObjectMapper mapper = new ObjectMapper();
85+
mapper.registerModule(module);
86+
String json = mapper.writeValueAsString(value);
87+
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
88+
}
89+
90+
@JsonMixin(type = { Name.class, NameAndAge.class })
91+
static class RenameMixInClass {
92+
93+
@JsonProperty("username")
94+
String getName() {
95+
return null;
96+
}
97+
98+
}
99+
100+
@JsonMixin(type = NameAndAge.class)
101+
abstract static class RenameMixInAbstractClass {
102+
103+
@JsonProperty("username")
104+
abstract String getName();
105+
106+
}
107+
108+
@JsonMixin(type = NameAndAge.class)
109+
interface RenameMixInInterface {
110+
111+
@JsonProperty("username")
112+
String getName();
113+
114+
}
115+
116+
@JsonMixin(type = { Name.class, NameAndAge.class })
117+
static class EmptyMixInClass {
118+
119+
}
120+
121+
}

0 commit comments

Comments
 (0)