Herramientas de usuario

Herramientas del sitio


unidades:07_arquitectura:02_excepciones

Excepciones

Nuestro siguiente tema a tratar con la arquitectura de Hibernate es el tratamiento de las excepciones.Antes de ver este tema es recomendable la lectura de Tratamiento de Excepciones.

Tratamiento de Excepciones

Al realizar una operación con Hibernate se pueden lanzar cualquiera de las siguiente 4 excepciones:

Hemos indicado únicamente estas 4 excepciones ya que vamos a tratar cada una de ellas de forma distinta , no siendo necesario tratar ninguna otra ya que siempre caerán dentro de alguna de las cuatro.

Veamos cómo vamos a tratarlas. El siguiente código dentro del catch deberá ponerse siempre que usemos Hibernate:

1
try {
    session.beginTransaction();
    session.saveOrUpdate(entity);
    session.getTransaction().commit();
} catch (javax.validation.ConstraintViolationException cve) {
    try {
        if (session.getTransaction().isActive()) {
            session.getTransaction().rollback();
        }
    } catch (Exception exc) {
        LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc);
    }
    throw new BussinessException(cve);            
} catch (org.hibernate.exception.ConstraintViolationException cve) {
    try {
        if (session.getTransaction().isActive()) {
            session.getTransaction().rollback();
        }
    } catch (Exception exc) {
        LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc);
    }
    throw new BussinessException(cve);
} catch (BussinessException ex) {
    try {
        if (session.getTransaction().isActive()) {
            session.getTransaction().rollback();
        }
    } catch (Exception exc) {
        LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc);
    }
    throw ex;               
} catch (RuntimeException ex) {
    try {
        if (session.getTransaction().isActive()) {
            session.getTransaction().rollback();
        }
    } catch (Exception exc) {
        LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc);
    }
    throw ex;
} catch (Exception ex) {
    try {
        if (session.getTransaction().isActive()) {
            session.getTransaction().rollback();
        }
    } catch (Exception exc) {
        LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc);
    }
    throw new RuntimeException(ex);
}

Lo que debemos hacer siempre en las 4 excepciones es ver si hay una transacción activa y en ese caso hacer un rollback. Si al hacer un rollback se produce un error , simplemente generaremos un mensaje en el log.

try {
    if (session.getTransaction().isActive()) {
        session.getTransaction().rollback();
    }
} catch (Exception exc) {
    LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc);
}

La propiedad estática LOGGER la definiremos como una propiedad a nivel de clase usando Java Logging APIs de la siguiente forma:

private final static Logger LOGGER = Logger.getLogger(ProfesorController.class .getName());

Veamos ahora el tratamiento individualizado para cada una de ellas:

RuntimeException

Si la excepción es de tipo RuntimeException lo único que hacemos es volver a relanzar la misma excepción mediante:

throw ex;

Exception

Si la excepción es de tipo Exception lo único que hacemos es relanzar la excepción envuelta en una RuntimeException:

throw new RuntimeException(ex);

De esa forma evitamos la obligación de tratar las Checked Exceptions de Java cuando no sabemos que hacer con ella.

javax.validation.ConstraintViolationException

Esta excepción la vamos a reconvertir en una BussinessException. En el siguiente apartado se explica en qué consiste BussinessException.

throw new BussinessException(cve);

org.hibernate.exception.ConstraintViolationException

Esta excepción la vamos a reconvertir en una BussinessException. En el siguiente apartado se explica en qué consiste BussinessException.

throw new BussinessException(cve);

BussinessException

Si se produce ésta excepción simplemente la volveremos a relanzar sin modificar nada ya que queremos que se vea tal y como es.

throw ex;
Al poner todos los catch en un trozo de código de Hibernate, puede que el compilador dé un error ya que alguna de las excepciones no se lanza nunca. En ese caso simplemente eliminaremos el catch que está dando el error.

Mejoras con BusinessException

En el apartado anterior hemos visto cómo tratamos las siguientes excepciones transformándolas en BussinessException:

El significado de la nueva excepción es avisarnos de que los datos de las entidades contienen algún tipo de error y que el usuario debe modificarlo. Por ello la excepción hereda de java.lang.Exception ya que el error sí que es recuperable y el código debería estar preparado para avisar de ello al usuario y que éste corrija los datos.

Además de éso, el usar BussinessException tiene las siguientes ventajas:

Unificar el tratamiento de las excepciones

Desde otras clases de la aplicación , ahora al llamar a los métodos que tratan con Hibernate sólo deberemos preocuparnos de tratar la nueva excepción BussinessException. Con ello simplificamos el resto del código de la aplicación. De lo contrario, habríamos tenido que arrastrar siempre el tratar con las 2 excepciones javax.validation.ConstraintViolationException y org.hibernate.exception.ConstraintViolationException.

Otra ventaja de la unificación de excepciones es que si nuestra aplicación dejará de usar Hibernate para volver a JDBC podríamos seguir usando la nueva BussinessException sin necesidad de cambiar nada del manejo del excepciones del resto de la aplicación. Es decir que es mejor independizar de excepciones específicas del Hibernate.

Simplificar el uso de javax.validation.ConstraintViolationException

El uso de javax.validation.ConstraintViolationException puede ser un poco raro de usar debido a las propiedades de la clase javax.validation.ConstraintViolation.

¿Qué hace el método getPropertyPath()? ¿Y getLeafBean()? Etc. Si la mayoría de estos métodos no los vamos a usar , mejor será crear una clase que contenga sólo la información importante ya que ayudará a los programadores a usarla mas rápidamente y sin errores.

Por ello BussinessExceptioncontendrá una lista de objetos de BussinessMessage. Esta estructura es similar a la que encontramos entre javax.validation.ConstraintViolationException y javax.validation.ConstraintViolation

En el siguiente diagrama UML podemos ver las similitudes y diferencias:


class BussinessException
BussinessException : Set getBussinessMessages()

class BussinessMessage

BussinessMessage : getFieldName()
BussinessMessage : getMessage()

BussinessException "1" -- "1..n" BussinessMessage : bussinessMessages

class ConstraintViolationException
ConstraintViolationException : Set getConstraintViolations()

class ConstraintViolation

ConstraintViolation: getPropertyPath()
ConstraintViolation: getMessage()
ConstraintViolation: getMessageTemplate()
ConstraintViolation: getRootBean()
ConstraintViolation: getRootBeanClass()
ConstraintViolation: getLeafBean()
ConstraintViolation: getInvalidValue()
ConstraintViolation: getConstraintDescriptor()

ConstraintViolationException "1" -- "1..n" ConstraintViolation : constraintViolations

Como podemos ver , aunque hayamos perdido cierta funcionalidad que debe encontrase en javax.validation.ConstraintViolation, si ésta no pensábamos usarla, es mejor simplificar el código.

Mejorar los mensajes de error

Al mostrar los mensajes de error que se incluyen en javax.validation.ConstraintViolation hay dos problemas principales:

  • El nombre de los campos es el de las propiedades Java y no un nombre amigable para el usuario.
  • Los mensajes empiezan en minúscula.

Un ejemplo de mensaje de error es el siguiente:

ape1 - el tamaño tiene que estar entre 3 y 50

¿Qué es “ape1”? ¿No debería indicarse “1º Apellido”?. Y luego el artículo “el” del mensaje debería debería empezar en mayúsculas.

Así que el mensaje correcto debería ser:

1º Apellido - El tamaño tiene que estar entre 3 y 50
Se podría argumentar que esos dos problemas deben ser de la capa de presentación y no de la capa de negocio. Pero ya que javax.validation.ConstraintViolation nos está ofreciendo los mensajes de error como un texto, deberíamos hacer todo lo posible por mejorar la calidad de los mismos y no hacer que cada presentación que realicemos tenga que preocuparse de estos mismos problemas.

El código de BussinessException

Ya hemos visto las ventajas y el diseño de BussinessException, veamos ahora el código que los implementa.

1 | BussinessException.java
public class BussinessException extends Exception {
 
    private Set<BussinessMessage> bussinessMessages = new TreeSet<>();
 
    public BussinessException(List<BussinessMessage> bussinessMessages) {
        this.bussinessMessages.addAll(bussinessMessages);
    }
 
    public BussinessException(BussinessMessage bussinessMessage) {
        this.bussinessMessages.add(bussinessMessage);
    }
 
    public BussinessException(Exception ex) {
        bussinessMessages.add(new BussinessMessage(null, ex.toString()));
    }
 
    public BussinessException(javax.validation.ConstraintViolationException cve) {
        for (ConstraintViolation constraintViolation : cve.getConstraintViolations()) {
            String fieldName;
            String message;
 
            fieldName = getCaptions(constraintViolation.getRootBeanClass(), constraintViolation.getPropertyPath());
            message = constraintViolation.getMessage();
 
            bussinessMessages.add(new BussinessMessage(fieldName, message));
        }
    }
 
    public BussinessException(org.hibernate.exception.ConstraintViolationException cve) {
        bussinessMessages.add(new BussinessMessage(null, cve.getLocalizedMessage()));
    }
 
    public Set<BussinessMessage> getBussinessMessages() {
        return bussinessMessages;
    }
}

Podemos ver que el código es bastante sencillo. Consta principalmente de varios constructores y el método getBussinessMessages().

  • Línea 3: Un java.util.TreeSet donde guardar todos los BussinessMessage. Se usa un TreeSet para que los mensajes salgan ordenados por orden alfabético.
  • Líneas 5-7: Constructor al que directamente se le pasa una lista de BussinessMessage.Ësto permitirá generar mensajes al usuario aunque no haya habido ninguna excepción.
  • Líneas 9-11: Constructor al que directamente se le pasa un único BussinessMessage.Esto permitirá generar mensajes al usuario aunque no haya habido ninguna excepción.
  • Líneas 13-15: Constructor al que se le pasa una Exception. Eso permite mostrar al usuario un mensaje aunque sea un Exception.
  • Líneas 13-15: Constructor al que se le pasa una javax.validation.ConstraintViolationException.Este constructor creará un BussinessMessage por cada uno de los javax.validation.ConstraintViolation.
  • Líneas 29-31: Constructor al que se le pasa una org.hibernate.exception.ConstraintViolationException. Creará un único BussinessMessage en función del mensaje de la excepción.
  • Línea 33-35: Retorna la lista de todos los BussinessMessage.

El código de BussinessMessage es el siguiente:

1 | BussinessMessage.java
public class BussinessMessage implements Comparable<BussinessMessage> {
    private final String fieldName;
    private final String message;
 
    public BussinessMessage(String fieldName, String message) {
        if (message==null) {
            throw new IllegalArgumentException("message no puede ser null");
        }
 
        if ((fieldName!=null) && (fieldName.trim().equals(""))) {
            this.fieldName =null;
        } else {
            this.fieldName = StringUtils.capitalize(fieldName);
        }
        this.message = StringUtils.capitalize(message);
    }
 
    @Override
    public String toString() {
        if (fieldName!=null) {
            return "'"+fieldName+ "'-"+message;
        } else {
            return message;
        }
    }
 
    /**
     * @return the fieldName
     */
    public String getFieldName() {
        return fieldName;
    }
 
    /**
     * @return the message
     */
    public String getMessage() {
        return message;
    }
 
 
    @Override
    public int compareTo(BussinessMessage o) {
        if ((getFieldName()==null) && (o.getFieldName()==null)) {
            return getMessage().compareTo(o.getMessage());
        } else if ((getFieldName()==null) && (o.getFieldName()!=null)) {
            return 1;
        } else if ((getFieldName()!=null) && (o.getFieldName()==null)) {
            return -1;
        } else if ((getFieldName()!=null) && (o.getFieldName()!=null)) {
            if (getFieldName().equals(o.getFieldName())) {
                return getMessage().compareTo(o.getMessage());
            } else {
                return getFieldName().compareTo(o.getFieldName());
            }
        } else {
           throw new RuntimeException("Error de lógica"); 
        } 
    }
 
 
}

La clase BussinessMessage es también sencilla pero tiene la particularidad de implementar el interfaz java.lang.Comparable.

  • Línea 1: Implementar el interfaz Comparable. Se implementa este interfaz para que se pueda usar dentro de la clase java.util.TreeSet y que los mensajes salga ordenados.
  • Líneas 13 y 15 : Se pone en mayúsculas el primer carácter del campo y el mensaje para que quede mas amigable al usuario.
  • Línea de la 43 a la 59: Método compareTo(T) del interfaz Comparable. Este método ordena las BussinessMessage de forma que primero van los mensajes que incluyen el nombre del campo y luego los que no.

Nombres de las columnas

La última parte que nos queda por tratar es el tema del nombre de las columnas. En el texto que muestra BussinessMessage se sigue viendo como nombre de una columna el nombre de la propiedad Java. Ya hemos explicado que esto debería ser solucionado.

Una sencilla solución es añadir una nueva anotación a cada propiedad que incluya un nombre amigable de dicha propiedad. La nueva anotación se llamará @Caption. Su código Java es el siguiente:

1 | Caption.java
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Caption {
    String value();
}

Ahora nuestras clase de dominio pueden quedar de la siguiente forma:

1 | Usuario.java
public class Profesor implements Serializable  {
 
    private int id;
    @NotBlank
    @Caption("Nombre")
    private String nombre;
 
    @NotBlank
    @Caption("1º Apellido")
    private String ape1;
 
    @Caption("2º Apellido")
    private String ape2; 
 
    public Profesor(){ 
    }
}

Vemos en las líneas 5, 9 y 12 como se ha añadido la nueva anotación @Caption para incluir un nombre de la columna válido para el usuario.

Vuelvo a insistir que ciertos frameworks de la capa de presentación hacen este mismo trabajo y se puede pensar que dicha anotación no debería estar en la capa de negocio.

Ahora debemos crear el código Java que en función de una clase de dominio y el nombre de la columna nos retorna el caption. El código Java es el siguiente:

1
private String getCaptions(Class clazz, Path path) {
    StringBuilder sb = new StringBuilder();
    if (path != null) {
        Class currentClazz = clazz;
        for (Path.Node node : path) {               
            ClassAndCaption clazzAndCaption = getSingleCaption(currentClazz, node.getName());
            if (clazzAndCaption.caption != null) {
                if (sb.length() != 0) {
                    sb.append(".");
                }
                if (node.isInIterable()) {
                    if (node.getIndex() != null) {
                        sb.append(node.getIndex());
                        sb.append("º ");
                        sb.append(clazzAndCaption.caption);
                    } else if (node.getKey() != null) {
                        sb.append(clazzAndCaption.caption);
                        sb.append(" de ");
                        sb.append(node.getKey());
                    } else {
                        sb.append(clazzAndCaption.caption);
                    }
                } else {
                    sb.append(clazzAndCaption.caption);
                }
            } else {
                sb.append("");
            }
            currentClazz = clazzAndCaption.clazz;
        }
 
        return sb.toString();
 
    } else {
        return null;
    }
 
}
 
private ClassAndCaption getSingleCaption(Class clazz, String fieldName) {
    ClassAndCaption clazzAndCaptionField;
    ClassAndCaption clazzAndCaptionMethod;
 
    if ((fieldName == null) || (fieldName.trim().equals(""))) {
        return new ClassAndCaption(clazz, null);
    }
 
    clazzAndCaptionField = getFieldCaption(clazz, fieldName);
    if ((clazzAndCaptionField != null) && (clazzAndCaptionField.caption != null)) {
        return clazzAndCaptionField;
    }
 
    clazzAndCaptionMethod = getMethodCaption(clazz, fieldName);
    if ((clazzAndCaptionMethod != null) && (clazzAndCaptionMethod.caption != null)) {
        return clazzAndCaptionMethod;
    }
 
    if (clazzAndCaptionField != null) {
        return new ClassAndCaption(clazzAndCaptionField.clazz,fieldName);
    } else if (clazzAndCaptionMethod != null) {
        return new ClassAndCaption(clazzAndCaptionMethod.clazz,fieldName);
    } else {
        return new ClassAndCaption(clazz, fieldName);
    }
}
 
private ClassAndCaption getFieldCaption(Class clazz, String fieldName) {
    Field field = ReflectionUtils.findField(clazz, fieldName);
    if (field == null) {
        return null;
    }
 
    Caption caption = field.getAnnotation(Caption.class);
    if (caption != null) {
        return new ClassAndCaption(field.getType(), caption.value());
    } else {
        return new ClassAndCaption(field.getType(), null);
    }
 
 
}
 
private ClassAndCaption getMethodCaption(Class clazz, String methodName) {
    String suffixMethodName = StringUtils.capitalize(methodName);
    Method method = ReflectionUtils.findMethod(clazz, "get" + suffixMethodName);
    if (method == null) {
        method = ReflectionUtils.findMethod(clazz, "is" + suffixMethodName);
        if (method == null) {
            return null;
        }
    }
 
    Caption caption = method.getAnnotation(Caption.class);
    if (caption != null) {
        return new ClassAndCaption(method.getReturnType(), caption.value());
    } else {
        return new ClassAndCaption(method.getReturnType(), null);
    }
 
 
}
 
private class ClassAndCaption {
 
    Class clazz;
    String caption;
 
    public ClassAndCaption(Class clazz, String caption) {
        this.clazz = clazz;
        this.caption = caption;
    }
}

Este código lo deberemos incluir en la clase BussinessException.java. No voy a entrar en detalles de como funciona dicho código ya que tiene cierta complejidad. Pero si que se va a explicar que hace cada función.

  • ClassAndCaption getMethodCaption(Class clazz, String methodName) (Líneas 59-74):Si un método tiene la anotación @Caption retorna el tipo y el valor del caption y sino retornará el tipo y null.
  • ClassAndCaption getFieldCaption(Class clazz, String fieldName) (Líneas 44-57): Si una propiedad tiene la anotación @Caption retorna el tipo y el valor del caption y sino retornará el tipo y null.
  • ClassAndCaption getSingleCaption(Class clazz, String fieldName) (Líneas 24-42): Retorna el tipo y el caption de un campo tanto si dicha anotación está propiedad Java o en un método Java.
  • String getCaptions(Class clazz, Path path) (Líneas 1-22): Dado que el nombre de los campos está en un objeto javax.validation.Path que puede contener varios nombres de campos. Se obtienen todos ellos separados por puntos.

Por último debemos modificar la línea 22 de BussinessException:

fieldName=constraintViolation.getPropertyPath().toString();

para que quede de la siguiente forma:

fieldName=getCaptions(constraintViolation.getRootBeanClass(), constraintViolation.getPropertyPath());
Para la realización de éste código se han usado las clases org.springframework.util.ReflectionUtils y org.springframework.util.StringUtils del framework Spring. Estas clases son simple clases de utilidad con funciones sencillas. Para poder usarlas se ha incluido el jar org.springframework.core-3.1.2.RELEASE.jar. En la siguiente sesión trataremos en profundidad el framework de Spring y explicaremos como instalarlo adecuadamente.Por ahora simplemente recordar el incluir el jar org.springframework.core-3.1.2.RELEASE.jar en los ejercicios que se realicen.
1)
Realmente Hibernate lanza excepciones del tipo org.hibernate.HibernateException pero como hereda de RuntimeException podemos tratar únicamente ésta última
unidades/07_arquitectura/02_excepciones.txt · Última modificación: 2023/04/07 21:26 por 127.0.0.1