Как преобразовать интерфейс аннотации в JSON? - PullRequest
0 голосов
/ 04 мая 2020

Я пытаюсь прочитать пользовательское значение аннотации, загруженное через другой загрузчик классов.

Как преобразовать объект аннотации в json?

@Retention(RententionPolicy.RUNTIME)
public @interface SendEmail{
public String id;
}

Gson gson = new Gson();

Object object = (Object)annotation // this holds SendEmail object loaded from different classloader

gson.toJson(object); 

//I get UnsupportedOperationException: Attempted to Serialize java.lang.Class: SendEmail. Forget to register a type adapter?

Что такое адаптер типа для использоваться для интерфейсов?

1 Ответ

1 голос
/ 05 мая 2020

Я не совсем уверен, почему вам нужно сериализовать экземпляры аннотаций, которые являются постоянными по конструкции, но вы, скорее всего, используете JVM Oracle, который создает аннотации с использованием java.lang.proxy.Proxy. Прокси обрабатываются как объекты данных с отражающим доступом в Gson (и в стандартном пакете Gson нет упоминаний о прокси), и Gson просто не удается сериализовать java.lang.Class, что не имеет большого смысла в вашем сценарии.

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

final class AnnotationTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory instance = new AnnotationTypeAdapterFactory();

    private AnnotationTypeAdapterFactory() {
    }

    static TypeAdapterFactory getInstance() {
        return instance;
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        @Nullable
        final Class<? extends Annotation> annotationClass = Annotations.lookupAnnotationClass(typeToken.getRawType());
        if ( annotationClass == null ) {
            return null;
        }
        final List<Method> methods = Annotations.lookupMethods(annotationClass);
        final int count = methods.size();
        final String[] names = new String[count];
        @SuppressWarnings("unchecked")
        final TypeAdapter<Object>[] typeAdapters = new TypeAdapter[count];
        final Map<String, TypeAdapter<Object>> namedTypeAdapters = new HashMap<>();
        for ( int i = 0; i < count; i++ ) {
            final Method method = methods.get(i);
            names[i] = method.getName();
            @SuppressWarnings({ "unchecked", "rawtypes" })
            final TypeAdapter<Object> typeAdapter = (TypeAdapter) gson.getAdapter(method.getReturnType());
            typeAdapters[i] = typeAdapter;
            namedTypeAdapters.put(names[i], typeAdapter);
        }
        final TypeAdapter<T> typeAdapter = new TypeAdapter<T>() {
            @Override
            @SuppressWarnings("resource")
            public void write(final JsonWriter out, final T annotation)
                    throws IOException {
                try {
                    out.beginObject();
                    for ( int i = 0; i < count; i++ ) {
                        out.name(names[i]);
                        typeAdapters[i].write(out, methods.get(i).invoke(annotation));
                    }
                    out.endObject();
                } catch ( final IllegalAccessException | InvocationTargetException ex ) {
                    throw new RuntimeException(ex);
                }
            }

            @Override
            public T read(final JsonReader in)
                    throws IOException {
                try {
                    in.beginObject();
                    final Map<String, Object> properties = new HashMap<>();
                    while ( in.hasNext() ) {
                        final String name = in.nextName();
                        @Nullable
                        final TypeAdapter<Object> objectTypeAdapter = namedTypeAdapters.get(name);
                        if ( objectTypeAdapter == null ) {
                            in.skipValue();
                        } else {
                            properties.put(name, objectTypeAdapter.read(in));
                        }
                    }
                    in.endObject();
                    @SuppressWarnings("unchecked")
                    final T annotation = (T) Annotations.create(annotationClass, properties);
                    return annotation;
                } catch ( final NoSuchMethodException ex ) {
                    throw new RuntimeException(ex);
                }
            }
        };
        return typeAdapter.nullSafe();
    }

}

где класс Annotations выглядит следующим образом:

final class Annotations {

    private static final boolean SUN_PACKAGE = false;

    private Annotations() {
    }

    static <T extends Annotation> T create(final Class<T> annotationClass, final Map<String, Object> properties)
            throws NoSuchMethodException {
        return create(annotationClass.getClassLoader(), annotationClass, properties);
    }

    static <T extends Annotation> T create(final ClassLoader classLoader, final Class<T> annotationClass, final Map<String, Object> properties)
            throws NoSuchMethodException {
        if ( SUN_PACKAGE ) {
            @SuppressWarnings("unchecked")
            final T annotation = (T) AnnotationParser.annotationForMap(annotationClass, properties);
            return annotation;
        }
        @SuppressWarnings("unchecked")
        final T annotation = (T) Proxy.newProxyInstance(
                classLoader,
                new Class<?>[]{ annotationClass },
                DynamicAnnotation.fromMap(annotationClass, lookupProperties(annotationClass, properties))
        );
        return annotation;
    }

    @Nullable
    static Class<? extends Annotation> lookupAnnotationClass(final Class<?> clazz) {
        if ( clazz.isAnnotation() ) {
            @SuppressWarnings("unchecked")
            final Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) clazz;
            return annotationClass;
        }
        final Class<?>[] interfaces = clazz.getInterfaces();
        if ( interfaces.length != 1 ) {
            return null;
        }
        final Class<?> iface = interfaces[0];
        if ( !Annotation.class.isAssignableFrom(iface) ) {
            return null;
        }
        @SuppressWarnings("unchecked")
        final Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) iface;
        return annotationClass;
    }

    static List<Method> lookupMethods(final Class<? extends Annotation> annotationClass) {
        final List<Method> methods = new ArrayList<>();
        for ( final Method method : annotationClass.getMethods() ) {
            if ( method.getDeclaringClass() == annotationClass ) {
                methods.add(method);
            }
        }
        return Collections.unmodifiableList(methods);
    }

    static Map<String, Object> lookupProperties(final Class<? extends Annotation> annotationClass, final Map<String, Object> properties) {
        final Map<String, Object> namedProperties = new HashMap<>();
        namedProperties.putAll(lookupDefaultProperties(annotationClass));
        namedProperties.putAll(properties);
        return Collections.unmodifiableMap(namedProperties);
    }

    static Map<String, Object> lookupDefaultProperties(final Class<? extends Annotation> annotationClass) {
        final Map<String, Object> defaultProperties = new HashMap<>();
        for ( final Method method : lookupMethods(annotationClass) ) {
            @Nullable
            final Object defaultValue = method.getDefaultValue();
            if ( defaultValue != null ) {
                defaultProperties.put(method.getName(), defaultValue);
            }
        }
        return Collections.unmodifiableMap(defaultProperties);
    }

    static String toString(@SuppressWarnings("TypeMayBeWeakened") final Class<? extends Annotation> annotationClass, final Map<String, Object> properties) {
        final StringBuilder builder = new StringBuilder("@")
                .append(annotationClass.getTypeName())
                .append('(');
        boolean atTail = false;
        for ( final Map.Entry<String, Object> e : properties.entrySet() ) {
            if ( atTail ) {
                builder.append(", ");
            }
            builder.append(e.getKey())
                    .append('=')
                    .append(e.getValue());
            atTail = true;
        }
        return builder.append(')')
                .toString();
    }

}

и пользовательская реализация аннотаций:

abstract class DynamicAnnotation
        implements Annotation, InvocationHandler {

    private static final Method java_lang_Object_equals;
    private static final Method java_lang_Object_hashCode;
    private static final Method java_lang_Object_toString;
    private static final Method java_lang_annotation_Annotation_annotationType;

    static {
        try {
            java_lang_Object_equals = Object.class.getDeclaredMethod("equals", Object.class);
            java_lang_Object_hashCode = Object.class.getDeclaredMethod("hashCode");
            java_lang_Object_toString = Object.class.getDeclaredMethod("toString");
            java_lang_annotation_Annotation_annotationType = Annotation.class.getDeclaredMethod("annotationType");
        } catch ( final NoSuchMethodException ex ) {
            throw new Error(ex);
        }
    }

    private final String toString;
    private final Class<? extends Annotation> annotationClass;

    private DynamicAnnotation(final String toString, final Class<? extends Annotation> annotationClass) {
        this.toString = toString;
        this.annotationClass = annotationClass;
    }

    static DynamicAnnotation fromMap(final Class<? extends Annotation> annotationClass, final Map<String, Object> properties)
            throws NoSuchMethodException {
        return FromMap.create(annotationClass, properties);
    }

    @Nullable
    protected abstract Object invoke(final Method method)
            throws Throwable;

    @Override
    public final Class<? extends Annotation> annotationType() {
        return annotationClass;
    }

    // must conform the https://docs.oracle.com/javase/6/docs/api/java/lang/annotation/Annotation.html#hashCode() contract
    @Override
    public final int hashCode() {
        //return hashCode;
        throw new UnsupportedOperationException();
    }

    // must conform the https://docs.oracle.com/javase/6/docs/api/java/lang/annotation/Annotation.html#equals(java.lang.Object) contract
    @Override
    public final boolean equals(@Nullable final Object obj) {
        throw new UnsupportedOperationException();
    }

    @Override
    public final String toString() {
        return toString;
    }

    @Override
    @Nonnull
    public final Object invoke(final Object proxy, final Method method, final Object[] args)
            throws Throwable {
        if ( method.equals(java_lang_annotation_Annotation_annotationType) ) {
            return annotationType();
        }
        if ( method.equals(java_lang_Object_equals) ) {
            return equals(args[0]);
        }
        if ( method.equals(java_lang_Object_hashCode) ) {
            return hashCode();
        }
        if ( method.equals(java_lang_Object_toString) ) {
            return toString();
        }
        @Nullable
        final Object returnValue = invoke(method);
        if ( returnValue == null ) {
            throw new NoSuchMethodException("The instance of " + annotationClass + " has no value associated with " + method.getName());
        }
        return returnValue;
    }

    private static final class FromMap
            extends DynamicAnnotation {

        private final Map<String, Object> properties;

        private FromMap(final String toString, final Class<? extends Annotation> annotationClass, final Map<String, Object> properties) {
            super(/*hashCode, */toString, annotationClass);
            this.properties = properties;
        }

        private static DynamicAnnotation create(final Class<? extends Annotation> annotationClass, final Map<String, Object> properties)
                throws NoSuchMethodException {
            final Map<String, Object> toStringProperties = new LinkedHashMap<>();
            for ( final Method method : Annotations.lookupMethods(annotationClass) ) {
                final String name = method.getName();
                if ( !properties.containsKey(name) ) {
                    throw new NoSuchMethodException("Cannot find " + name + " in " + properties + " while constructing an instance of " + annotationClass);
                }
                final Object value = properties.get(name);
                toStringProperties.put(name, value);
            }
            final String toString = Annotations.toString(annotationClass, Collections.unmodifiableMap(toStringProperties));
            return new FromMap(toString, annotationClass, properties);
        }

        @Override
        protected Object invoke(final Method method) {
            return properties.get(method.getName());
        }

    }

}

Вы также можете использовать AnnotationUtils (если это работает для вас) или AnnotationParser из «пакета Sun» (если это также опция) для выполнения контрактов интерфейса аннотации.

Вот пример использования для раунда поездка:

private static final Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(AnnotationTypeAdapterFactory.getInstance())
        .create();

public static void main(final String... args)
        throws NoSuchFieldException {
    final SendEmail before = Wrapper.class.getDeclaredField("constant").getAnnotation(SendEmail.class);
    System.out.println(before);
    final String json = gson.toJson(before);
    System.out.println(json);
    final SendEmail after = gson.fromJson(json, SendEmail.class);
    System.out.println(after);
    System.out.println(after.annotationType());
    System.out.println(gson.toJson(after));
}

private static final class Wrapper {

    @SendEmail(id = "email@mail.com")
    private static final Object constant = new Object();

}
...