Merge pull request #4 from banjjoknim/jackson

Jackson 커스텀 직렬화 예제 코드 추가
This commit is contained in:
Colt
2022-03-13 22:09:24 +09:00
committed by GitHub
5 changed files with 466 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
package com.banjjoknim.playground.jackson.common
import com.banjjoknim.playground.jackson.jsonserialize.UsingJsonSerializeAnnotationCarSerializer
import com.fasterxml.jackson.databind.annotation.JsonSerialize
data class Car(
val name: String,
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingNoAnnotation(
val name: String = "banjjoknim",
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingJsonSerializeAnnotation(
val name: String = "banjjoknim",
@JsonSerialize(using = UsingJsonSerializeAnnotationCarSerializer::class)
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)
data class CarUsingSecretAnnotation(
val name: String = "banjjoknim",
@field:Secret("****")
val secret: String = "secret",
val price: Int = 10000000,
val owner: Owner = Owner()
)

View File

@@ -0,0 +1,3 @@
package com.banjjoknim.playground.jackson.common
data class Owner(val name: String = "ban", val age: Int = 30)

View File

@@ -0,0 +1,16 @@
package com.banjjoknim.playground.jackson.common
import com.fasterxml.jackson.annotation.JacksonAnnotation
/**
* [jackson-annotations](https://www.baeldung.com/jackson-annotations) 참고.
*
* @see com.fasterxml.jackson.annotation.JacksonAnnotation
* @see com.fasterxml.jackson.annotation.JacksonAnnotationsInside
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD) // 현재 상황에서는 PROPERTY 로 적용할 경우 제대로 적용되지 않는다. 아마 어노테이션 자체가 자바 기반으로 사용되어 PROPERTY 를 인식하지 못하는 것 같다(자바에서는 PROPERTY 타입을 사용할 수 없음).
@JacksonAnnotation // NOTE: important; MUST be considered a 'Jackson' annotation to be seen(or recognized otherwise via AnnotationIntrospect.isHandled())
annotation class Secret(
val substituteValue: String = ""
)

View File

@@ -0,0 +1,158 @@
package com.banjjoknim.playground.jackson.jsonserialize
import com.banjjoknim.playground.jackson.common.Car
import com.banjjoknim.playground.jackson.common.Secret
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.AnnotatedMember
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier
import com.fasterxml.jackson.databind.ser.std.StdSerializer
/**
* [jackson-object-mapper-tutorial](https://www.baeldung.com/jackson-object-mapper-tutorial) 참고.
*
* [how-to-(de)serialize-field-from-object-based-on-annotation-using-jackson](https://stackoverflow.com/questions/18659835/how-to-deserialize-field-from-object-based-on-annotation-using-jackson)
*
* Custom Serializer 를 만들기 위해서는 아래와 같이 StdSerializer<T> 를 상속해야 한다.
*
* 만약 어노테이션을 이용한 설정 또는 프로퍼티마다 다르게 작동하는 Serializer 를 만들고 싶다면 JsonSerializer 의 add-on interface 인 ContextualSerializer 를 구현하면 된다.
*
* JsonSerializer<T> 만 확장할 경우엔 애노테이션 정보를 얻을 수 없다. 추가적으로 ContextualSerializer 인터페이스를 구현해주면 createContextual() 메서드를 구현해줘야 하는데 두번째 인자로 넘어오는 BeanProperty 를 이용해 애노테이션 정보를 구할 수 있다.
*
* ContexutalSerializer 를 사용하는 방법은 [TestContextualSerialization](https://github.com/FasterXML/jackson-databind/blob/master/src/test/java/com/fasterxml/jackson/databind/contextual/TestContextualSerialization.java) 참고하도록 한다.
*
* Custom Serializer 가 JsonSerializer<T> 와 ContextualSerialier 를 모두 구현할 경우 createContextual() 함수가 먼저 호출된다.
*
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer
* @see com.fasterxml.jackson.databind.ser.std.StringSerializer
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer
*/
class CarSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeNumberField("price", value.price)
gen.writeEndObject()
}
}
class CarNameSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeEndObject()
}
}
class CarPriceSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeNumberField("price", value.price)
gen.writeEndObject()
}
}
class CarNameOwnerSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeObjectField("owner", value.owner)
gen.writeEndObject()
}
}
class CarNameOwnerNameSerializer : StdSerializer<Car>(Car::class.java) {
override fun serialize(value: Car, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeStartObject()
gen.writeStringField("name", value.name)
gen.writeObjectFieldStart("owner")
gen.writeStringField("name", value.owner.name)
gen.writeEndObject()
}
}
class UsingJsonSerializeAnnotationCarSerializer : StdSerializer<String>(String::class.java) {
override fun serialize(value: String, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeString("****")
}
}
/**
* AnnotationIntrospector 를 상속한 JacksonAnnotationIntrospector 은 Jackson 라이브러리가 직렬화/역직렬화시 `JacksonAnnotation` 정보를 어떻게 처리할지에 대한 정보가 정의되어 있는 클래스다.
*
* 따라서 어노테이션별로 어떻게 처리할지 재정의하고 싶다면 이 녀석을 override 해준뒤 ObjectMapper 에 등록해주면 된다.
*
* 등록할 때는 `ObjectMapper#setAnnotationIntrospector()` 를 사용한다.
*
* [FasterXML - AnnotationIntrospector](https://github.com/FasterXML/jackson-docs/wiki/AnnotationIntrospector)
*
* @see com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector#
* @see com.fasterxml.jackson.databind.ObjectMapper
*
*/
class SecretAnnotationIntrospector : JacksonAnnotationIntrospector() {
/**
*
* `@JsonIgnore` 를 적용했을 때 무시할지 여부를 판단하는 함수이다.
*
* 따라서 직렬화 / 역직렬화시 무시하고 싶은 프로퍼티가 있다면 이 함수를 override 하면 된다.
*/
override fun hasIgnoreMarker(m: AnnotatedMember): Boolean {
return super.hasIgnoreMarker(m)
}
/**
* `@JsonSerailize` 가 붙은 어노테이션의 처리를 재정의할 때 override 하는 함수이다.
*
* 자세한 내용은 JacksonAnnotationIntrospector#findSerializer() 의 구현을 살펴보도록 한다.
*
* 특정 프로퍼티에 대해 어떤 Serializer 를 사용할 것인지 결정하는 함수이다.
*
* 따라서 특정 조건에 따라 직렬화 처리에 사용할 Serializer 를 정의하고 싶다면 이 함수를 override 하면 된다.
*/
override fun findSerializer(a: Annotated): Any? {
val annotation = a.getAnnotation(Secret::class.java)
if (annotation != null) {
return SecretAnnotationSerializer(annotation.substituteValue)
}
return super.findSerializer(a) // 기존 JacksonAnnotationIntrospector 의 것을 사용한다.
}
}
class SecretAnnotationSerializer(private val substituteValue: String) : StdSerializer<String>(String::class.java) {
override fun serialize(value: String, gen: JsonGenerator, provider: SerializerProvider) {
gen.writeString(substituteValue)
}
}
/**
* 직렬화될 프로퍼티를 수정하도록 하는 방법이다.
*
* BeanSerializerModifier#changeProperties() 를 재정의한 뒤,
*
* SimpleModule 을 이용해서 ObjectMapper 에 등록한다.
*
* 아래처럼 하면 직렬화 대상에서 완전히 제외된다(`@JsonIgnore`와 동일한 효과).
*
* @see com.fasterxml.jackson.databind.ser.BeanSerializerModifier
* @see com.fasterxml.jackson.databind.module.SimpleModule
*/
class SecretBeanSerializerModifier : BeanSerializerModifier() {
override fun changeProperties(
config: SerializationConfig,
beanDesc: BeanDescription,
beanProperties: MutableList<BeanPropertyWriter>
): MutableList<BeanPropertyWriter> {
SimpleModule()
return beanProperties
.filter { property -> property.getAnnotation(Secret::class.java) == null }
.toMutableList()
// return super.changeProperties(config, beanDesc, beanProperties)
}
}

View File

@@ -0,0 +1,256 @@
package com.banjjoknim.playground.jackson.jsonserialize
import com.banjjoknim.playground.jackson.common.Car
import com.banjjoknim.playground.jackson.common.CarUsingJsonSerializeAnnotation
import com.banjjoknim.playground.jackson.common.CarUsingNoAnnotation
import com.banjjoknim.playground.jackson.common.CarUsingSecretAnnotation
import com.banjjoknim.playground.jackson.common.Owner
import com.fasterxml.jackson.databind.AnnotationIntrospector
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
/**
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.module.SimpleModule
* @see com.fasterxml.jackson.databind.ser.DefaultSerializerProvider
* @see com.fasterxml.jackson.databind.ser.BeanSerializer -> 객체를 직렬화할때 사용되는 Serializer
* @see com.fasterxml.jackson.core.json.WriterBasedJsonGenerator
* @see com.fasterxml.jackson.databind.ser.std.BeanSerializerBase
* @see com.fasterxml.jackson.databind.ser.BeanPropertyWriter
*/
class CarSerializersTest {
private lateinit var mapper: ObjectMapper
companion object {
private val owner = Owner("ban", 30)
private val car = Car("banjjoknim", 10_000_000, owner)
private val carUsingNoAnnotation = CarUsingNoAnnotation()
private val carUsingJsonSerializeAnnotation = CarUsingJsonSerializeAnnotation()
private val carUsingSecretAnnotation = CarUsingSecretAnnotation()
}
@BeforeEach
fun setup() {
mapper = ObjectMapper().registerKotlinModule()
}
@Test
fun `기본 ObjectMapper의 동작을 테스트한다`() {
// given
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@DisplayName("등록된 커스텀 직렬화기의 동작을 테스트한다")
@Nested
inner class CarSerializerTestCases {
@Test
fun `자동차의 모든 필드만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","price":10000000}""")
}
@Test
fun `자동차의 이름만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarNameSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim"}""")
}
@Test
fun `자동차의 가격만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarPriceSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"price":10000000}""")
}
@Test
fun `자동차의 이름과 오너의 모든 필드를 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarNameOwnerSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","owner":{"name":"ban","age":30}}""")
}
@Test
fun `자동차의 이름과 오너의 이름만 직렬화한다`() {
// given
val module = SimpleModule()
module.addSerializer(Car::class.java, CarNameOwnerNameSerializer())
mapper.registerModule(module)
// when
val result = mapper.writeValueAsString(car)
// then
assertThat(result).isEqualTo("""{"name":"banjjoknim","owner":{"name":"ban"}}""")
}
@Test
fun `아무 어노테이션도 적용하지 않고 직렬화한다`() {
// given
// when
val actual = mapper.writeValueAsString(carUsingNoAnnotation)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","secret":"secret","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@Test
fun `@JsonSerialize 어노테이션을 적용하여 직렬화한다`() {
// given
// when
val actual = mapper.writeValueAsString(carUsingJsonSerializeAnnotation)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","secret":"****","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@Test
fun `@Secret 어노테이션, AnnotationIntrospector 을 적용하여 직렬화한다`() {
// given
mapper.setAnnotationIntrospector(SecretAnnotationIntrospector())
// when
val actual = mapper.writeValueAsString(carUsingSecretAnnotation)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","secret":"****","price":10000000,"owner":{"name":"ban","age":30}}""")
}
@Test
fun `@Secret 어노테이션, BeanSerializerModifier 를 적용하여 직렬화한다`() {
// given
val module = object : SimpleModule() {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.addBeanSerializerModifier(SecretBeanSerializerModifier())
}
}
mapper.registerModule(module)
// when
val actual = mapper.writeValueAsString(carUsingSecretAnnotation)
// then
assertThat(actual).isEqualTo("""{"name":"banjjoknim","price":10000000,"owner":{"name":"ban","age":30}}""")
}
/**
*
* Kotlin + Spring Boot 를 사용한다면 `com.fasterxml.jackson.module:jackson-module-kotlin` 의존성을 사용할 것이다.
*
* 이를 사용하면 기본 생성자 없이도 `@RequestBody` 에서 json 을 객체로 역직렬화 할 수 있다.
*
* `com.fasterxml.jackson.module:jackson-module-kotlin` 에서 이러한 역할을 해주는 것이 KotlinAnnotationIntrospector 이다.
*
* 하지만 새로운 AnnotationIntrospector 를 등록하면 KotlinAnnotationIntrospector 가 무시되어 기본생성자 없이는 `@RequestBody` 객체를 만들지 못하게 된다.
*
* 따라서 아래와 같이 기존의 AnnotationIntrospector 도 등록해주어야 한다.
*
* 이는 AnnotationIntrospector.Pair 도우미 클래스를 사용해서 할 수 있다.
*
* 이때, 순서대로 기본 Introspector, 보조 Introspector 로 등록된다.
*
* [AnnotationIntrospector](https://github.com/FasterXML/jackson-docs/wiki/AnnotationIntrospector)
*
* @see com.fasterxml.jackson.databind.ObjectMapper
* @see com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair
* @see com.fasterxml.jackson.databind.AnnotationIntrospector
* @see com.fasterxml.jackson.module.kotlin.KotlinAnnotationIntrospector
*/
@Test
fun `Kotlin + Spring Boot를 사용하면 기본적으로 3가지의 AnnotationIntrospector 가 ObjectMapper 에 존재한다`() {
// given
val originalAnnotationIntrospector = mapper.serializationConfig.annotationIntrospector
// when
val allIntrospectorNames = mapper.serializationConfig.annotationIntrospector.allIntrospectors()
.map { annotationIntrospector -> annotationIntrospector::class.simpleName }
// then
assertThat(originalAnnotationIntrospector.allIntrospectors()).hasSize(3)
assertThat(allIntrospectorNames[0]).isEqualTo("KotlinAnnotationIntrospector")
assertThat(allIntrospectorNames[1]).isEqualTo("JacksonAnnotationIntrospector")
assertThat(allIntrospectorNames[2]).isEqualTo("KotlinNamesAnnotationIntrospector")
}
@Test
fun `Kotlin + Spring Boot 를 사용할 시 ObjectMapper 에 새로운 AnnotationIntrospector 를 추가하면 KotlinAnnotationIntrospector 가 무시된다`() {
// given
val originalAnnotationIntrospector = mapper.serializationConfig.annotationIntrospector
// when
mapper.setAnnotationIntrospector(SecretAnnotationIntrospector())
val allIntrospectorNames = mapper.serializationConfig.annotationIntrospector.allIntrospectors()
.map { annotationIntrospector -> annotationIntrospector::class.simpleName }
// then
assertThat(originalAnnotationIntrospector.allIntrospectors()).hasSize(3)
assertThat(allIntrospectorNames).hasSize(1)
assertThat(allIntrospectorNames[0]).isEqualTo("SecretAnnotationIntrospector")
}
@Test
fun `Kotlin + Spring Boot 를 사용할 시 ObjectMapper 에 새로운 AnnotationIntrospector 를 추가할 때 Pair 로 추가하면 KotlinAnnotationIntrospector 가 무시되지 않는다`() {
// given
val originalAnnotationIntrospector = mapper.serializationConfig.annotationIntrospector
// when
mapper.setAnnotationIntrospector(
AnnotationIntrospector.pair(SecretAnnotationIntrospector(), originalAnnotationIntrospector) // 내부 구현은 아래와 같다.
// AnnotationIntrospectorPair(SecretAnnotationIntrospector(), originalAnnotationIntrospector)
)
val allIntrospectorNames = mapper.serializationConfig.annotationIntrospector.allIntrospectors()
.map { annotationIntrospector -> annotationIntrospector::class.simpleName }
// then
assertThat(allIntrospectorNames).hasSize(4)
assertThat(allIntrospectorNames[0]).isEqualTo("SecretAnnotationIntrospector")
assertThat(allIntrospectorNames[1]).isEqualTo("KotlinAnnotationIntrospector")
assertThat(allIntrospectorNames[2]).isEqualTo("JacksonAnnotationIntrospector")
assertThat(allIntrospectorNames[3]).isEqualTo("KotlinNamesAnnotationIntrospector")
}
}
}