Cómo hacer excepciones personalizadas en Java

En este artículo, cubriremos el proceso de creación de excepciones personalizadas tanto marcadas como no marcadas en Java. Si desea leer más sobre excepciones y exc...

Visión general

En este artículo, cubriremos el proceso de creación de excepciones personalizadas tanto marcadas como no marcadas en Java.

If you'd like to read more about exceptions and exception handling in Java, we've covered it in detail in - Manejo de excepciones en Java: una guía completa con las mejores y peores prácticas

¿Por qué usar excepciones personalizadas? {#por qué usar excepciones personalizadas}

Aunque las excepciones de Java, tal como son, cubren casi todos los casos y condiciones excepcionales, su aplicación puede generar una excepción personalizada específica, única para su código y lógica.

A veces, necesitamos crear las nuestras para representar excepciones de lógica de negocios, es decir, excepciones que son específicas de nuestra lógica de negocios o flujo de trabajo. Por ejemplo EmailNotUniqueException, InvalidUserStateException, etc.

Ayudan a los clientes de aplicaciones a comprender mejor qué salió mal. Son particularmente útiles para realizar el manejo de excepciones para API REST, ya que las diferentes restricciones de la lógica comercial requieren que se envíen diferentes códigos de respuesta al cliente.

Si definir una excepción personalizada no proporciona ningún beneficio sobre el uso de una excepción regular en Java, no hay necesidad de definir excepciones personalizadas y solo debe apegarse a las que ya están disponibles, no es necesario inventar agua caliente nuevamente.

Excepción marcada personalizada

Consideremos un escenario en el que queremos validar un correo electrónico que se pasa como argumento a un método.

Queremos comprobar si es válido o no. Ahora podríamos usar la ‘IllegalArgumentException’ incorporada de Java, que está bien si solo estamos verificando una sola cosa, como si coincide con un REGEX predefinido o no.

Pero supongamos que también tenemos una condición comercial para verificar que todos los correos electrónicos en nuestro sistema deben ser únicos. Ahora tenemos que realizar una segunda comprobación (llamada DB/red). Por supuesto, podemos usar la misma IllegalArgumentException, pero no estará claro cuál es la causa exacta: si el correo electrónico falló en la validación REGEX o si el correo electrónico ya existe en la base de datos.

Vamos a crear una excepción personalizada para manejar esta situación. Para crear una excepción, como cualquier otra excepción, debemos extender la clase java.lang.Exception:

1
2
3
4
5
6
public class EmailNotUniqueException extends Exception {

    public EmailNotUniqueException(String message) {
        super(message);
    }
}

Tenga en cuenta que proporcionamos un constructor que toma un mensaje de error String y llama al constructor de la clase principal. Ahora, esto no es obligatorio, pero es una práctica común tener algún tipo de detalles sobre la excepción que ocurrió.

Al llamar a super(mensaje), inicializamos el mensaje de error de la excepción y la clase base se encarga de configurar el mensaje personalizado, de acuerdo con el mensaje.

Ahora, usemos esta excepción personalizada en nuestro código. Dado que estamos definiendo un método que puede lanzar una excepción en la capa de servicio, lo marcaremos con la palabra clave throws.

Si el correo electrónico de entrada ya existe en nuestra base de datos (en este caso, una lista), “lanzamos” nuestra excepción personalizada:

1
2
3
4
5
6
7
8
9
public class RegistrationService {  
    List<String> registeredEmails = Arrays.asList("[correo electrónico protegido]", "[correo electrónico protegido]");

    public void validateEmail(String email) throws EmailNotUniqueException {
        if (registeredEmails.contains(email)) {
            throw new EmailNotUniqueException("Email Already Registered");
        }
    }
}

Ahora escribamos un cliente para nuestro servicio. Dado que es una excepción verificada, debemos cumplir con la regla de manejar o declarar. En el ejemplo anterior, decidimos "manejarlo":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class RegistrationServiceClient {  
    public static void main(String[] args) {
        RegistrationService service = new RegistrationService();
        try {
            service.validateEmail("[correo electrónico protegido]");
        } catch (EmailNotUniqueException e) {
            // logging and handling the situation
        }
    }
}

Ejecutar este fragmento de código producirá:

1
2
3
mynotes.custom.checked.exception.EmailNotUniqueException: Email Already Registered  
    at mynotes.custom.checked.exception.RegistrationService.validateEmail(RegistrationService.java:12)
    at mynotes.custom.checked.exception.RegistrationServiceClient.main(RegistrationServiceClient.java:9)

Nota: El proceso de manejo de excepciones se omite por brevedad, pero no obstante es un proceso importante.

Excepción no verificada personalizada

Esto funciona perfectamente bien, pero nuestro código se ha vuelto un poco desordenado. Además, estamos forzando al cliente a capturar nuestra excepción en un bloque try-catch. En algunos casos, esto puede llevar a obligar a los desarrolladores a escribir código repetitivo.

En este caso, puede ser beneficioso crear una excepción de tiempo de ejecución personalizada en su lugar. Para crear una excepción no verificada personalizada, debemos extender la clase java.lang.RuntimeException.

Consideremos la situación en la que tenemos que verificar si el correo electrónico tiene un nombre de dominio válido o no:

1
2
3
4
5
6
public class DomainNotValidException extends RuntimeException {

    public DomainNotValidException(String message) {
        super(message);
    }
}

Ahora vamos a usarlo en nuestro servicio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class RegistrationService {

    public void validateEmail(String email) {
        if (!isDomainValid(email)) {
            throw new DomainNotValidException("Invalid domain");
        }
    }

    private boolean isDomainValid(String email) {
        List<String> validDomains = Arrays.asList("gmail.com", "yahoo.com", "outlook.com");
        if (validDomains.contains(email.substring(email.indexOf("@") + 1))) {
            return true;
        }
        return false;
    }
}

Tenga en cuenta que no tuvimos que usar las palabras clave throws en la firma del método, ya que es una excepción no verificada.

Ahora escribamos un cliente para nuestro servicio. No tenemos que usar un bloque try-catch esta vez:

1
2
3
4
5
6
7
public class RegistrationServiceClient {

    public static void main(String[] args) {
        RegistrationService service = new RegistrationService();
        service.validateEmail("[correo electrónico protegido]");
    }
}

Ejecutar este fragmento de código producirá:

1
2
3
Exception in thread "main" mynotes.custom.unchecked.exception.DomainNotValidException: Invalid domain  
    at mynotes.custom.unchecked.exception.RegistrationService.validateEmail(RegistrationService.java:10)
    at mynotes.custom.unchecked.exception.RegistrationServiceClient.main(RegistrationServiceClient.java:7)

Nota: Por supuesto, puede rodear su código con un bloque try-catch para capturar la excepción que surja, pero ahora el compilador no lo fuerza.

Volver a generar una excepción envuelta dentro de una excepción personalizada {#relanzar una excepción envuelta dentro de una excepción personalizada}

A veces necesitamos capturar una excepción y volver a lanzarla agregando algunos detalles más. Esto suele ser común si tiene varios códigos de error definidos en su aplicación que deben registrarse o devolverse al cliente en caso de esa excepción en particular.

Suponga que su aplicación tiene una clase ErrorCodes estándar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public enum ErrorCodes {  
    VALIDATION_PARSE_ERROR(422);

    private int code;

    ErrorCodes(int code) {
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}

Vamos a crear nuestra excepción personalizada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class InvalidCurrencyDataException extends RuntimeException {

    private Integer errorCode;

    public InvalidCurrencyDataException(String message) {
        super(message);
    }

    public InvalidCurrencyDataException(String message, Throwable cause) {
        super(message, cause);
    }

    public InvalidCurrencyDataException(String message, Throwable cause, ErrorCodes errorCode) {
        super(message, cause);
        this.errorCode = errorCode.getCode();
    }

    public Integer getErrorCode() {
        return errorCode;
    }
}

Tenga en cuenta que tenemos varios constructores y dejemos que la clase de servicio decida cuál usar. Dado que estamos volviendo a lanzar la excepción, siempre es una buena práctica capturar la causa raíz de la excepción, por lo tanto, el argumento Throwable que se puede pasar` al constructor de la clase principal.

También estamos capturando el código de error en uno de los constructores y configuramos el errorCode dentro de la excepción misma. El errorCode podría ser utilizado por el cliente para iniciar sesión o para cualquier otro propósito. Esto ayuda en un estándar más centralizado para el manejo de excepciones.

Escribamos nuestra clase de servicio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class CurrencyService {  
    public String convertDollarsToEuros(String value) {
        try {
        int x = Integer.parseInt(value);
    } catch (NumberFormatException e) {
        throw new InvalidCurrencyDataException("Invalid data", e, ErrorCodes.VALIDATION_PARSE_ERROR);
        }
        return value;
    }
}

Así que detectamos la NumberFormatException estándar y lanzamos nuestra propia InvalidCurrencyDataException. Pasamos la excepción principal a nuestra excepción personalizada para que no perdamos la causa raíz por la que ocurrieron.

Escribamos un cliente de prueba para este servicio:

1
2
3
4
5
6
public class CurrencyClient {  
    public static void main(String[] args) {
        CurrencyService service = new CurrencyService();
    service.convertDollarsToEuros("asd");
    }
}

Producción:

1
2
3
4
5
6
7
8
9
Exception in thread "main" mynotes.custom.unchecked.exception.InvalidCurrencyDataException: Invalid data  
    at mynotes.custom.unchecked.exception.CurrencyService.convertDollarsToEuro(CurrencyService.java:10)
    at mynotes.custom.unchecked.exception.CurrencyClient.main(CurrencyClient.java:8)
Caused by: java.lang.NumberFormatException: For input string: "asd"  
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:580)
    at java.lang.Integer.parseInt(Integer.java:615)
    at mynotes.custom.unchecked.exception.CurrencyService.convertDollarsToEuro(CurrencyService.java:8)
    ... 1 more

Como puede ver, tenemos un buen seguimiento de pila de la excepción que podría ser útil para fines de depuración.

Mejores prácticas para excepciones personalizadas

  • Adherirse a la convención de nomenclatura general en todo el ecosistema de Java - Todos los nombres de clase de excepción personalizados deben terminar con "Excepción"
  • Evite hacer excepciones personalizadas si las excepciones estándar del propio JDK pueden cumplir el propósito. En la mayoría de los casos, no es necesario definir excepciones personalizadas.
  • Prefiera las excepciones de tiempo de ejecución a las excepciones comprobadas. Frameworks como Primavera han envuelto todas las excepciones marcadas en excepciones de tiempo de ejecución, por lo que no obligan al cliente a escribir código repetitivo que no quiere o no necesita.
  • Proporcione muchos constructores sobrecargados en función de cómo se lanzaría la excepción personalizada. Si se está utilizando para volver a generar una excepción existente, definitivamente proporcione un constructor que establezca la causa.

Conclusión

Las excepciones personalizadas se utilizan para la lógica y los requisitos comerciales específicos. En este artículo, discutimos la necesidad de ellos y cubrimos su uso.

El código de los ejemplos utilizados en este artículo se puede encontrar en Github. ).

Licensed under CC BY-NC-SA 4.0