Crear anotaciones en Java

Entre las versiones 1.5 y 1.6 del JDK se introdujeron las llamadas anotaciones de las que hoy muchos frameworks dependen. Básicamente es un agregado sintáctico que nos permite definir un comportamiento particular sobre clases, métodos, atributos y argumentos.

La forma de invocarlas es siempre con un arroba previo al nombre de la anotación. Por ejemplo, en Spring Boot, para indicar que una clase se comportará como un controlador REST, le damos la anotación @RestController:

@RestController
public class MiController {
    // metodos
}

Así de simple. Con solo agregar una anotación transformamos el comportamiento de nuestro programa.

En este artículo vamos a crear una anotación personalizada, y de paso entenderemos como funcionan por debajo. Para un mejor entendimiento nos dividiremos en 3 pasos: Definición de una anotación, su implementación en métodos y finalmente como le asignamos un comportamiento particular.

Como se define una anotación

Las anotaciones se definen mediante interfaces. Por ejemplo:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MessageFilter {
    String value() default "";
}
  • La primera anotación indica que se procesará en tiempo de ejecución.
  • La segunda anotación dice que nuestra anotación será válida para aplicarse sobre métodos.
  • La variable default será el parámetro de la anotación, sin problemas podemos quitar este atributo y la anotación se podrá utilizar sin parámetros.
  • Por último notar que se utiliza la palabra clave @interface.

Si vamos sobre un método cualquiera, ya podemos asignarle esta nueva anotación aunque no tenga efecto alguno:

@MessageFilter("parametro")
public int suma(int x, int y) {
    return x + y;
}

Implementar anotación

Para nuestro ejemplo vamos a programar un bot que va a responder solo ante ciertos mensajes, por ejemplo, si recibe “hola” responderá con un “chao!”. Veamos:

public class Bot {

    @MessageFilter("hola")
    public String responderHola() {
        return "y chao!";
    }

    @MessageFilter("dame la hora")
    public String responderConLaHora() {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        return sdf.format(new Date());
    }

}

Tenemos 2 métodos con nuestra anotación @MessageFilter, y como vemos, el parámetro de la anotación será el mensaje al que nosotros responderemos:

  • Si me dicen “hola” respondo “y chao!”
  • Si me dicen “dame la hora”, respondo la hora actual

Entonces ya creamos la anotación y los métodos que lo implementan, pero falta una pieza y esta es el código que le da el comportamiento.

Programar comportamiento

Para poder darle vida a nuestro bot, lo expondremos mediante un servidor socket donde leeremos mensajes en texto plano que nos manden vía telnet por ejemplo.

public class App {

    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(1111);

        while (true) {
            Socket socket = server.accept();
            InputStreamReader isr = new InputStreamReader(socket.getInputStream());
            BufferedReader br = new BufferedReader(isr);
            String receivedMessage = br.readLine();

            // aca entrará el código "reflection"

            socket.close();
        }
    }

}

El programa es simple:

  • Levantamos un servidor socket en el puerto 1111
  • Aceptamos cualquier conexión entrante
  • Leemos el mensaje de entrada
  • Y cerramos la conexión

Ahora viene la parte más entretenida.

El comportamiento de nuestra anotación se lo asignaremos a través de “Reflection”, que es básicamente una API de Java para conocer la estructura de nuestro programa en tiempo de ejecución.

Esto nos permitirá saber que métodos implementan nuestra nueva anotación, para así invocarlos cada vez que recibamos “cierto mensaje” desde un socket cliente.

// escaneamos el classpath de nuestro programa
Reflections reflections = new Reflections("io.leiva", new SubTypesScanner(false));
Set<Class<? extends Object>> allClasses = reflections.getSubTypesOf(Object.class);

// recorremos todas las clases
for (Class<?> qlass : allClasses) {

    // recorremos todos los métodos de cada clase
    for (Method method : qlass.getDeclaredMethods()) {

        // si hace uso de nuestra anotación "@MessageFilter()"
        if (method.isAnnotationPresent(MessageFilter.class)) {

            String annotationMessage = method.getAnnotation(MessageFilter.class).value();

            // y si ademas, el mensaje recibido del socket, es igual al de la anotación
            if (annotationMessage.equals(receivedMessage)) {

                // entonces ejecutamos el método por reflection
                String returnedMessage = (String) method.invoke(qlass.newInstance());
                returnedMessage += "\n";

                // y el retorno, se lo devolvemos al socket cliente!
                socket.getOutputStream().write(returnedMessage.getBytes());

            }

        }

    }

}

Para este caso utilizamos la librería reflections para acceder a los metadatos de la aplicación java

Se puede ver un poco complicado pero la verdad son simples pasos. Lo que hacemos es que mediante reflection recorremos todas las clases de nuestro programa en búsqueda de métodos que implementen nuestra nueva anotación.

Si el filtro de nuestra anotación (@MessageFilter(“filtro”)) coincide con el mensaje que nos envió el socket cliente, entonces invocamos “manualmente” mediante reflection el método al que estaba asignada está anotación.

El retorno de ese llamado, se lo devolvemos al cliente como respuesta.

Hagamos la pruebita 🙂

Conclusión

Las anotaciones son una herramienta útil del lenguaje: Son fáciles de usar y nos ayudan a escribir código muchísimo más limpio y ordenado (casi siempre).

Muchos frameworks y librerías la implementan y lo común es que la ocupemos en nuestro día a día sin problemas, pero no está de más tampoco saber como implementarlas por las nuestras, de este modo tendremos una comprensión más amplia de como funcionan nuestras propias aplicaciones.

Digamos que te puede servir hasta para crear tus propios frameworks o librerías, o simplemente como una forma de manejar la complejidad sobre algunos de tus desarrollos.

Te dejo el link a la librería reflections que utilizamos en este artículo: https://github.com/ronmamo/reflections.

Y por último te dejo el link al repositorio en github donde tengo el programa que implementamos: https://github.com/felipeleivav/java-custom-annotations-example.

Nota: El código del programa es solo ejemplificativo. El código del socket no debe tomarse como la forma correcta de manejar conexiones.

1 Comment

Add yours →

  1. Excelnte ejemplo…!!!!!

Agregar un comentario

Su dirección de correo no se hará público.