Criando uma anotação personalizada em Java

Criando uma anotação personalizada em Java

1. Introdução

As anotações Java são um mecanismo para adicionar informações de metadados ao nosso código-fonte. Eles são uma parte poderosa do Java e foram adicionados no JDK5. As anotações oferecem uma alternativa ao uso de descritores XML e interfaces de marcador.

Embora possamos anexá-los a pacotes, classes, interfaces, métodos e campos, as anotações por si só não afetam a execução de um programa.

Neste tutorial, vamos nos concentrar em como criar anotações personalizadas e como processá-las. Podemos ler mais sobre anotações emour article on annotation basics.

Leitura adicional:

Classes abstratas em Java

Aprenda como e quando usar classes abstratas como parte de uma hierarquia de classes em Java.

Read more

Interfaces de marcadores em Java

Aprenda sobre interfaces de marcadores Java e como elas se comparam às interfaces e anotações típicas

Read more

2. Criando anotações personalizadas

Vamos criar três anotações personalizadas com o objetivo de serializar um objeto em uma string JSON.

Usaremos o primeiro no nível da classe, para indicar ao compilador que nosso objeto pode ser serializado. A seguir, vamos aplicar o segundo aos campos que queremos incluir na string JSON.

Finalmente, vamos usar a terceira anotação no nível do método, para especificar o método que usaremos para inicializar nosso objeto.

2.1. Exemplo de anotação em nível de classe

A primeira etapa para criar uma anotação personalizada éto declare it using the @interface keyword:

public @interface JsonSerializable {
}

A próxima etapa éadd meta-annotations to specify the scope and the target de nossa anotação personalizada:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.Type)
public @interface JsonSerializable {
}

Como podemos ver, nossa primeira anotaçãohas runtime visibility, and we can apply it to types (classes). Além disso, ele não possui métodos e, portanto, serve como um marcador simples para marcar classes que podem ser serializadas no JSON.

2.2. Exemplo de anotação no nível do campo

Da mesma maneira, criamos nossa segunda anotação, para marcar os campos que vamos incluir no JSON gerado:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonElement {
    public String key() default "";
}

A anotação declara um parâmetro String com o nome “key” e uma string vazia como o valor padrão.

Ao criar anotações personalizadas com métodos, devemos estar cientes de que essesmethods must have no parameters, and cannot throw an exception. Além disso,the return types are restricted to primitives, String, Class, enums, annotations, and arrays of these types,and the default value cannot be null.

2.3. Exemplo de anotação no nível do método

Vamos imaginar que, antes de serializar um objeto em uma string JSON, queremos executar algum método para inicializar um objeto. Por esse motivo, vamos criar uma anotação para marcar este método:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Init {
}

Declaramos uma anotação pública com visibilidade de tempo de execução que podemos aplicar aos métodos de nossas classes.

2.4. Aplicando anotações

Agora, vamos ver como podemos usar nossas anotações personalizadas. Por exemplo, vamos imaginar que temos um objeto do tipoPerson que queremos serializar em uma string JSON. Esse tipo possui um método que coloca em maiúscula a primeira letra do nome e do sobrenome. Queremos chamar este método antes de serializar o objeto:

@JsonSerializable
public class Person {

    @JsonElement
    private String firstName;

    @JsonElement
    private String lastName;

    @JsonElement(key = "personAge")
    private String age;

    private String address;

    @Init
    private void initNames() {
        this.firstName = this.firstName.substring(0, 1).toUpperCase()
          + this.firstName.substring(1);
        this.lastName = this.lastName.substring(0, 1).toUpperCase()
          + this.lastName.substring(1);
    }

    // Standard getters and setters
}

Usando nossas anotações personalizadas, estamos indicando que podemos serializar um objetoPerson para uma string JSON. Além disso, a saída deve conter apenas os camposfirstName,lastName eage desse objeto. Além disso, queremos que o métodoinitNames() seja chamado antes da serialização.

Ao definir o parâmetrokey da anotação@JsonElement para “personAge”, estamos indicando que usaremos esse nome como identificador para o campo na saída JSON.

Para fins de demonstração, tornamosinitNames() privado, portanto, não podemos inicializar nosso objeto chamando-o manualmente e nossos construtores também não o estão usando.

3. Anotações de processamento

Até agora, vimos como criar anotações personalizadas e como usá-las para decorar a classePerson. Agora,we’re going to see how to take advantage of them by using Java’s Reflection API.

O primeiro passo será verificar se nosso objeto énull ou não, bem como se seu tipo possui a anotação@JsonSerializable ou não:

private void checkIfSerializable(Object object) {
    if (Objects.isNull(object)) {
        throw new JsonSerializationException("The object to serialize is null");
    }

    Class clazz = object.getClass();
    if (!clazz.isAnnotationPresent(JsonSerializable.class)) {
        throw new JsonSerializationException("The class "
          + clazz.getSimpleName()
          + " is not annotated with JsonSerializable");
    }
}

Em seguida, procuramos qualquer método com a anotação @Init e o executamos para inicializar os campos do nosso objeto:

private void initializeObject(Object object) throws Exception {
    Class clazz = object.getClass();
    for (Method method : clazz.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Init.class)) {
            method.setAccessible(true);
            method.invoke(object);
        }
    }
 }

A chamada demethod.setAccessible (true) nos permite executar o métodoinitNames() privado.

Após a inicialização, iteramos sobre os campos do nosso objeto, recuperamos a chave e o valor dos elementos JSON e os colocamos em um mapa. Em seguida, criamos a string JSON a partir do mapa:

private String getJsonString(Object object) throws Exception {
    Class clazz = object.getClass();
    Map jsonElementsMap = new HashMap<>();
    for (Field field : clazz.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.isAnnotationPresent(JsonElement.class)) {
            jsonElementsMap.put(getKey(field), (String) field.get(object));
        }
    }

    String jsonString = jsonElementsMap.entrySet()
        .stream()
        .map(entry -> "\"" + entry.getKey() + "\":\""
          + entry.getValue() + "\"")
        .collect(Collectors.joining(","));
    return "{" + jsonString + "}";
}

Novamente, usamosfield.setAccessible (true) porque os campos do objetoPerson são privados.

Nossa classe de serializador JSON combina todas as etapas acima:

public class ObjectToJsonConverter {
    public String convertToJson(Object object) throws JsonSerializationException {
        try {
            checkIfSerializable(object);
            initializeObject(object);
            return getJsonString(object);
        } catch (Exception e) {
            throw new JsonSerializationException(e.getMessage());
        }
    }
}

Por fim, executamos um teste de unidade para validar que nosso objeto foi serializado conforme definido por nossas anotações personalizadas:

@Test
public void givenObjectSerializedThenTrueReturned() throws JsonSerializationException {
    Person person = new Person("soufiane", "cheouati", "34");
    JsonSerializer serializer = new JsonSerializer();
    String jsonString = serializer.serialize(person);
    assertEquals(
      "{\"personAge\":\"34\",\"firstName\":\"Soufiane\",\"lastName\":\"Cheouati\"}",
      jsonString);
}

4. Conclusão

Neste artigo, vimos como criar diferentes tipos de anotações personalizadas. Depois discutimos como usá-los para decorar nossos objetos. Finalmente, vimos como processá-los usando a API Reflection do Java.

Como sempre, o código completo está disponível emGitHub.