Skip to content

Commit 37d2d4d

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

File tree

5 files changed

+278
-1
lines changed

5 files changed

+278
-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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.core.annotation.AliasFor;
26+
import org.springframework.stereotype.Component;
27+
28+
/**
29+
* {@link Component @Component} that provides mix-in class implementations to be
30+
* registered with Jackson when {@link JsonMixinModule} is in use.
31+
*
32+
* @author Guirong Hu
33+
* @see JsonMixinModule
34+
* @since 2.7.0
35+
*/
36+
@Target(ElementType.TYPE)
37+
@Retention(RetentionPolicy.RUNTIME)
38+
@Documented
39+
@Component
40+
public @interface JsonMixin {
41+
42+
/**
43+
* Explicitly specify the name of the Spring bean definition associated with the
44+
* {@code @JsonMixin} class. If left unspecified (the common case), a bean name will
45+
* be automatically generated.
46+
* @return the explicit component name, if any (or empty String otherwise)
47+
*/
48+
@AliasFor(annotation = Component.class)
49+
String value() default "";
50+
51+
/**
52+
* The types that are handled by the provided mix-in class.
53+
* @return the types that should be mixed-in by the component
54+
* @since 2.7.0
55+
*/
56+
Class<?>[] type() default {};
57+
58+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.util.Map;
20+
21+
import com.fasterxml.jackson.databind.Module;
22+
import com.fasterxml.jackson.databind.module.SimpleModule;
23+
24+
import org.springframework.beans.BeansException;
25+
import org.springframework.beans.factory.BeanFactory;
26+
import org.springframework.beans.factory.BeanFactoryAware;
27+
import org.springframework.beans.factory.HierarchicalBeanFactory;
28+
import org.springframework.beans.factory.InitializingBean;
29+
import org.springframework.beans.factory.ListableBeanFactory;
30+
import org.springframework.core.annotation.MergedAnnotation;
31+
import org.springframework.core.annotation.MergedAnnotations;
32+
import org.springframework.util.ObjectUtils;
33+
34+
/**
35+
* Spring Bean and Jackson {@link Module} to register {@link JsonMixin @JsonMixin}
36+
* annotated beans.
37+
*
38+
* @author Guirong Hu
39+
* @since 2.7.0
40+
* @see JsonMixin
41+
*/
42+
public class JsonMixinModule extends SimpleModule implements BeanFactoryAware, InitializingBean {
43+
44+
private BeanFactory beanFactory;
45+
46+
@Override
47+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
48+
this.beanFactory = beanFactory;
49+
}
50+
51+
@Override
52+
public void afterPropertiesSet() throws Exception {
53+
BeanFactory beanFactory = this.beanFactory;
54+
while (beanFactory != null) {
55+
if (beanFactory instanceof ListableBeanFactory) {
56+
addJsonMixinBeans((ListableBeanFactory) beanFactory);
57+
}
58+
beanFactory = (beanFactory instanceof HierarchicalBeanFactory)
59+
? ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory() : null;
60+
}
61+
}
62+
63+
private void addJsonMixinBeans(ListableBeanFactory beanFactory) {
64+
Map<String, Object> beans = beanFactory.getBeansWithAnnotation(JsonMixin.class);
65+
for (Object bean : beans.values()) {
66+
addJsonMixinBean(bean);
67+
}
68+
}
69+
70+
private void addJsonMixinBean(Object bean) {
71+
Class<?> mixinClass = bean.getClass();
72+
MergedAnnotation<JsonMixin> annotation = MergedAnnotations
73+
.from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(JsonMixin.class);
74+
Class<?>[] targetTypes = annotation.getClassArray("type");
75+
if (ObjectUtils.isEmpty(targetTypes)) {
76+
return;
77+
}
78+
for (Class<?> targetType : targetTypes) {
79+
setMixInAnnotation(targetType, mixinClass);
80+
}
81+
}
82+
83+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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.beans.factory.annotation.Lookup;
26+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
/**
31+
* Tests for {@link JsonMixinModule}.
32+
*
33+
* @author Guirong Hu
34+
*/
35+
public class JsonMixinModuleTests {
36+
37+
private AnnotationConfigApplicationContext context;
38+
39+
@AfterEach
40+
void closeContext() {
41+
if (this.context != null) {
42+
this.context.close();
43+
}
44+
}
45+
46+
@Test
47+
void jsonWithModuleWithRenameMixInClassShouldBeMixedIn() throws Exception {
48+
load(RenameMixInClass.class);
49+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
50+
assertMixIn(module, new Name("spring"), "{\"username\":\"spring\"}");
51+
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
52+
}
53+
54+
@Test
55+
void jsonWithModuleWithEmptyMixInClassShouldNotBeMixedIn() throws Exception {
56+
load(EmptyMixInClass.class);
57+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
58+
assertMixIn(module, new Name("spring"), "{\"name\":\"spring\"}");
59+
assertMixIn(module, new NameAndAge("spring", 100), "{\"name\":\"spring\",\"age\":100}");
60+
}
61+
62+
@Test
63+
void jsonWithModuleWithRenameAndContainsLookupMethodMixInAbstractClassShouldBeMixedIn() throws Exception {
64+
load(RenameAndContainsLookupMethodMixInAbstractClass.class);
65+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
66+
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
67+
}
68+
69+
@Test
70+
void jsonWithModuleWithRenameAndContainsLookupMethodMixInInterfaceShouldBeMixedIn() throws Exception {
71+
load(RenameAndContainsLookupMethodMixInInterface.class);
72+
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
73+
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
74+
}
75+
76+
private void load(Class<?>... configs) {
77+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
78+
context.register(configs);
79+
context.register(JsonMixinModule.class);
80+
context.refresh();
81+
this.context = context;
82+
}
83+
84+
private void assertMixIn(Module module, Name value, String expectedJson) throws Exception {
85+
ObjectMapper mapper = new ObjectMapper();
86+
mapper.registerModule(module);
87+
String json = mapper.writeValueAsString(value);
88+
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
89+
}
90+
91+
@JsonMixin(type = { Name.class, NameAndAge.class })
92+
static class RenameMixInClass {
93+
94+
@JsonProperty("username")
95+
String getName() {
96+
return null;
97+
}
98+
99+
}
100+
101+
@JsonMixin(type = NameAndAge.class)
102+
abstract static class RenameAndContainsLookupMethodMixInAbstractClass {
103+
104+
@JsonProperty("username")
105+
@Lookup
106+
abstract String getName();
107+
108+
}
109+
110+
@JsonMixin(type = NameAndAge.class)
111+
interface RenameAndContainsLookupMethodMixInInterface {
112+
113+
@JsonProperty("username")
114+
@Lookup
115+
String getName();
116+
117+
}
118+
119+
@JsonMixin(type = { Name.class, NameAndAge.class })
120+
static class EmptyMixInClass {
121+
122+
}
123+
124+
}

0 commit comments

Comments
 (0)