Как правильно переопределить JacksonAnnotationIntrospector._findAnnotation, чтобы заменить аннотацию элемента - PullRequest
2 голосов
/ 22 октября 2019

Я пытаюсь создать несколько классов, сериализуемых Джексоном. Я хочу аннотировать некоторые элементы стандартной аннотацией Джексона (давайте рассмотрим JsonIgnore для этого примера), но я хочу, чтобы они имели эффект только в моем конкретном маппере.

Поэтому я решил создать свои собственные аннотации, подобные стандартным(например, MyJsonIgnore) и обрабатывать их только в интроспекторе аннотаций, используемом моим картографом. Я нашел переопределенный метод _findAnnotation. Javadoc говорит следующее:

...overridable that sub-classes may, if they choose to, 
mangle actual access block access ("hide" annotations) 
or perhaps change it.

Я нашел способ заблокировать некоторые аннотации (однако это включает переопределение _hasAnnotation, а не _findAnnotation), но я полностью застрял с изменением аннотаций.

Вот некоторый минимальный пример того, что я пытаюсь сделать:

object Mappers {
    /**
     * Same as JsonIgnore but mapper-specific
     */
    annotation class MyJsonIgnore(val value: Boolean = true)

    private class MyIntrospector : JacksonAnnotationIntrospector() {
        override fun <A : Annotation> _findAnnotation(
            annotated: Annotated,
            annoClass: Class<A>
        ): A {
            if (annoClass == JsonIgnore::class.java && _hasAnnotation(annotated, MyJsonIgnore::class.java)) {
                val mji = _findAnnotation(annotated, MyJsonIgnore::class.java)
                return JsonIgnore(mji.value) // Does not compile, type mismatch
                // return JsonIgnore(mji.value) as A // Does not compile, annotation class cannot be instantiated, same as Java, see below
            }
            return super._findAnnotation(annotated, annoClass)
        }
    }

    fun myMapper(): ObjectMapper {
        return ObjectMapper().setAnnotationIntrospector(MyIntrospector())
    }
}

Я также не могу сделать это с помощью Java:

public class Mappers {
    /**
     * Same as JsonIgnore but mapper-specific
     */
    public @interface MyJsonIgnore {
        boolean value() default true;
    }

    private static class MyIntrospector extends JacksonAnnotationIntrospector {
        @Override
        protected <A extends Annotation> A _findAnnotation(Annotated annotated,
                                                           Class<A> annoClass) {
            if (annoClass == JsonIgnore.class && _hasAnnotation(annotated, MyJsonIgnore.class)) {
                MyJsonIgnore mji = _findAnnotation(annotated, MyJsonIgnore.class);
                return new JsonIgnore(mji.value()); // Does not compile, JsonIgnore is abstract
            }
            return super._findAnnotation(annotated, annoClass);
        }
    }

    static ObjectMapper myMapper() {
        return new ObjectMapper().setAnnotationIntrospector(new MyIntrospector())
    }
}

Так, каков предполагаемый способ изменитьаннотации путем переопределения этого метода? Есть ли? Является ли мой подход правильным или я должен сделать это по-другому?

Ответы [ 2 ]

2 голосов
/ 22 октября 2019

Основная проблема в том, что вы не можете создать экземпляр класса аннотаций. Однако есть одно решение: вы можете хранить одну аннотацию как элемент другой аннотации следующим образом:

@Retention(AnnotationRetention.RUNTIME) // don't forget 
@Target(AnnotationTarget.FIELD)         // these annotations
annotation class MyJsonIgnore(val value: Boolean = true, val jsonIgnore: JsonIgnore = JsonIgnore())

Так что MyJsonIgnore будет иметь экземпляр JsonIgnore внутри. И тогда вы можете использовать его в своем AnnotationIntrospector:

private class MyIntrospector : JacksonAnnotationIntrospector() {
    override fun <A : Annotation> _findAnnotation(
            annotated: Annotated,
            annoClass: Class<A>
    ): A? {
        if (annoClass == JsonIgnore::class.java && _hasAnnotation(annotated, MyJsonIgnore::class.java)) {
            val mji = _findAnnotation(annotated, MyJsonIgnore::class.java)
            if (mji?.value == true) {
                return mji.jsonIgnore as A // this cast should be safe because we have checked
                                           // the annotation class
            }
        }
        return super._findAnnotation(annotated, annoClass)
    }
}

Я проверил это со следующим классом

class Test {
    @MyJsonIgnore
    val ignoreMe = "IGNORE"
    val field = "NOT IGNORE"
}

и методом

fun main() {
    println(Mappers.myMapper().writeValueAsString(Test()))
    println(ObjectMapper().writeValueAsString(Test()))
}

и на выходе было

{"field":"NOT IGNORE"}
{"ignoreMe":"IGNORE","field":"NOT IGNORE"}
0 голосов
/ 24 октября 2019

Итак, вот мои дальнейшие мысли. Ответ Кирилла Симонова является правильным и безопасным с точки зрения типов (альтернативой может быть создание экземпляра аннотации с использованием отражения Kotlin, но это не безопасно).

Вот некоторые проблемы с моим исходным кодом и мысли об исходном подходе:

  1. Вы должны последовательно переопределить _hasAnnotation и _getAnnotation

Вы не можете быть уверены, что _getAnnotation будет вызван после проверки _hasAnnotation. Вы не можете быть уверены, какой из них будет использоваться для проверки замененной аннотации (в моем случае @JsonIgnore), не просматривая код JacksonAnnotationIntrospector. Кажется, что их постоянное переопределение было бы хорошей практикой. Поэтому мы должны также добавить следующее переопределение в наш класс, если мы хотим использовать этот подход:

override fun <A : Annotation> _hasAnnotation(
    annotated: Annotated,
    annoClass: Class<A>
): Boolean {
    if (annoClass == JsonIgnore::class.java && _hasAnnotation(annotated, MyJsonIgnore::class.java)) {
        return true
    }
    return super._hasAnnotation(annotated, annoClass)
}
_getAnnotation тип возвращаемого значения должен быть обнуляемым

Это было исправлено Кириллом, но не было явно указано. _getAnnotation может и иногда возвращает ноль.

Вы (возможно) не можете иметь одну магическую аннотацию MyConditional.

Ответ Кирилла может побудить вас создать нечто вроде условной аннотации для всех аннотаций Джексона, которая могла быиспользовать следующим образом:

@MyConditional([
    JsonIgnore, JsonProperty("propertyName")
])

У вас просто не может быть параметра полиморфной аннотации. Вам нужно будет создать My* для каждой необходимой вам аннотации Джексона, а для аннотации с параметрами она будет не такой аккуратной, как с @MyJsonIgnore.

Вы можете попытаться сделать повторяемую аннотацию, которая будет применятьсякак показано ниже и реализовано с использованием отражения.

@MyConditional(
    clazz = JsonProperty::class.java,
    args = [
        // Ah, you probably cannot have an array of any possible args here, forget it.
    ]
)

_hasAnnotation и _getAnnotation - не единственные способы, которые JacksonAnnotationIntrospector использует для получения или проверки аннотаций

После использования аналогичного подхода для создания условных @JsonProperty аннотация, я заметил, что это не работает для элементов enum. После некоторой отладки я обнаружил, что метод findEnumValues использует java.lang.reflect.Field::getAnnotation напрямую (из-за "различных причин", упомянутых в устаревшем findEnumValue). Если вы хотите, чтобы ваша условная аннотация работала, вы должны переопределить (как минимум) findEnumValues.

Будьте осторожны с ObjectMapper::setAnnotationIntrospector

Ну, его Javadoc прямо говорит: будьте осторожны. Он заменяет весь интроспектор аннотации вашего картографа, удаляя все добавленные (прикованные) модулями интроспекторов. Он не появился в моем коде в вопросе (это было сделано для создания минимального примера), но на самом деле я случайно сломал десериализацию с KotlinModule. Вы можете рассмотреть возможность реализации JacksonModule и добавления вашего интроспектора к существующим.

Рассмотрим другой подход: реализация функционально-ориентированных методов в NopAnnotationIntrospector.

В конце я остановился на этом подходе (в основном из-за 4.). Мне нужно было переопределить findEnumValues и hasIgnoreMarker, и этого мне было достаточно. Он включал в себя немного кода копирования-вставки из JacksonAnnotationMapper, но если вам не нужно сделать большую часть аннотации условной, это может сработать (в любом случае, реализация этого требует большого количества стандартного кода). Таким образом, вполне вероятно, что вы действительно хотите связать этот интроспектор, а не set его.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...