Optional en Java

  ___       _   _               _    _________
/ _ \ _ __| |_(_)___ _ _ __ _| | / /_ _\ \
| (_) | '_ \ _| / _ \ ' \/ _` | | < < | | > >
\___/| .__/\__|_\___/_||_\__,_|_| \_\ |_| /_/
|_|

Desde que programo en Java, una de las excepciones más recurrentes y, por qué no decirlo, molestas, ha sido la infame NullPointerException. Durante años, la forma de evitarla era anidar bloques if por todo el código, lo que no solo lo hacía más verboso, sino también más difícil de leer y mantener. Siempre quedaba la otra opción (la vieja confiable): meter un try/catch a todo el bloque sospechoso, pero esto, claramente, no mejora la legibilidad y abre otros problemas, como saber qué hacer cuando ocurre la NullPointerException.

A partir de Java 8, se introdujo una nueva herramienta en el lenguaje para lidiar con este problema de una manera más elegante y funcional: la clase Optional<T>.

¿Desde cuándo está disponible?

Optional fue una de las adiciones más destacadas del JDK 8, lanzado en marzo de 2014. Nació como parte del esfuerzo por modernizar el lenguaje e introducir paradigmas de programación funcional, junto con las expresiones Lambda y la API de Streams.

¿Cuál es su objetivo principal?

El propósito fundamental de Optional es proporcionar un contenedor que puede o no tener un valor no nulo. En lugar de devolver null para indicar la ausencia de un resultado, un método puede devolver un Optional que esté “vacío”.

Esto tiene una ventaja semántica enorme: la firma de un método que devuelve Optional<String> comunica explícitamente al programador que el valor podría no estar presente. Obliga a quien llama al método a “desenvolver” el Optional y manejar conscientemente el caso de ausencia, eliminando la ambigüedad de un null que podría ser un valor esperado o un error no controlado.

Beneficios de usar Optional

Adoptar Optional en tu código trae varias ventajas:

  1. Claridad en el diseño de APIs: Como mencioné, si un método devuelve Optional<T>, sabes de inmediato que debes prepararte para un resultado vacío. Se acabaron las sorpresas con valores nulos inesperados.
  2. Reducción de NullPointerException: Al forzar un manejo explícito de la ausencia de valor, el código se vuelve más robusto y menos propenso a errores en tiempo de ejecución.
  3. Código más expresivo y funcional: Optional viene con una serie de métodos de estilo funcional (map, flatMap, filter, ifPresent) que permiten encadenar operaciones de una manera fluida y legible, evitando la necesidad de bloques if-else anidados.

Ejemplos prácticos

Veamos cómo trabajar con Optional.

Creación de un Optional

1
2
3
4
5
6
7
8
9
10
11
12
// Ejemplos de creación de un Optional
// 1. Optional con un valor no nulo
// (si 'valor' es null, lanza NullPointerException)
Optional<String> optionalConValor = Optional.of("Hola Mundo");

// 2. Optional que permite valores nulos
// (si 'valorNulo' es null, crea un Optional vacío)
String valorNulo = null;
Optional<String> optionalVacio = Optional.ofNullable(valorNulo);

// 3. Creación explícita de un Optional vacío
Optional<String> vacio = Optional.empty();

Comprobando si hay un valor

1
2
3
4
5
6
7
8
9
10
// Ejemplos de comprobación de valor
if (optionalConValor.isPresent()) {
System.out.println("El optional tiene un valor: "
+ optionalConValor.get());
}

// A partir de Java 11, puedes usar isEmpty()
if (vacio.isEmpty()) {
System.out.println("El optional está vacío.");
}

Ejecutando acciones si hay un valor

Una forma muy común y limpia de usar Optional es con ifPresent.

1
2
3
4
// Ejemplo de uso de ifPresent con una expresión lambda
optionalConValor.ifPresent(valor -> {
System.out.println("El valor es: " + valor);
});

Obteniendo el valor de forma segura

El método .get() es peligroso porque lanzará una NoSuchElementException si el Optional está vacío. Es mucho mejor usar alternativas que proveen un valor por defecto.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Formas seguras de obtener el valor
// Si el optional está vacío, devuelve "default"
String resultado1 = optionalVacio.orElse("default");

// Similar a orElse, pero el valor por defecto se genera a
// través de un Supplier. Esto es más eficiente si la generación
// del valor por defecto es costosa.
String resultado2 = optionalVacio.orElseGet(() -> "valor generado");

// Si el optional está vacío, lanza una excepción.
String resultado3 = optionalVacio.orElseThrow(() ->
new IllegalStateException("No se encontró el valor")
);

Transformando valores con map y flatMap

Aquí es donde Optional realmente brilla, permitiendo un estilo de programación funcional.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Transformando el valor con map y flatMap
Optional<String> optionalString = Optional.of(" texto con espacios ");

// Usamos map para transformar el valor interno de forma segura
Optional<String> sinEspacios = optionalString.map(String::trim);

// Imprime "'texto con espacios'"
sinEspacios.ifPresent(s -> System.out.println("'" + s + "'"));

// flatMap se usa cuando la función map devuelve otro Optional,
// para evitar tener un Optional<Optional<T>>.
Optional<Optional<String>> anidado = sinEspacios.map(s ->
Optional.of(s.toUpperCase()) // ¡No hagas esto!
);

Optional<String> enMayusculas = sinEspacios.flatMap(s ->
Optional.of(s.toUpperCase()) // ¡Esta es la forma correcta!
);

// Imprime "TEXTO CON ESPACIOS"
enMayusculas.ifPresent(System.out::println);

Sin lugar a dudas, lo que más uso de Optional es en conjunto con Spring Data, específicamente en los repositorios. Funcionalmente, el patrón es algo como esto: obtengo la entidad que busco y, si no existe, lanzo una excepción ExampleNotFoundException (que hereda de RuntimeException) que luego es capturada por un GlobalExceptionHandler para devolver un código de estado HTTP 404. Te invito a revisar mi artículo sobre Manejo de excepciones en Spring Boot.

1
2
3
4
5
// Ejemplo con un repositorio de Spring Data
public ExampleModel findById(String id) {
return this.exampleRepository.findById(id)
.orElseThrow(() -> new ExampleNotFoundException(id));
}

Palabras al cierre

Optional no es una solución mágica para todos los problemas de null, pero es una herramienta poderosa para diseñar APIs más claras y escribir código más seguro y expresivo. Al principio puede costar acostumbrarse a su enfoque funcional, pero una vez que lo dominas, te preguntarás cómo pudiste vivir sin él.


Para seguir leyendo

Si quieres profundizar más en el uso y las buenas prácticas de Optional, te recomiendo los siguientes recursos:

Manejo de excepciones en Spring Boot

Motivación

Se requiere un mecanismo global o centralizado basado en las excepciones, para poder manejar y traducir en errores entendibles hacia las integraciones (frontend y/o otros clientes).

Introducción

La gestión de excepciones en el sentido amplio de la palabra, se refiere a la captura de eventos de tipo errores que se producen en las aplicaciones durante su ejecución. Estos errores pueden ser de muchos tipos, desde fallas de red, errores en accesos a una base de datos, errores en tiempo de ejecución (runtime) o simplemente por validaciones de negocio.

El manejo de excepciones tiene como objetivo hacer más seguras y robustas las aplicaciones así como también tener un mecanismo para dar una respuesta elegante a los usuarios mejorando la confiabilidad y resistencia a fallas en las aplicaciones.

Usos

  • Manejo global de excepciones:

    • ¿ Cuándo usarlo ?
      • Cuando se necesita un formato de respuesta de error consistente para toda la aplicación.
      • Para evitar exponer trazas de la aplicación con información relevante, de esta forma evitar posibles futuros atacantes.
  • Manejo de excepciones en el controlador:

    • Utilizar cuando se necesita una solución particular (distinta a la ofrecida por el manejo de errores globales), como por ejemplo, integraciones donde necesitamos adaptar nuestra respuesta.

Implementación

Para su implementación, Spring Boot nos provee algunas herramientas, las más comunes para este tipo de manejadores son @ControllerAdvice y de @RestControllerAdvice, el primero para cualquier tipo de controlador anotado con @Controller (MCV de Spring) y el segundo esta especializado en controladores de tipo Rest (aplica sólo a los controladores anotados como @RestController)

Para que estos componentes puedan funcionar correctamente y como su nombre lo indica, deben poder atrapar excepciones del tipo específico (jerarquía) que necesitemos manejar y hacer la interpretación de la excepciín, como por ejemplo: se requiere atrapar cualquier Exception del tipo IllegalArgumentException y convertirla en un bad request (error 400 de http).

Hablemos un poco de excepciones

Los manejadores de excepciones siempre actúan cuando una excepción es lanzada y no ha sido atrapada por la capa de controladores, éstas pueden ser lanzadas desde las capas de acceso a datos, directamente desde la capa de negocio o simplemente desde la capa de controladores (validación de entrada).

Aquí la discusión se vuelve un poco densa y depende mucho el cómo quieres trabajar tu jerarquía de excepciones, puede ser tan compleja como quieras (casos en que necesitas mucho detalle en la respuesta del manejador) o tan simple como mantener un par de excepciones que manejar e interpretar.

En un articulo anterior escribí acerca de tipos de excepciones, jerarquía y características el manejo de excepciones. Les dejo aquí el enlace a este artículo Checked y Unchecked Exception.

Una de las alternativas mas simples es utilizar Excepciones de tipo RunTimeException ya que tienen la característica de no ser atrapadas de forma imperativa o declarativa, de esta forma excepciones que son lanzadas en las capas inferiores saltan hasta el manejador de Excepciones, claro está, solo si son manejadas en dicho manejador.

Veamos un ejemplo

En SpringBoot tenemos un par de mecanismos para poder manejar excepciones y que nos permite evitar exponer el stack trace directo al usuario (nos da seguridad en caso de exponer algún dato sensible) y en su reemplazo nos permite modelar una salida elegante dependiendo de la excepción lanzada.

Para efectos prácticos en este artículo utilizaremos sólo excepciones de tipo Runtime ya que este tipo de excepciones suben por el stack sin ser capturadas por nuestro código hasta llegar al lugar donde queremos hacer el manejo de la excepción.

Rest Controller Advice

GlobalExceptionHandleres una clase que se anota con @RestControllerAdvice y es donde podremos hacer el manejode la excepción. Veamos un ejemplo a continuación:

En este primer ejemplo, se evidencia que se atrapan las exepciones especificas y mas altas de la jerarquía, NotFoundException, BadRequestException y UnauthorizedException, ademas para cada una de ellas hay un método disponible donde podremos por ejemplo hacer un log del error para tener el detalle de lo ocurrido y finalmente enviar hacia el consumnidor, el correspondiente codigo Http para cada caso. Nota: en este ejemplo el body de la respuesta es vacío.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cl.pcollaog.eh.handler;

import cl.pcollaog.eh.exception.BadRequestException;
import cl.pcollaog.eh.exception.NotFoundException;
import cl.pcollaog.eh.exception.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<Void> badRequestHandler(BadRequestException bre) {
LOGGER.error("badRequestHandler - message {}", bre.getMessage());
return ResponseEntity.badRequest().build();
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Void> notFoundExceptionHandler(NotFoundException nfe) {
LOGGER.error("notFoundExceptionHandler - message: {}", nfe.getMessage());
return ResponseEntity.notFound().build();
}

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<Void> unauthorizedExceptionHandler(UnauthorizedException ue) {
LOGGER.error("unauthorizedExceptionHandler - message: {}", ue.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}

En este segundo ejemplo, vamos a manipular la respuesta (vamos a suponer que la integración lo pide así) para cada caso.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package cl.pcollaog.eh.handler;

import cl.pcollaog.eh.exception.BadRequestException;
import cl.pcollaog.eh.exception.NotFoundException;
import cl.pcollaog.eh.exception.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<CustomResponse> badRequestHandler(BadRequestException bre) {
LOGGER.error("badRequestHandler - message {}", bre.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.BAD_REQUEST), bre.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<CustomResponse> notFoundExceptionHandler(NotFoundException nfe) {
LOGGER.error("notFoundExceptionHandler - message: {}", nfe.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.NOT_FOUND), nfe.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<CustomResponse> unauthorizedExceptionHandler(UnauthorizedException ue) {
LOGGER.error("unauthorizedExceptionHandler - message: {}", ue.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.UNAUTHORIZED), ue.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

public static class CustomResponse {

private final LocalDateTime date;

private final HttpStatus status;

private final String message;

public CustomResponse(HttpStatus status, String message) {
this.date = LocalDateTime.now();
this.status = status;
this.message = message;
}
// ... Omitiré los getter
}
}

Para el caso de NotFoundException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 404 
Connection: keep-alive
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:20:02 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:20:02.462356",
"message": "Entity not found",
"status": "NOT_FOUND"
}

Para el caso de BadRequestException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 400 
Connection: close
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:16:41 GMT
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:16:41.886069",
"message": "Invalid argument",
"status": "BAD_REQUEST"
}

Finalmente para el caso de UnauthorizedException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 401 
Connection: keep-alive
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:21:38 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:21:38.011316",
"message": "Invalid Token",
"status": "UNAUTHORIZED"
}

Como podrán ver, en el handler (método) podrán manejar la excepción a conveniencia y podrán enviar el mensaje y codigo de error que necesiten hacia el consumidor del servicio.

Para finalizar les dejo un diagrama del funcionamiento del GlobalExceptionHandler con el ejemplo de 404 Not Found.

Si tienen alguna observación no duden en comentar.

jEnv una solución para múltiples instalaciones de Java

jEnv es una herramienta de linea de comandos que ayuda a mantener múltiples instalaciones de Java (versiones y/o sabores) y permite cambiar las versiones por linea de comandos para poder mantener diversos entornos de desarrollo.

Una de las funcionalidades interesantes de jEnv es que configura las variables de entorno de java JAVA_HOME al vuelo según una pequeña configuración que se agrega al proyecto. Más adelante veremos algunas gracias de jEnv.

Instalación

La instalación es bien sencilla y la pueden revisar en la página oficial de jEnv, acá dejo un resumen:

1
$ git clone https://github.com/gcuisinier/jenv.git ~/.jenv

Ahora debes ejecutar (dependiendo de tu shell) lo siguiente:

1
2
$ echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(jenv init -)"' >> ~/.bash_profile
1
2
$ echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(jenv init -)"' >> ~/.zshrc

Reiniciamos el terminal y/o cargamos las nueva variables de entorno de nuestra shell y primero probamos que todo funcione correctamente con el siguiente comando:

1
$ jenv --version

Si el comando te responde con la versión de jEnv es porque esta todo correctamente instalado.

Configuración

Luego tenemos que agregar las diferentes versiones de java que tengas instaladas en tu sistema de la siguiente forma:

1
2
3
4
5
$ jenv add /path/to/java6-oracle/java_home
$ jenv add /path/to/java6-openjdk/java_home
$ jenv add /path/to/java7-oracle/java_home
...
...

Una ves que hayas concluido la configuración de las diferentes versiones de Java de tu sistema, puedes listarlos con el siguiente comando:

1
$ jenv versions

Y aparecerá algo como esto:

1
2
3
4
5
6
7
  system
* 1.7 (set by /home/pcollaog/.jenv/version)
1.7.0.79
1.8
1.8.0.45
oracle64-1.7.0.79
oracle64-1.8.0.45

Ahora corresponde configurar cual de todas esas instalaciones será la que funcionará de forma global, es decir, la configuración por omisión de Java. En este ejemplo se configura la versión oracle64-1.7.0.79 como global.

1
$ jenv global oracle64-1.7.0.79

Tambien se puede configurar de forma mas genérico, es decir, la última versión de java 7, por ejemplo:

1
$ jenv global 1.7

Con esto siempre tomará la versión mas nueva de java 7 que tengan previamente configurada.

Activar la configuración automática de JAVA_HOME

Para delegar la configuración de las variables de entorno de java a jEnv, debemos activar el plugin export de la siguiente forma:

1
$ jenv enable-plugin export

Nota: para que funcione correctamente el plugin, se debe eliminar la configuración de las variables de entorno que se tengan en los perfiles de bash (.bashrc) , zsh (.zshrc) o de su shell favorita.

Luego deben reiniciar su terminal o volver a cargar los perfiles de su shell y probar:

1
$ echo $JAVA_HOME

Debería aparecer algo como lo que sigue:

1
/home/pcollaog/.jenv/versions/1.7

Y si vemos la versión de java debería aparecer algo como esto:

1
2
3
4
$ java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

Configuración personalizada de Java

Si por alguna razón necesitas tener una versión especifica de Java en un proyecto de código fuente, debes configurar (estando dentro del directorio principal del proyecto) de la siguiente forma:

1
$ jenv local 1.7

Este comando creará un archivo en el directorio en el que te encuentras (directorio base de tu código fuente) llamado .java-version y cuyo contenido será la versión de java especificada. Con esto todos los subdirectorios (a partir de donde se encuentras este archivo) estarán configurados con la versión de java seleccionada. Si tienes correctamente configurado el plugin export, jEnv hará el trabajo sucio de configurar la variable JAVA_HOME.

Sus comentarios son bienvenidos!

Spring Constructor Namespace

En esta oportunidad les escribo sobre un nuevo namespace que apareció en Spring 3.1 (y me dí cuenta recién XD ) y sirve para configurar los beans haciendo uso de su constructor. Como su nombre lo indica este es un namespace que opera sólo sobre los constructores de beans y así permitir la inyección de beans o valores.

Les dejo una imagen de como se activa este nuevo namespace en el STS.

Veamos un ejemplo simple de cómo usar namespace C.

Supongamos la siguiente clase:

1
2
3
4
5
6
7
8
public class SimpleSPImpl extends StoredProcedure
implements SimpleSP {

public SimpleSPImpl(DataSource ds, String spName) {
super(ds, spName);
}

}
1
2
3
4
5
6
7
8
9
10
<bean id="bean_id" class="com.example.SimpleSPImpl">
<constructor-arg name="ds" ref="datasource" />
<constructor-arg name="spName" value="spNameTest" />
</bean>

<!-- Definición del datasource de ejemplo -->
<bean id="datasource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
...
...
</bean>

Ahora usando el namespace c quedaría algo mas simple:

1
2
3
4
5
6
7
8
<bean id="bean_id" class="com.example.SimpleSPImpl 
c:ds-ref="datasource" c:spName="spNameTest" />

<!-- Definición del datasource de ejemplo -->
<bean id="datasource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
...
...
</bean>

Como pueden notar en el ejemplo, el namespace C permite configurar los constructores de los beans y tiene dos formas básicas, por valor y referencia. Para inyectar una instancia preconfigurada se debe usar el sufijo -ref para hacer alusión a que es una referencia. Si se desea inyectar un valor, sólo se usa el nombre del argumento del constructor.

Como siempre sus comentarios son bienvenidos.

Checked y Unchecked Exception

Dado que varios de mis lectores me han solicitado un post sobre tipos de excepciones en Java, les dejaré un par de notas para que consideren al momento de diseñar soluciones y por su puesto el cómo manejar los errores. Aquí vamos!

Primero que todo, un par de definiciones básicas y características antes de partir.

Jerarquía de Excepciones

Esta es la jerarquía de excepciones de mas alto nivel que encontramos en Java.

Unchecked Exception

Generalmente este tipo de excepciones son lanzadas por la aplicación y se generan a partir de errores en tiempo de Runtime. Este tipo de excepciones representan errores en el código y que la aplicación no es capaz de controlar. Algunos de errores causados y que lanzan este tipo de excepciones, por ejemplo, argumentos inválidos pasados a un método (argumentos null pueden causar NullPointerException), otro error común son la excepciones del tipo IndexOutOfBoundsException y que son lanzadas cuando se quieren obtener elementos de una lista y el índice que se entrega está fuera del tamaño del arreglo. Como podrán ver, son errores de programación y que generarán defectos en momento de correr la aplicación (no así al compilar).

Unchecked runtime exceptions represent conditions that, generally speaking, reflect errors in your program’s logic and cannot be reasonably recovered from at runtime.

Gosling, Arnold and Holmes, The Java Programming Language

Las excepciones de tipo Unchecked son subclases que heredan desde RuntimeException. Además este tipo de excepciones no tienen la obligación de ser declaradas con la cláusula throws en la cabecera del método. Otra característica es que tampoco se tiene la obligación de atraparlas con un catch como se muestra en el ejemplo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Ejemplo de exception tipo {@link RuntimeException}
*
*/
public class RuntimeDemo {

/**
* Método principal
*/
public void mainMethod() {
methodThowsRuntimeException();
methodThowsRuntimeException2();
}

/**
* Este método lanzará una excepción de tipo runtime no declarada en su
* firma
*/
public void methodThowsRuntimeException() {
throw new ExampleRuntimeException();
}

/**
* Este método lanzará una excepción de tipo runtime está declarada en su
* firma (no es obligación) pero deja mas claro al desarrollador las
* excepciones que debería manejar con la API.
*
* @throws ExampleRuntimeException
* en caso de error
*/
public void methodThowsRuntimeException2() throws ExampleRuntimeException {
throw new ExampleRuntimeException();
}

/**
* Clase de error tipo Runtime
*/
public static class ExampleRuntimeException extends RuntimeException {

public ExampleRuntimeException() {
super();
}
}
}

Checked Exception

Este tipo de excepciones representan condiciones inválidas en el contexto de la línea de ejecución y que están fuera del control de dicho contexto, como por ejemplo, problemas con la base de datos, problemas de red, acceso a los archivos. También pueden ser condiciones de ingreso al sistema en donde el sistema no tiene ninguna participación, como por ejemplo, ingresar un nombre de usuario y contraseña incorrectos.

Contexto de ejecución ó scope: Corresponde al las líneas de código que están encerradas en un bloque de código, como por ejemplo, un método, un try/catch, bloque estático, etc.

Este tipo de excepciones deben ser declaradas en la firma del método. Además deben ser atrapadas dentro de los bloques de código donde se invoque un método que contenga la clausula throws.

Todas las excepciones de este tipo son subclases que heredan desde Exception, como por ejemplo:

Otra característica de este tipo de excepciones es que existe una probabilidad de recuperación de la ejecución y el método puede realizar alguna acción correctiva y/o informativa (log) en el bloque catch o simplemente relanzar la excepción y confiar en que el método invocante la atrape y haga algo con ella.

Para enviar el stacktrace al sistema de log (systemout) pueden usar los métodos de Throwable y en específico al método printStackTrace.

Un pequeño ejemplo de checked exception:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* Clase de ejemplo para checked exceptions
*/
public class CheckedExample {

/**
* Método que atrapa una checked exception
*/
public void catchCheckedException() {
try {
throwCheckedException();
} catch (ExampleCheckedException e) {
// TODO: hacer algo en caso de error (recuperacion)
}
}

/**
* Método que relanza la checked exception a un método superior
*
* @throws ExampleCheckedException
* checked exception error
*/
public void rethrowCheckedException() throws ExampleCheckedException {
throwCheckedException();
}

/**
* Método que lanza una checked exception
*
* @throws ExampleCheckedException
* checked exception error
*/
private void throwCheckedException() throws ExampleCheckedException {
throw new ExampleCheckedException();
}

/**
* Clase que representa la excepcion de ejemplo
*/
private static final class ExampleCheckedException extends Exception {

public ExampleCheckedException() {
super();
}

}
}

Error

Las excepciones de tipo Error son excepciones en las que el sistema no puede hacer nada con ellas, son clasificadas como errores irreversibles y que en su mayoría provienen desde la JVM, como por ejemplo: IOError, NoClassDefFoundError, NoSuchMethodError, OutOfMemoryError y VirtualMachineError por mencionar algunos de los errores.

Un poco de diseño y consejos para el manejo de excepciones

Para iniciar esta última parte, comenzaré con algunos ejemplos de malas prácticas con las que siempre nos encontramos cuando programamos, les dejo unas pocas:

1
2
3
4
5
try {
//ejecución que lanza checked exceptions
} catch (ExampleCheckedException e) {
// No hace nada
}

En el ejemplo de arriba claramente no se hace nada con la excepción dentro del try, lo recomendable es que si de verdad no vas a hacer nada con la excepción, al menos debes enviarla a tu sistema de Logger favorito con algún nivel de debug aceptable para poder revisar el log. Siempre se recomienda hacer algo en el bloque catch ya que ocurrió un error que debe ser controlado.

1
2
3
4
5
try {
//ejecución que lanza checked exceptions
} catch (ExampleCheckedException e) {
throw e;
}

Es similar ejemplo 1 pero lo que se hace es relanzar la excepción atrapada hacia el método invocante. La recomendación es que nunca hagas eso ya que se presta para confusión al leer el código fuente y en la practica se estarían ejecutando dos bloques catch para la misma excepción (el directo y el del método invocante).

1
2
3
4
5
try {
//ejecución que lanza checked exceptions
} catch (Exception e) {
// alguna lógica de negocio
}

Es recomendable que nunca atrapen todas las excepciones en un bloque catch y básicamente porque uno pierde la noción de por qué se produjo la excepción. Además en ese bloque también se atrapan las excepciones de tipo Runtime y ya mencionamos que estas excepciones significan errores en tu programa y que deben ser depurados (no escondidos debajo de la alfombra). Lo mejor es atrapar cada una de las excepciones y darle un tratamiento a cada una, si necesitan agrupar usen la herencia/jerarquía de las excepciones.

1
2
3
4
5
6
7
8
9
10
11
try {
//ejecución que lanza checked exceptions
} catch (Exception e) {
// alguna lógica de negocio

if (e instanceof BlaException ){

} else if (e instanceof FooException) {

} else if .....
}

Ese bloque de if’s compuestos se debe transformar en varios catch para cada una de las excepciones lanzadas. No usen un control de errores manual, es mejor usar las herramientas que te provee el lenguaje.

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
//ejecución que lanza checked exceptions
} catch (ErrorBlaException e) {
...
} catch (ErrorFooException e) {
...
} catch (ErrorOMGException e) {
...
} catch (SDWException e) {
...
} catch (Exception e) {
...
}

No abusar de las checked exceptions ya que hacen nuestro código confuso y poco mantenible. Si bien es cierto, es la herramienta que nos provee el lenguaje, no abusemos de ella y convirtamos los bloques catch en pseudo programas y rutinas anexas a la lógica de negocio (que es la que vale).

1
2
3
private void foo() throws Exception {
// código de negocio
}

Nunca lancen Exception como una excepción de su lógica de negocio y es que básicamente los catch están pensados en atrapar excepciones particulares y al lanzar Exception (de la mas alta jerarquía) jamás entrarás al bloque catch que corresponda y que pueda gestionar el error. Por otro lado el programador pierde la visibilidad de los errores particulares que debe gestionar.

1
2
3
4
5
6
7
private void foo() throws Exception {
try {
// logica de negocio
} catch (ParserConfigurationException e) {
throw new RuntimeException("Error");
}
}

Jamás se debe hacer esto, jamás!. Esto romperá todo tu programa ya que al lanzar la excepción RuntimeException esta llegará sin control a la capa mas alta provocando un error. Recuerden que ese tipo de excepciones son errores sin recuperación y justamente estamos tratando de hacer lo contrario gestionar los errores de lógica de negocio.

Mantener un árbol de excepciones

Esta sección del post quizás sea el más polémico ya que no hay receta perfecta para el manejo de excepciones y daré mis consejos (personales), puede que estén de acuerdo como puede que no.

Les recomiendo siempre mantener un árbol de excepciones que representen los errores (de negocio y de ejecución) de tu aplicación. Para que sea mas simple la mantención del árbol de excepciones, usen polimorfismo, herencia y todas las herramientas que ofrece OOP. En este punto siempre hay detractores de los árboles de excepciones con la excusa de su mantención.

Contraria a mi propuesta de manejo de errores, existen quienes mantienen sólo 1 excepción y a dicha excepción le agregan atributos y cuanta metadata puedan agregar. Qué se consigue finalmente con ese esquema de errores, es llenarte de IF por todos lados mirando los atributos que contiene la instancia de excepción y haciendo todo un control de errores manual.

Palabras al cierre

Quedan muchas cosas por mencionar de las excepciones y ahondar mucho mas en cómo diseñar y construir un árbol de excepciones, creo que sera materia para otro artículo. Demás esta decirles que esta abierta la discusión. Los comentarios bienvenidos sean.

Ud. no lo haga - Parte 1

En este post, pondré algunas capturas de pantallas de cosas que Ud. como desarrollador (java) no debe hacer (en algunos casos debe evitar hacer).

Constantes

En Java las constantes por lo general se declaran en estilo uppercase usando como separador de palabras el underscore ALGO_COMO_ESTO, en este ejemplo, se pueden notar que agregan un underscore al principio (No lo haga!). Generalmente se usa underscore al principio para nombrar los atributos de una clase, de esta forma no usas this para identificar un atributo de clase.

El falso catch

Si va hacer algún tipo de control sobre una exception, pués hágalo. En este ejemplo sólo se captura la exception y se vuelve a lanzar. Si realmente quiere hacer eso, no haga el *catch] de la exception, déjela salir libremente.

La estética del código si importa

Si esta programando y al final de su algoritmo, le queda algo parecido a lo que sale en la imagen, recapacite, tome aire y refactorice su código. Hay algunas alertas que se pueden ver fácilmente con la estética del código, es decir, al cómo queda escrito (forma, silueta). Hay algunos desarrolladores que les encanta tener sus líneas de código hasta el infinito, lo que dificulta su lectura cuando tienes una pantalla distinta a la del desarrollador.

Otro ejemplo más:

Algunos consejos que te ayudarán a darte cuenta de errores en tu código de forma visual, aquí los dejo:

  • Ajusta tu IDE para que te corte las lineas en los 80/120 caracteres. Con esto podrás tener como buena práctica nombrar bien tus variables (cortas y precisas) y es un buen límite cuando empiezas a avanzar en la identación del código producto de los if/else/for/while/try/catch. Si terminas escribiendo código cerca de los 80 caracteres es que algún problema tienes en tu código y necesita refactorización.
  • Si trabajas en equipo, es indispensable que todos tengan los mismos settings para la identación y el encoding, de esta forma, no tendrás problemas al comparar código (diff).

Estos son mis ajustes en el STS/Eclipse:

Estilo Visual Basic

Si esta escribiendo código Java, por favor no cometa este error. Si va a declarar una variable, hágalo en el lugar donde se utilizará, de esta forma los refactoring de código son mas simples. Por otro lado, estéticamente queda feo tu código. Ahora si entramos en el micro manejo de memoria, posiblemente estas reservando memoria que no utilizarás en todo el método. En este ejemplo, se declaran muchas variables con un valor, pero que pasa si salta una exception? o algún control de flujo que no considere todas las variables?, habrás perdido innecesariamente un par de bytes.

WTF!!!

Nunca, pero nunca, asignes a una variable el valor de una constante, no tiene ningún sentido. Además en este ejemplo, podrán notar que hay código que no tiene ningún sentido, nameCombobox nunca jamás en la vida va a ser null por lo tanto ese if esta de sobra. Escriba código que realmente es útil y que funciona.

Hay algunas cosas que no tienen una explicación razonable, como instanciar un objeto para luego no utilizarlo. Esto sólo provoca perdida de preciados bytes y ciclos de procesador.

Creo que califica en la misma descripción de arriba, escriba código que funcione.

Si va a utilizar StringBuffer hágalo de la forma correcta, se merece un mínimo de respeto dicha clase.

Evite el código que está de más, la API de commons-lang StringUtils.isEmpty() evalua que sea null y vacío. Ahora bien en el código podrían utilizar de la misma API StringUtils.isNotEmpty() y con eso le sacan el signo ! y todo queda mas bonito, por su puesto que la primera parte del if vuela también del código.

Finalmente esto se traduce en:

1
2
3
if (StringUtils.isNotEmpty(codeAdditional)){
....
}

Espero les sirvan estos anti-ejemplos de código fuente. A medida que siga revisando código iré agregando algún otro post con mas código para el bronce. La discusión esta abierta por si quieren agregar algún otro tip.

PD: Las variables han sido renombradas para proteger a los verdaderos autores, cualquier coincidencia con la realidad es casualmente cierta y verídica.

Javassist ProxyFactory

Directo al grano, cómo construir un proxy con la librería javassist. Si quieres saber que es un proxy o un dynamic proxy, puedes revisar los post anteriores, Patrones De Diseño: Proxy, Java Dynamic Proxy y SpringFramework: ProxyFactoryBean.

Una de las cosas interesantes que tiene javassist es que no es necesario tener una clase con una interfaz definida, tal como se usa en JDK Dynamic Proxy o con ProxyFactoryBean (aunque también lo tiene usando cglib) para poder crear el Proxy.

Otra de las características de javassist es que posee mecanismos para mitigar la baja de rendimiento al utilizar el patrón Proxy, internamente maneja cache e intervención a nivel de bytecode para optimizar su uso.

Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level.

Javassist ProxyFactory

Vamos al ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CalculadoraProxyFactory {

/**
* La misma calculadora de siempre (con interfaz)
*/
public static Calculadora createCalculadoraInstance()
throws InstantiationException, IllegalAccessException {
ProxyFactory pf = new ProxyFactory();
pf.setSuperclass(CalculadoraImpl.class);
pf.setFilter(new SumaMethodFilter());
pf.setUseCache(true);
Class<?> proxyClass = pf.createClass();

Object proxyObject = proxyClass.newInstance();
((Proxy) proxyObject).setHandler(new CalculadoraMethodHandler());

return (Calculadora) proxyObject;
}

/**
* Una copia de la implementación de la calculadora pero sin interfaz
*/
public static CalculadoraWithoutInterface createCalculadoraWithoutInterface()
throws InstantiationException, IllegalAccessException {
ProxyFactory pf = new ProxyFactory();
pf.setSuperclass(CalculadoraWithoutInterface.class);
pf.setFilter(new SumaMethodFilter());
pf.setUseCache(true);
Class<?> proxyClass = pf.createClass();

Object proxyObject = proxyClass.newInstance();
((Proxy) proxyObject).setHandler(new CalculadoraMethodHandler());

return (CalculadoraWithoutInterface) proxyObject;
}
}

Como podrán ver, el uso del ProxyFactory de javassist es muy parecido a los otros (ProxyFactoryBean y JDK Dynamic Proxy), además usa un nuevo concepto llamado MethodFilter que permite agregar un filtro para los nombres de los métodos, con la finalidad de que el proxy sólo funciona cuando el método isHandled retorna true, de lo contrario hará una llamada directa a la clase RealSubject.

MethodFilter

Veamos la implementación de MethodFilter para este ejemplo:

1
2
3
4
5
6
7
8
9
final class SumaMethodFilter implements MethodFilter {

private static final String METHOD_NAME_TO_PROXY = "suma";

@Override
public boolean isHandled(Method m) {
return METHOD_NAME_TO_PROXY.equals(m.getName());
}
}

MethodHandler

Esta clase es la que contiene la lógica de negocio del proxy, es parecida a los interceptores (MethodInterceptor) de SpringFramework para el uso de ProxyFactoryBean o de InvocationHandler para JDK Dynamic Proxy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final class CalculadoraMethodHandler implements MethodHandler {

@Override
public Object invoke(Object self, Method thisMethod, Method proceed,
Object[] args) throws Throwable {

for (Object object : args) {
if (object instanceof Integer) {
Integer number = Integer.valueOf(object.toString());

if (number.intValue() > 0) {
System.out.println("Parameter [" + number + "]");
} else {
System.err.println("Invalid number [" + number + "]");
throw new RuntimeException("Invalid number [" + number
+ "]");
}
} else {
System.err.println("Invalid type");
throw new RuntimeException("Invalid type");
}
}
return proceed.invoke(self, args);
}
}

Test Unitario

Para hacer la prueba, realicé dos test unitarios, uno usando las mismas clases que he usado en los otros ejemplos y una nueva clase calculadora que no tiene una interfaz definida, con la finalidad de probar la funcionalidad de manipulación de bytecode en tiempo de runtime de javassist (crea una intrefaz a partir de la información de la clase).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ProxyFactoryJavassistTest {

private Calculadora _proxy;

private CalculadoraWithoutInterface _proxyWithoutInterface;

@Before
public void before() {
System.out.println("--------------------");
try {
_proxy = CalculadoraProxyFactory.createCalculadoraInstance();
_proxyWithoutInterface = CalculadoraProxyFactory
.createCalculadoraWithoutInterface();
assertNotNull(_proxy);
assertNotNull(_proxyWithoutInterface);
} catch (Exception e) {
fail();
}
}

// Con intrefaz definida

@Test
public void proxyFactoryJavassist() throws Exception {
Integer result = _proxy.suma(1, 2);
assertNotNull(result);
assertEquals(Integer.valueOf(3), result);
}

@Test(expected = RuntimeException.class)
public void shouldFailWithProxyFactory() {
Integer a = -1;
Integer b = 2;
_proxy.suma(a, b);
}

// Sin intrefaz definida

@Test
public void proxyFactoryJavassistWithoutInterface() throws Exception {
Integer result = _proxyWithoutInterface.suma(1, 2);
assertNotNull(result);
assertEquals(Integer.valueOf(3), result);
}

@Test(expected = RuntimeException.class)
public void shouldFailWithoutInterface() {
Integer a = -1;
Integer b = 2;
_proxyWithoutInterface.suma(a, b);
}
}

Y finalmente la salida a la consola del test unitario es la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running patterns.proxyjavassist.ProxyFactoryJavassistTest
--------------------
Invalid number [-1]
--------------------
Parameter [1]
Parameter [2]
--------------------
Invalid number [-1]
--------------------
Parameter [1]
Parameter [2]
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 sec

En otro post, trataré de profundizar en la librería javassist ya que desde hace mucho la vengo mirando como dependencias de varios frameworks y aplicaciones (Hibernate, jboss, Spring Framework) y me llama la atención el cómo poder generar bytecode utilizando una API. La otra librería que hace algo similar es cglib y que Hibernate usó en los inicios del proyecto, aunque al parecer ya va en retroceso (también le echaré un vistazo).

Nota: Si ven en los debug de Hibernate algo como NombreDeClase_$$_javassist_0 son los proxys que arma Hibernate con javassist para las entidades que se cargan en modo lazy.

SpringFramework: ProxyFactoryBean

Una manera sencilla de crear Proxy es usando SpringFramework, siempre y cuando tengas configurado tu aplicación con Spring, de otra forma, puedes buscar otras librerías que puedan ayudarte con el patrón Proxy (dinámico), (cglib y javassist) o puedes utilizar la API de java para crearlos. Revisa aquí Java Dynamic Proxy o Patrones De Diseño: Proxy.

Por ahora voy a mostrar como configurar un Proxy Dinámico con SprinFramework, configuraciones y par un de conceptos.

Conceptos

  • Target: Es la clase que será envuelta por el proxy (RealSubject)
  • Interceptor(es): Son las clases accesorias donde deberás agregar la lógica de negocio asociada al Proxy. Estos interceptores pueden ser mas de uno para un mismo target.
  • ProxyFactoryBean: Es la clase de Spring Framework para crear proxys dinámicos, al estilo Spring (con un FactoryBean). Deja un bean configurado dentro del contexto, listo para ser usado. Lo bueno de esta estrategia es que la creación del Proxy se realiza sólo una vez y cuando levanta el contexto, al contrario del Proxy que provee la API de Java, por cada invocación se debe crear el Proxy (aunque es rápido pero todo suma).

Interceptores

Los interceptores son clases que contienen la lógica de negocio del Proxy, es el símil a el InvocationHandler de la API de Java. Estas clases deben ser implementadas desde la interfaz Interceptor que es la de mas alta jerarquía o de su herencia, MethodInterceptor o ConstructorInterceptor. Estas últimas clases pertenecen a la librería de aopalliance versión 1.0 en el package org.aopalliance.intercept.

Para poder agregar las 2 funcionalidades (loggear los parámetros de entrada y validar que no sean negativos), implementaremos 2 Interceptores con la finalidad de aislar las lógicas de negocio de cada uno.

Implementación de Validador de Argumentos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ValidateArgumentsInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
for (Object arg : invocation.getArguments()) {
if (arg instanceof Integer) {
Integer number = Integer.class.cast(arg);
if (number.intValue() < 0) {
throw new InvalidNumberValueException("Invalid number ["
+ number + "]");
}
} else {
throw new InvalidNumberValueException("Invalid type");
}
}
return invocation.proceed();
}

public static class InvalidNumberValueException extends RuntimeException {

private static final long serialVersionUID = 3067612583360286918L;

/**
* @param message
*/
public InvalidNumberValueException(String message) {
super(message);
}

}
}

Implementación de Logger de Argumentos

1
2
3
4
5
6
7
8
9
10
11
public class LoggerArgumentsInterceptor implements MethodInterceptor {

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
for (Object arg : invocation.getArguments()) {
System.out.println("Argument " + arg.toString());
}
return invocation.proceed();
}

}

Ahora que tenemos los Interceptores, juntamos todo en la configuración del contexto de Spring.

Configurar contexto Spring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- RealSubject o target -->
<bean id="calculadora" class="patterns.dynamicproxy.CalculadoraImpl" />

<!-- Proxy -->
<bean id="calculadoraWithProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="calculadora" />
<property name="interceptorNames">
<list>
<value>validate</value>
<value>logger</value>
</list>
</property>
</bean>

<!-- Interceptores -->
<bean id="validate" class="patterns.proxyAOP.ValidateArgumentsInterceptor" />
<bean id="logger" class="patterns.proxyAOP.LoggerArgumentsInterceptor" />
</beans>

Donde:

  • calculadora: Es la clase a envolver en el Proxy (RealSubject o target).
  • calculadoraWithProxy: Proxy Factory que crea el bean configurado como Proxy. En este bean deben fijarse en las propiedades configuradas:
    • target: Es una referencia al bean con id calculadora.
    • interceptorNames: Es una lista con los id de los beans que harán de interceptores. En este caso son dos los interceptores validate y logger.

Notas

  • Como podrán ver, usé las mismas clases del post Java Dynamic Proxy y sólo agregue los Interceptores donde se concentra la lógica de negocio solicitada.
  • Recuerden que como se trata de un Proxy Dinámico se necesita que CalculadoraImpl implemente una interfaz conocida, en este caso, Calculadora.
  • La clase ProxyFactoryBean posee varios atributos mas que permiten agregarle funcionalidad y configuraciones al proxy, échenle un vistazo.

Test Unitario

Para poder probar las funcionalidades realicé el siguiente test unitario:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "/proxy-application-context.xml" })
public class CalculadoraServiceAOPTest {

@Autowired
@Qualifier("calculadoraWithProxy")
private Calculadora _proxy;

@Before
public void before() {
System.out.println("----------------------------------");
assertNotNull("Calculadora cannot be null", _proxy);
}

@Test
public void calculatorBeanWithProxyFactory() {
Integer a = 1;
Integer b = 2;

Integer result = _proxy.suma(a, b);
assertNotNull(result);
assertEquals(Integer.valueOf(3), result);
}

@Test(expected = InvalidNumberValueException.class)
public void shouldFailCalculatorBeanWithProxyFactory() {
Integer a = -1;
Integer b = 2;
_proxy.suma(a, b);
}

}

Y finalmente la salida de la consola:

1
2
3
4
5
6
7
8
9
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running patterns.proxyAOP.CalculadoraServiceAOPTest
----------------------------------
Argument 1
Argument 2
----------------------------------
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.592 sec

Espero les sirva, comentarios bienvenidos sean.

Java Dynamic Proxy

Los proxy dinámicos fueron introducidos en la API de Java 1.3 agregando múltiples funcionalidades a la plataforma Java. En este post pretendo enseñarles algunos usos de los proxy dinámicos y cómo implementar uno sencillo.

Primero tienen que conocer la definición de proxy y sus posibles usos, que ya fueron descritas en un post anterior, puedes revisarlo antes de continuar: Patrones de Diseño: Proxy

Primero debemos conocer las clases que hacen posible la implementación de Dynamic Proxy en la API de Java, estas son java.lang.reflect.Proxy y java.lang.reflect.InvocationHandler, además debes tener conceptos básicos de Reflexión (sólo para la invocación de métodos).

java.lang.reflect.Proxy

Para crear un objeto Proxy debes utilizar la clase java.lang.reflect.Proxy de la siguiente forma:

1
Object obj = Proxy.newProxyInstance(classLoader, interfaces, handler);

Donde:

  • classLoader: classloader que contiene la definición de las interfaces que componen el objeto target (o RealSubject).
  • interfaces: Arreglo con la definición de las interfaces que implementa el objeto target (o RealSubject).
  • handler: Esta es la clase que nos permitirá implementar el Proxy, es la clase que contiene la lógica de negocio del Proxy.

java.lang.reflect.InvocationHandler

Esta interfaz debe ser implementada y tenemos el método invoke que hará las veces de interceptor, es aquí donde debemos escribir nuestra lógica de negocio.

1
2
3
4
5
public interface InvocationHandler {

public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

Reglas para crear un Dynamic Proxy

  • El objeto a “proxiar” debe tener una interfaz, no una clase concreta o abstracta, una Interfaz.
  • El arreglo de Interfaces no debe contener interfaces duplicadas.
  • Todas las interfaces deben ser visibles en el classloader que se esta utilizando (usado en la construcción del Proxy).
  • Las interfaces del proxy no deben tener conflictos de métodos. Ver la especificación de Lenguaje Java sobre sobre-escritura de métodos.

Ejemplo Práctico

Veamos un ejemplo práctico de la implementación de un proxy dinámico usando la API de Java:

1
2
3
4
5
6
7
8
9
10
public interface Calculadora {

/**
* @param a
* @param b
* @return suma de a+b
*/
Integer suma(Integer a, Integer b);

}

Ahora veamos la implementación de Calculadora:

1
2
3
4
5
6
7
8
public class CalculadoraImpl implements Calculadora {

@Override
public Integer suma(Integer a, Integer b) {
return a + b;
}

}

Supongamos que estas clases ya están creadas y necesitamos agregar un par de funcionalidades sin tener que tocar el código ya existente. La primera funcionalidad es validar que los números a y b sean mayores a 0 y la segunda funcionalidad es loggear los parámetros de entrada.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class CalculadoraProxyFactory implements InvocationHandler {

private final Calculadora _target;

/**
* @param target
*/
public CalculadoraProxyFactory(Calculadora target) {
_target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
for (Object object : args) {
if (object instanceof Integer) {
Integer number = Integer.valueOf(object.toString());

if (number.intValue() > 0) {
System.out.println("Parameter [" + number + "]");
} else {
System.err.println("Invalid number [" + number + "]");
throw new RuntimeException("Invalid number [" + number
+ "]");
}
} else {
System.err.println("Invalid type");
throw new RuntimeException("Invalid type");
}
}
System.out.println("Execute method [" + method.getName() + "]");
return method.invoke(_target, args);
}

/**
* Factory para crear proxy de Calculadora
*
* @param target
* @return proxy de Calculadora
*/
public static Calculadora proxyFactory(Calculadora target) {
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
return (Calculadora) Proxy.newProxyInstance(classLoader, interfaces,
new CalculadoraProxyFactory(target));
}
}

Como podrán ver en el ejemplo, en el método invoke se hace el control y el log sobre los argumentos que entran al método suma del RealSubject. Además para simplificar la creación del Proxy, se implementó un method-factory que crea el proxy dinámico (método proxyFactory).

Ahora el test unitario que prueba el funcionamiento del Proxy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CalculadoraProxyFactoryTest {

private Calculadora _calculadoraProxy;

@Before
public void before() {
System.out.println("------------------------------");
_calculadoraProxy = CalculadoraProxyFactory
.proxyFactory(new CalculadoraImpl());
}

@Test
public void shouldWork() throws Exception {
Integer result = _calculadoraProxy.suma(1, 2);
assertNotNull(result);
assertEquals(Integer.valueOf(3), result);
}

@Test(expected = RuntimeException.class)
public void failWrongNumber() throws Exception {
_calculadoraProxy.suma(-1, 2);
}

@Test(expected = RuntimeException.class)
public void failNullNumber() throws Exception {
_calculadoraProxy.suma(null, 2);
}
}

La salida de este test a la consola con maven es la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running patterns.dynamicproxy.CalculadoraProxyFactoryTest
------------------------------
Invalid number [-1]
------------------------------
Invalid type
------------------------------
Parameter [1]
Parameter [2]
Execute method [suma]

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.045 sec

Espero que les haya sido útil, en un próximo post escribiré sobre otras formas de crear Proxy. Deje su comentario.

Patrones de Diseño: Proxy

El patrón Proxy

El patrón Proxy esta clasificado dentro de los Patrones Estructurales, es también conocido como, Embajador, Apoderado (surrogante). Veamos primero su definición:

Un proxy fuerza a que un método de un objeto RealSubject sea indirectamente ejecutado a través de un objeto Proxy, el cual actúa como delegado o sustituto del objeto RealSubject. Los objetos Proxy son usualmente declarados sin que el Cliente sepa de que se trata de un Objeto Proxy.

Les dejo un diagrama de clases de cómo se implementa este patrón.

Usos del patrón Proxy

  • Control de Acceso: El patrón puede ser usado para controlar el acceso a los métodos del RealSubject. La lógica del control de acceso se delegará en el proxy, de esta forma quedará mas limpio el RealSubject (en términos de código).
  • Acceso a objetos remotos: Representación de un objeto remoto de forma local, es decir, el Proxy resuelve y enmascara la forma en cómo conectarse al objeto remoto. Ejemplos de implementación de este patrón los pueden encontrar en EJB y RMI.
  • Proxy Virtual: Crea objetos costosos bajo demanda. Por ejemplo, al iniciar una aplicación se pueden instanciar con este patrón todos los iconos de dicha aplicación, pero sólo cuando éste sea solicitado realmente ira a buscarlo al disco y presentado al usuario.
  • Proxy de Referencia Inteligente: Sustituto de una referencia que hace operaciones adicionales cuando se accede a un objeto, como por ejemplo, controlar concurrencia, contar numero de instancias, cargar un objeto en memoria, manejo de cache)

Implementación de Ejemplo

Partamos con la interfaz que deben cumplir tanto RealSubject como el SubjectProxy

1
2
3
4
5
public interface Subject {

void doOperation(String username);

}

Veamos la implementación del RealSubject

1
2
3
4
5
6
7
8
public class SubjectReal implements Subject {

@Override
public void doOperation(String username) {
System.out.println("doOperation Real");
}

}

Finalmente la implementación del SubjectProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SubjectProxy implements Subject {

private final Subject _subjectReal;

private boolean _connected = false;

public SubjectProxy() {
_subjectReal = new SubjectReal();
}

@Override
public void doOperation(String username) {
// Control de Acceso simple
if (!username.isEmpty() && "admin".equals(username)) {
System.out.println("doOperacion proxied");
if (_connected) {
_subjectReal.doOperation(username);
} else {
connectToRemote();
_subjectReal.doOperation(username);
}
} else {
System.out.println("Access Denied");
}
}

private void connectToRemote() {
System.out.println("Connecting to remote");
_connected = true;
}
}

Nuestro cliente (quien consumirá a Subject) será un test unitario:

1
2
3
4
5
6
7
8
public class SubjectTest {

@Test
public void testSubjectSimpleUser() {
Subject subjectProxied = new SubjectProxy();
subjectProxied.doOperation("user");
}
}

Esto imprimirá lo siguiente en la consola (systemOut)

1
Access Denied

La explicación al Access Denied esta dada porque el patrón Proxy se utilizó como control de acceso al método doOperation. Sólo si el username es admin se ejecutará dicho método.

Veamos un Segundo Test Unitario

1
2
3
4
5
6
7
8
9
public class SubjectTest {

@Test
public void testSubjectAdmin() {
Subject subjectProxied = new SubjectProxy();
subjectProxied.doOperation("admin");
subjectProxied.doOperation("admin");
}
}

Esta es la salida:

1
2
3
4
5
doOperacion proxied
Connecting to remote
doOperation Real
doOperacion proxied
doOperation Real

En este caso, el usuario admin puede ejecutar la operación y la primera vez que lo haga se conectará un objeto remoto o difícil de crear (simulado para el ejemplo). Luego una vez conectado, podrá ejecutar la operación.

Nuevamente el usuario admin ejecutará la operación por segunda vez y el SubjectProxy ya posee una conexión al objeto remoto, aquí simplemente ejecutará la operación.

Espero les haya gustado y les sirva en sus proyectos, en el siguiente post les comentaré sobre Dynamic Proxy de Java.

Referencias:

Nota: Busquen el error en wikipedia español si lo encuentra me lo comenta ;)