Skip to content

Commit f9f6544

Browse files
committed
Merge pull request spring-projects#16544 from maly7
* pr/16544: Polish "Support JsonComponent key serializers/deserialzers" Support JsonComponent key serializers/deserialzers Closes spring-projectsgh-16544
2 parents 063bb90 + bf633fc commit f9f6544

File tree

8 files changed

+432
-42
lines changed

8 files changed

+432
-42
lines changed

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java

+47-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2019 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.
@@ -24,16 +24,16 @@
2424

2525
import com.fasterxml.jackson.databind.JsonDeserializer;
2626
import com.fasterxml.jackson.databind.JsonSerializer;
27+
import com.fasterxml.jackson.databind.KeyDeserializer;
2728

2829
import org.springframework.core.annotation.AliasFor;
2930
import org.springframework.stereotype.Component;
3031

3132
/**
32-
* {@link Component} that provides {@link JsonSerializer} and/or {@link JsonDeserializer}
33-
* implementations to be registered with Jackson when {@link JsonComponentModule} is in
34-
* use. Can be used to annotate {@link JsonSerializer} or {@link JsonDeserializer}
35-
* implementations directly or a class that contains them as inner-classes. For example:
36-
* <pre class="code">
33+
* {@link Component} that provides {@link JsonSerializer}, {@link JsonDeserializer} or
34+
* {@link KeyDeserializer} implementations to be registered with Jackson when
35+
* {@link JsonComponentModule} is in use. Can be used to annotate implementations directly
36+
* or a class that contains them as inner-classes. For example: <pre class="code">
3737
* &#064;JsonComponent
3838
* public class CustomerJsonComponent {
3939
*
@@ -56,6 +56,7 @@
5656
* @see JsonComponentModule
5757
* @since 1.4.0
5858
* @author Phillip Webb
59+
* @author Paul Aly
5960
*/
6061
@Target(ElementType.TYPE)
6162
@Retention(RetentionPolicy.RUNTIME)
@@ -71,4 +72,44 @@
7172
@AliasFor(annotation = Component.class)
7273
String value() default "";
7374

75+
/**
76+
* The types that are handled by the provided serializer/deserializer. This attribute
77+
* is mandatory for a {@link KeyDeserializer}, as the type cannot be inferred. For a
78+
* {@link JsonSerializer} or {@link JsonDeserializer} it can be used to limit handling
79+
* to a subclasses of type inferred from the generic.
80+
* @return the types that should be handled by the component
81+
* @since 2.2.0
82+
*/
83+
Class<?>[] type() default {};
84+
85+
/**
86+
* The scope under which the serializer/deserializer should be registered with the
87+
* module.
88+
* @return the component's handle type
89+
* @since 2.2.0
90+
*/
91+
Scope scope() default Scope.VALUES;
92+
93+
/**
94+
* The various scopes under which a serializer/deserialzier can be registered.
95+
* @since 2.2.0
96+
*/
97+
enum Scope {
98+
99+
/**
100+
* A serializer/deserializer for regular value content.
101+
* @see JsonSerializer
102+
* @see JsonDeserializer
103+
*/
104+
VALUES,
105+
106+
/**
107+
* A serializer/deserializer for keys.
108+
* @see JsonSerializer
109+
* @see KeyDeserializer
110+
*/
111+
KEYS
112+
113+
}
114+
74115
}

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java

+69-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2018 the original author or authors.
2+
* Copyright 2012-2019 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.
@@ -18,26 +18,36 @@
1818

1919
import java.lang.reflect.Modifier;
2020
import java.util.Map;
21+
import java.util.function.BiConsumer;
2122

2223
import javax.annotation.PostConstruct;
2324

2425
import com.fasterxml.jackson.databind.JsonDeserializer;
2526
import com.fasterxml.jackson.databind.JsonSerializer;
27+
import com.fasterxml.jackson.databind.KeyDeserializer;
2628
import com.fasterxml.jackson.databind.Module;
2729
import com.fasterxml.jackson.databind.module.SimpleModule;
2830

31+
import org.springframework.beans.BeanUtils;
2932
import org.springframework.beans.BeansException;
3033
import org.springframework.beans.factory.BeanFactory;
3134
import org.springframework.beans.factory.BeanFactoryAware;
3235
import org.springframework.beans.factory.HierarchicalBeanFactory;
3336
import org.springframework.beans.factory.ListableBeanFactory;
37+
import org.springframework.boot.jackson.JsonComponent.Scope;
3438
import org.springframework.core.ResolvableType;
39+
import org.springframework.core.annotation.MergedAnnotation;
40+
import org.springframework.core.annotation.MergedAnnotations;
41+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
42+
import org.springframework.util.Assert;
43+
import org.springframework.util.ObjectUtils;
3544

3645
/**
3746
* Spring Bean and Jackson {@link Module} to register {@link JsonComponent} annotated
3847
* beans.
3948
*
4049
* @author Phillip Webb
50+
* @author Paul Aly
4151
* @since 1.4.0
4252
* @see JsonComponent
4353
*/
@@ -72,38 +82,74 @@ private void addJsonBeans(ListableBeanFactory beanFactory) {
7282
}
7383

7484
private void addJsonBean(Object bean) {
85+
MergedAnnotation<JsonComponent> annotation = MergedAnnotations
86+
.from(bean.getClass(), SearchStrategy.EXHAUSTIVE)
87+
.get(JsonComponent.class);
88+
Class<?>[] types = annotation.getClassArray("type");
89+
Scope scope = annotation.getEnum("scope", JsonComponent.Scope.class);
90+
addJsonBean(bean, types, scope);
91+
}
92+
93+
private void addJsonBean(Object bean, Class<?>[] types, Scope scope) {
7594
if (bean instanceof JsonSerializer) {
76-
addSerializerWithDeducedType((JsonSerializer<?>) bean);
95+
addJsonSerializerBean((JsonSerializer<?>) bean, scope, types);
96+
}
97+
else if (bean instanceof JsonDeserializer) {
98+
addJsonDeserializerBean((JsonDeserializer<?>) bean, types);
7799
}
78-
if (bean instanceof JsonDeserializer) {
79-
addDeserializerWithDeducedType((JsonDeserializer<?>) bean);
100+
else if (bean instanceof KeyDeserializer) {
101+
addKeyDeserializerBean((KeyDeserializer) bean, types);
80102
}
81103
for (Class<?> innerClass : bean.getClass().getDeclaredClasses()) {
82-
if (!Modifier.isAbstract(innerClass.getModifiers())
83-
&& (JsonSerializer.class.isAssignableFrom(innerClass)
84-
|| JsonDeserializer.class.isAssignableFrom(innerClass))) {
85-
try {
86-
addJsonBean(innerClass.newInstance());
87-
}
88-
catch (Exception ex) {
89-
throw new IllegalStateException(ex);
90-
}
104+
if (isSuitableInnerClass(innerClass)) {
105+
Object innerInstance = BeanUtils.instantiateClass(innerClass);
106+
addJsonBean(innerInstance, types, scope);
91107
}
92108
}
93109
}
94110

95-
@SuppressWarnings({ "unchecked" })
96-
private <T> void addSerializerWithDeducedType(JsonSerializer<T> serializer) {
97-
ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
98-
serializer.getClass());
99-
addSerializer((Class<T>) type.resolveGeneric(), serializer);
111+
private boolean isSuitableInnerClass(Class<?> innerClass) {
112+
return !Modifier.isAbstract(innerClass.getModifiers())
113+
&& (JsonSerializer.class.isAssignableFrom(innerClass)
114+
|| JsonDeserializer.class.isAssignableFrom(innerClass)
115+
|| KeyDeserializer.class.isAssignableFrom(innerClass));
116+
}
117+
118+
@SuppressWarnings("unchecked")
119+
private <T> void addJsonSerializerBean(JsonSerializer<T> serializer,
120+
JsonComponent.Scope scope, Class<?>[] types) {
121+
Class<T> baseType = (Class<T>) ResolvableType
122+
.forClass(JsonSerializer.class, serializer.getClass()).resolveGeneric();
123+
addBeanToModule(serializer, baseType, types,
124+
(scope == Scope.VALUES) ? this::addSerializer : this::addKeySerializer);
125+
126+
}
127+
128+
@SuppressWarnings("unchecked")
129+
private <T> void addJsonDeserializerBean(JsonDeserializer<T> deserializer,
130+
Class<?>[] types) {
131+
Class<T> baseType = (Class<T>) ResolvableType
132+
.forClass(JsonDeserializer.class, deserializer.getClass())
133+
.resolveGeneric();
134+
addBeanToModule(deserializer, baseType, types, this::addDeserializer);
135+
}
136+
137+
private void addKeyDeserializerBean(KeyDeserializer deserializer, Class<?>[] types) {
138+
Assert.notEmpty(types, "Type must be specified for KeyDeserializer");
139+
addBeanToModule(deserializer, Object.class, types, this::addKeyDeserializer);
100140
}
101141

102-
@SuppressWarnings({ "unchecked" })
103-
private <T> void addDeserializerWithDeducedType(JsonDeserializer<T> deserializer) {
104-
ResolvableType type = ResolvableType.forClass(JsonDeserializer.class,
105-
deserializer.getClass());
106-
addDeserializer((Class<T>) type.resolveGeneric(), deserializer);
142+
@SuppressWarnings("unchecked")
143+
private <E, T> void addBeanToModule(E element, Class<T> baseType, Class<?>[] types,
144+
BiConsumer<Class<T>, E> consumer) {
145+
if (ObjectUtils.isEmpty(types)) {
146+
consumer.accept(baseType, element);
147+
return;
148+
}
149+
for (Class<?> type : types) {
150+
Assert.isAssignable(baseType, type);
151+
consumer.accept((Class<T>) type, element);
152+
}
107153
}
108154

109155
}

Diff for: spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java

+91-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2018 the original author or authors.
2+
* Copyright 2012-2019 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.
@@ -16,6 +16,12 @@
1616

1717
package org.springframework.boot.jackson;
1818

19+
import java.io.IOException;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import com.fasterxml.jackson.core.type.TypeReference;
24+
import com.fasterxml.jackson.databind.JsonMappingException;
1925
import com.fasterxml.jackson.databind.Module;
2026
import com.fasterxml.jackson.databind.ObjectMapper;
2127
import org.junit.After;
@@ -24,12 +30,14 @@
2430
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2531

2632
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2734

2835
/**
2936
* Tests for {@link JsonComponentModule}.
3037
*
3138
* @author Phillip Webb
3239
* @author Vladimir Tsanev
40+
* @author Paul Aly
3341
*/
3442
public class JsonComponentModuleTests {
3543

@@ -73,6 +81,38 @@ public void moduleShouldAllowInnerAbstractClasses() throws Exception {
7381
context.close();
7482
}
7583

84+
@Test
85+
public void moduleShouldRegisterKeySerializers() throws Exception {
86+
load(OnlyKeySerializer.class);
87+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
88+
assertKeySerialize(module);
89+
}
90+
91+
@Test
92+
public void moduleShouldRegisterKeyDeserializers() throws Exception {
93+
load(OnlyKeyDeserializer.class);
94+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
95+
assertKeyDeserialize(module);
96+
}
97+
98+
@Test
99+
public void moduleShouldRegisterInnerClassesForKeyHandlers() throws Exception {
100+
load(NameAndAgeJsonKeyComponent.class);
101+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
102+
assertKeySerialize(module);
103+
assertKeyDeserialize(module);
104+
}
105+
106+
@Test
107+
public void moduleShouldRegisterOnlyForSpecifiedClasses() throws Exception {
108+
load(NameAndCareerJsonComponent.class);
109+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
110+
assertSerialize(module, new NameAndCareer("spring", "developer"),
111+
"{\"name\":\"spring\"}");
112+
assertSerialize(module);
113+
assertDeserializeForSpecifiedClasses(module);
114+
}
115+
76116
private void load(Class<?>... configs) {
77117
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
78118
context.register(configs);
@@ -81,11 +121,17 @@ private void load(Class<?>... configs) {
81121
this.context = context;
82122
}
83123

84-
private void assertSerialize(Module module) throws Exception {
124+
private void assertSerialize(Module module, Name value, String expectedJson)
125+
throws Exception {
85126
ObjectMapper mapper = new ObjectMapper();
86127
mapper.registerModule(module);
87-
String json = mapper.writeValueAsString(new NameAndAge("spring", 100));
88-
assertThat(json).isEqualToIgnoringWhitespace("{\"name\":\"spring\",\"age\":100}");
128+
String json = mapper.writeValueAsString(value);
129+
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
130+
}
131+
132+
private void assertSerialize(Module module) throws Exception {
133+
assertSerialize(module, new NameAndAge("spring", 100),
134+
"{\"name\":\"spring\",\"age\":100}");
89135
}
90136

91137
private void assertDeserialize(Module module) throws Exception {
@@ -97,6 +143,37 @@ private void assertDeserialize(Module module) throws Exception {
97143
assertThat(nameAndAge.getAge()).isEqualTo(100);
98144
}
99145

146+
private void assertDeserializeForSpecifiedClasses(JsonComponentModule module)
147+
throws IOException {
148+
ObjectMapper mapper = new ObjectMapper();
149+
mapper.registerModule(module);
150+
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(() -> mapper
151+
.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class));
152+
NameAndCareer nameAndCareer = mapper.readValue(
153+
"{\"name\":\"spring\",\"career\":\"developer\"}", NameAndCareer.class);
154+
assertThat(nameAndCareer.getName()).isEqualTo("spring");
155+
assertThat(nameAndCareer.getCareer()).isEqualTo("developer");
156+
}
157+
158+
private void assertKeySerialize(Module module) throws Exception {
159+
ObjectMapper mapper = new ObjectMapper();
160+
mapper.registerModule(module);
161+
Map<NameAndAge, Boolean> map = new HashMap<>();
162+
map.put(new NameAndAge("spring", 100), true);
163+
String json = mapper.writeValueAsString(map);
164+
assertThat(json).isEqualToIgnoringWhitespace("{\"spring is 100\": true}");
165+
}
166+
167+
private void assertKeyDeserialize(Module module) throws IOException {
168+
ObjectMapper mapper = new ObjectMapper();
169+
mapper.registerModule(module);
170+
TypeReference<Map<NameAndAge, Boolean>> typeRef = new TypeReference<Map<NameAndAge, Boolean>>() {
171+
};
172+
Map<NameAndAge, Boolean> map = mapper.readValue("{\"spring is 100\": true}",
173+
typeRef);
174+
assertThat(map).containsEntry(new NameAndAge("spring", 100), true);
175+
}
176+
100177
@JsonComponent
101178
static class OnlySerializer extends NameAndAgeJsonComponent.Serializer {
102179

@@ -121,4 +198,14 @@ static class ConcreteSerializer extends AbstractSerializer {
121198

122199
}
123200

201+
@JsonComponent(scope = JsonComponent.Scope.KEYS)
202+
static class OnlyKeySerializer extends NameAndAgeJsonKeyComponent.Serializer {
203+
204+
}
205+
206+
@JsonComponent(scope = JsonComponent.Scope.KEYS, type = NameAndAge.class)
207+
static class OnlyKeyDeserializer extends NameAndAgeJsonKeyComponent.Deserializer {
208+
209+
}
210+
124211
}

0 commit comments

Comments
 (0)