From bf633fc5754f455cd9317f095b9cda967b5ed31f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 2 Jun 2019 17:29:17 -0700 Subject: [PATCH] Polish "Support JsonComponent key serializers/deserialzers" See gh-16544 --- .../boot/jackson/JsonComponent.java | 53 ++++---- .../boot/jackson/JsonComponentModule.java | 121 +++++++++--------- .../jackson/JsonComponentModuleTests.java | 6 +- .../boot/jackson/NameAndAge.java | 7 +- .../jackson/NameAndAgeJsonKeyComponent.java | 4 +- .../jackson/NameAndCareerJsonComponent.java | 4 +- 6 files changed, 99 insertions(+), 96 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java index 2732cd60b3..e11e0716d4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2019 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. @@ -30,11 +30,10 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; /** - * {@link Component} that provides {@link JsonSerializer} and/or {@link JsonDeserializer} - * implementations to be registered with Jackson when {@link JsonComponentModule} is in - * use. Can be used to annotate {@link JsonSerializer}, {@link JsonDeserializer}, or - * {@link KeyDeserializer} implementations directly or a class that contains them as - * inner-classes. For example:
+ * {@link Component} that provides {@link JsonSerializer}, {@link JsonDeserializer} or
+ * {@link KeyDeserializer} implementations to be registered with Jackson when
+ * {@link JsonComponentModule} is in use. Can be used to annotate implementations directly
+ * or a class that contains them as inner-classes. For example: 
  * @JsonComponent
  * public class CustomerJsonComponent {
  *
@@ -57,6 +56,7 @@ import org.springframework.stereotype.Component;
  * @see JsonComponentModule
  * @since 1.4.0
  * @author Phillip Webb
+ * @author Paul Aly
  */
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
@@ -73,33 +73,40 @@ public @interface JsonComponent {
 	String value() default "";
 
 	/**
-	 * Indicates whether the component should be registered as a type serializer and/or
-	 * deserializer or a key serializer and/or deserializer.
+	 * The types that are handled by the provided serializer/deserializer. This attribute
+	 * is mandatory for a {@link KeyDeserializer}, as the type cannot be inferred. For a
+	 * {@link JsonSerializer} or {@link JsonDeserializer} it can be used to limit handling
+	 * to a subclasses of type inferred from the generic.
+	 * @return the types that should be handled by the component
+	 * @since 2.2.0
+	 */
+	Class[] type() default {};
+
+	/**
+	 * The scope under which the serializer/deserializer should be registered with the
+	 * module.
 	 * @return the component's handle type
+	 * @since 2.2.0
 	 */
-	Handle handle() default Handle.TYPES;
+	Scope scope() default Scope.VALUES;
 
 	/**
-	 * Specify the classes handled by the serialization and/or deserialization of the
-	 * component. Necessary to be specified for a {@link KeyDeserializer}, as the type
-	 * cannot be inferred. On other types can be used to only handle a subset of
-	 * subclasses.
-	 * @return the classes that should be handled by the component
+	 * The various scopes under which a serializer/deserialzier can be registered.
+	 * @since 2.2.0
 	 */
-	Class[] handleClasses() default {};
-
-	/**
-	 * An enumeration of possible handling types for the component.
-	 */
-	enum Handle {
+	enum Scope {
 
 		/**
-		 * Register the component as a Type serializer and/or deserializer.
+		 * A serializer/deserializer for regular value content.
+		 * @see JsonSerializer
+		 * @see JsonDeserializer
 		 */
-		TYPES,
+		VALUES,
 
 		/**
-		 * Register the component as a Key serializer and/or deserializer.
+		 * A serializer/deserializer for keys.
+		 * @see JsonSerializer
+		 * @see KeyDeserializer
 		 */
 		KEYS
 
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
index 4f9145293e..750327900f 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2018 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -18,6 +18,7 @@ package org.springframework.boot.jackson;
 
 import java.lang.reflect.Modifier;
 import java.util.Map;
+import java.util.function.BiConsumer;
 
 import javax.annotation.PostConstruct;
 
@@ -27,13 +28,19 @@ import com.fasterxml.jackson.databind.KeyDeserializer;
 import com.fasterxml.jackson.databind.Module;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.BeanFactoryAware;
 import org.springframework.beans.factory.HierarchicalBeanFactory;
 import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.boot.jackson.JsonComponent.Scope;
 import org.springframework.core.ResolvableType;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
 
 /**
  * Spring Bean and Jackson {@link Module} to register {@link JsonComponent} annotated
@@ -70,88 +77,78 @@ public class JsonComponentModule extends SimpleModule implements BeanFactoryAwar
 		Map beans = beanFactory
 				.getBeansWithAnnotation(JsonComponent.class);
 		for (Object bean : beans.values()) {
-			JsonComponent annotation = AnnotationUtils.findAnnotation(bean.getClass(),
-					JsonComponent.class);
-			addJsonBean(bean, annotation);
+			addJsonBean(bean);
 		}
 	}
 
-	private void addJsonBean(Object bean, JsonComponent annotation) {
+	private void addJsonBean(Object bean) {
+		MergedAnnotation annotation = MergedAnnotations
+				.from(bean.getClass(), SearchStrategy.EXHAUSTIVE)
+				.get(JsonComponent.class);
+		Class[] types = annotation.getClassArray("type");
+		Scope scope = annotation.getEnum("scope", JsonComponent.Scope.class);
+		addJsonBean(bean, types, scope);
+	}
+
+	private void addJsonBean(Object bean, Class[] types, Scope scope) {
 		if (bean instanceof JsonSerializer) {
-			addSerializerForTypes((JsonSerializer) bean, annotation.handle(),
-					annotation.handleClasses());
+			addJsonSerializerBean((JsonSerializer) bean, scope, types);
 		}
-		if (bean instanceof KeyDeserializer) {
-			addKeyDeserializerForTypes((KeyDeserializer) bean,
-					annotation.handleClasses());
+		else if (bean instanceof JsonDeserializer) {
+			addJsonDeserializerBean((JsonDeserializer) bean, types);
 		}
-		if (bean instanceof JsonDeserializer) {
-			addDeserializerForTypes((JsonDeserializer) bean,
-					annotation.handleClasses());
+		else if (bean instanceof KeyDeserializer) {
+			addKeyDeserializerBean((KeyDeserializer) bean, types);
 		}
 		for (Class innerClass : bean.getClass().getDeclaredClasses()) {
-			if (!Modifier.isAbstract(innerClass.getModifiers())
-					&& (JsonSerializer.class.isAssignableFrom(innerClass)
-							|| JsonDeserializer.class.isAssignableFrom(innerClass)
-							|| KeyDeserializer.class.isAssignableFrom(innerClass))) {
-				try {
-					addJsonBean(innerClass.newInstance(), annotation);
-				}
-				catch (Exception ex) {
-					throw new IllegalStateException(ex);
-				}
+			if (isSuitableInnerClass(innerClass)) {
+				Object innerInstance = BeanUtils.instantiateClass(innerClass);
+				addJsonBean(innerInstance, types, scope);
 			}
 		}
 	}
 
-	@SuppressWarnings({ "unchecked" })
-	private  void addSerializerForTypes(JsonSerializer serializer,
-			JsonComponent.Handle handle, Class[] types) {
-		for (Class type : types) {
-			addSerializerWithType(serializer, handle, (Class) type);
-		}
-
-		if (types.length == 0) {
-			ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
-					serializer.getClass());
-			addSerializerWithType(serializer, handle, (Class) type.resolveGeneric());
-		}
+	private boolean isSuitableInnerClass(Class innerClass) {
+		return !Modifier.isAbstract(innerClass.getModifiers())
+				&& (JsonSerializer.class.isAssignableFrom(innerClass)
+						|| JsonDeserializer.class.isAssignableFrom(innerClass)
+						|| KeyDeserializer.class.isAssignableFrom(innerClass));
 	}
 
-	private  void addSerializerWithType(JsonSerializer serializer,
-			JsonComponent.Handle handle, Class type) {
-		if (JsonComponent.Handle.KEYS.equals(handle)) {
-			addKeySerializer(type, serializer);
-		}
-		else {
-			addSerializer(type, serializer);
-		}
+	@SuppressWarnings("unchecked")
+	private  void addJsonSerializerBean(JsonSerializer serializer,
+			JsonComponent.Scope scope, Class[] types) {
+		Class baseType = (Class) ResolvableType
+				.forClass(JsonSerializer.class, serializer.getClass()).resolveGeneric();
+		addBeanToModule(serializer, baseType, types,
+				(scope == Scope.VALUES) ? this::addSerializer : this::addKeySerializer);
+
 	}
 
-	@SuppressWarnings({ "unchecked" })
-	private  void addDeserializerForTypes(JsonDeserializer deserializer,
+	@SuppressWarnings("unchecked")
+	private  void addJsonDeserializerBean(JsonDeserializer deserializer,
 			Class[] types) {
-		for (Class type : types) {
-			addDeserializer((Class) type, deserializer);
-		}
-
-		if (types.length == 0) {
-			addDeserializerWithDeducedType(deserializer);
-		}
+		Class baseType = (Class) ResolvableType
+				.forClass(JsonDeserializer.class, deserializer.getClass())
+				.resolveGeneric();
+		addBeanToModule(deserializer, baseType, types, this::addDeserializer);
 	}
 
-	@SuppressWarnings({ "unchecked" })
-	private  void addDeserializerWithDeducedType(JsonDeserializer deserializer) {
-		ResolvableType type = ResolvableType.forClass(JsonDeserializer.class,
-				deserializer.getClass());
-		addDeserializer((Class) type.resolveGeneric(), deserializer);
-
+	private void addKeyDeserializerBean(KeyDeserializer deserializer, Class[] types) {
+		Assert.notEmpty(types, "Type must be specified for KeyDeserializer");
+		addBeanToModule(deserializer, Object.class, types, this::addKeyDeserializer);
 	}
 
-	private void addKeyDeserializerForTypes(KeyDeserializer deserializer,
-			Class[] types) {
+	@SuppressWarnings("unchecked")
+	private  void addBeanToModule(E element, Class baseType, Class[] types,
+			BiConsumer, E> consumer) {
+		if (ObjectUtils.isEmpty(types)) {
+			consumer.accept(baseType, element);
+			return;
+		}
 		for (Class type : types) {
-			addKeyDeserializer(type, deserializer);
+			Assert.isAssignable(baseType, type);
+			consumer.accept((Class) type, element);
 		}
 	}
 
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
index 3b2d01e429..7a9af5f2df 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2018 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -198,12 +198,12 @@ public class JsonComponentModuleTests {
 
 	}
 
-	@JsonComponent(handle = JsonComponent.Handle.KEYS)
+	@JsonComponent(scope = JsonComponent.Scope.KEYS)
 	static class OnlyKeySerializer extends NameAndAgeJsonKeyComponent.Serializer {
 
 	}
 
-	@JsonComponent(handle = JsonComponent.Handle.KEYS, handleClasses = NameAndAge.class)
+	@JsonComponent(scope = JsonComponent.Scope.KEYS, type = NameAndAge.class)
 	static class OnlyKeyDeserializer extends NameAndAgeJsonKeyComponent.Deserializer {
 
 	}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
index f343c1cc2a..7935b13723 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2017 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -22,6 +22,7 @@ import org.springframework.util.ObjectUtils;
  * Sample object used for tests.
  *
  * @author Phillip Webb
+ * @author Paul Aly
  */
 public final class NameAndAge extends Name {
 
@@ -37,7 +38,7 @@ public final class NameAndAge extends Name {
 	}
 
 	public String asKey() {
-		return name + " is " + age;
+		return this.name + " is " + this.age;
 	}
 
 	@Override
@@ -48,7 +49,6 @@ public final class NameAndAge extends Name {
 		if (obj == null) {
 			return false;
 		}
-
 		if (obj instanceof NameAndAge) {
 			NameAndAge other = (NameAndAge) obj;
 			boolean rtn = true;
@@ -56,7 +56,6 @@ public final class NameAndAge extends Name {
 			rtn = rtn && ObjectUtils.nullSafeEquals(this.age, other.age);
 			return rtn;
 		}
-
 		return super.equals(obj);
 	}
 
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
index bda3f435a1..10d2dc2d6b 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2017 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -29,7 +29,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
  *
  * @author Paul Aly
  */
-@JsonComponent(handle = JsonComponent.Handle.KEYS, handleClasses = NameAndAge.class)
+@JsonComponent(type = NameAndAge.class, scope = JsonComponent.Scope.KEYS)
 public class NameAndAgeJsonKeyComponent {
 
 	public static class Serializer extends JsonSerializer {
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
index 778705d883..9eeafc17a5 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2017 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -30,7 +30,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
  *
  * @author Paul Aly
  */
-@JsonComponent(handleClasses = NameAndCareer.class)
+@JsonComponent(type = NameAndCareer.class)
 public class NameAndCareerJsonComponent {
 
 	public static class Serializer extends JsonObjectSerializer {