Creación de una anotación personalizada en Java

1. Introducción

Las anotaciones de Java son un mecanismo para agregar información de metadatos a nuestro código fuente. Son una parte poderosa de Java y se agregaron en JDK5. Las anotaciones ofrecen una alternativa al uso de descriptores XML e interfaces de marcadores.

Aunque podemos adjuntarlos a paquetes, clases, interfaces, métodos y campos, las anotaciones por sí mismas no tienen ningún efecto en la ejecución de un programa.

En este tutorial, nos centraremos en cómo crear anotaciones personalizadas y cómo procesarlas. Podemos leer más sobre las anotaciones en nuestro artículo sobre conceptos básicos de las anotaciones.

2. Creación de anotaciones personalizadas

Vamos a crear tres anotaciones personalizadas con el objetivo de serializar un objeto en una cadena JSON.

Usaremos el primero en el nivel de clase, para indicarle al compilador que nuestro objeto se puede serializar. A continuación, aplicaremos el segundo a los campos que queremos incluir en la cadena JSON.

Finalmente, usaremos la tercera anotación en el nivel del método, para especificar el método que usaremos para inicializar nuestro objeto.

2.1. Ejemplo de anotación de nivel de clase

El primer paso para crear una anotación personalizada es declararla usando la palabra clave @interface :

public @interface JsonSerializable { }

El siguiente paso es agregar meta-anotaciones para especificar el alcance y el objetivo de nuestra anotación personalizada:

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

Como podemos ver, nuestra primera anotación tiene visibilidad en tiempo de ejecución y podemos aplicarla a tipos (clases) . Además, no tiene métodos y, por lo tanto, sirve como un marcador simple para marcar clases que se pueden serializar en JSON.

2.2. Ejemplo de anotación de nivel de campo

De la misma forma creamos nuestra segunda anotación, para marcar los campos que vamos a incluir en el JSON generado:

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

La anotación declara un parámetro de cadena con el nombre "clave" y una cadena vacía como valor predeterminado.

Al crear anotaciones personalizadas con métodos, debemos tener en cuenta que estos métodos no deben tener parámetros y no pueden generar una excepción . Además, los tipos de retorno están restringidos a primitivas, String, Class, enumeraciones, anotaciones y matrices de estos tipos, y el valor predeterminado no puede ser nulo .

2.3. Ejemplo de anotación de nivel de método

Imaginemos que, antes de serializar un objeto en una cadena JSON, queremos ejecutar algún método para inicializar un objeto. Por esa razón, vamos a crear una anotación para marcar este método:

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

Declaramos una anotación pública con visibilidad en tiempo de ejecución que podemos aplicar a los métodos de nuestras clases.

2.4. Aplicar anotaciones

Ahora, veamos cómo podemos usar nuestras anotaciones personalizadas. Por ejemplo, imaginemos que tenemos un objeto de tipo Person que queremos serializar en una cadena JSON. Este tipo tiene un método que escribe en mayúscula la primera letra del nombre y apellido. Queremos llamar a este método antes de serializar el 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 }

Al usar nuestras anotaciones personalizadas, estamos indicando que podemos serializar un objeto Person en una cadena JSON. Además, la salida debe contener solo los campos firstName , lastName y age de ese objeto. Además, queremos que se llame al método initNames () antes de la serialización.

Al establecer el parámetro clave de la anotación @JsonElement en “personAge”, estamos indicando que usaremos este nombre como identificador del campo en la salida JSON.

En aras de la demostración, hicimos initNames () privado, por lo que no podemos inicializar nuestro objeto llamándolo manualmente, y nuestros constructores tampoco lo están usando.

3. Procesamiento de anotaciones

Hasta ahora, hemos visto cómo crear anotaciones personalizadas y cómo usarlas para decorar la clase Person . Ahora, veremos cómo aprovecharlos utilizando la API Reflection de Java.

The first step will be to check whether our object is null or not, as well as whether its type has the @JsonSerializable annotation or not:

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"); } }

Then, we look for any method with @Init annotation, and we execute it to initialize our object's fields:

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); } } }

The call of method.setAccessible(true) allows us to execute the private initNames() method.

After the initialization, we iterate over our object's fields, retrieve the key and value of JSON elements, and put them in a map. Then, we create the JSON string from the map:

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 + "}"; }

Nuevamente, usamos field . setAccessible ( tru e ) porque los campos del objeto Person son privados.

Nuestra clase de serializador JSON combina todos los pasos anteriores:

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()); } } }

Finalmente, ejecutamos una prueba unitaria para validar que nuestro objeto fue serializado según lo definido por nuestras anotaciones 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. Conclusión

En este artículo, vimos cómo crear diferentes tipos de anotaciones personalizadas. Luego discutimos cómo usarlos para decorar nuestros objetos. Finalmente, vimos cómo procesarlos usando la API Reflection de Java.

Como siempre, el código completo está disponible en GitHub.