Herramientas de usuario

Herramientas del sitio


unidades:07_arquitectura:02_excepciones
no way to compare when less than two revisions

Diferencias

Muestra las diferencias entre dos versiones de la página.


unidades:07_arquitectura:02_excepciones [2023/04/07 21:26] (actual) – creado - editor externo 127.0.0.1
Línea 1: Línea 1:
 +====== 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 [[patrones:excepciones]].
 +===== Tratamiento de Excepciones =====
 +Al realizar una operación con Hibernate se pueden lanzar cualquiera de las siguiente 4 excepciones:
 +  * <javadoc jee6>javax.validation.ConstraintViolationException</javadoc>
 +  * <javadoc h41>org.hibernate.exception.ConstraintViolationException</javadoc>
 +  * <javadoc jdk7>java.lang.RuntimeException|RuntimeException</javadoc> (( Realmente Hibernate lanza excepciones del tipo <javadoc h41>org.hibernate.HibernateException</javadoc> pero como hereda de <javadoc jdk7>java.lang.RuntimeException|RuntimeException</javadoc> podemos tratar únicamente ésta última ))
 +  * <javadoc jdk7>java.lang.Exception|Exception</javadoc>
  
 +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:
 +<code java 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);
 +}
 +</code>
 +
 +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.
 +
 +<code java>
 +try {
 +    if (session.getTransaction().isActive()) {
 +        session.getTransaction().rollback();
 +    }
 +} catch (Exception exc) {
 +    LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc);
 +}
 +</code>
 +
 +La propiedad estática ''LOGGER'' la definiremos como una propiedad a nivel de clase usando [[http://docs.oracle.com/javase/6/docs/technotes/guides/logging/|Java Logging APIs]] de la siguiente forma:
 +<code java>
 +private final static Logger LOGGER = Logger.getLogger(ProfesorController.class .getName());
 +</code>
 +
 +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:
 +<code java>
 +throw ex;
 +</code>
 +
 +==== Exception ====
 +Si la excepción es de tipo ''Exception'' lo único que hacemos es relanzar la excepción envuelta en una ''RuntimeException'':
 +<code java>
 +throw new RuntimeException(ex);
 +</code>
 +
 +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''.
 +<code java>
 +throw new BussinessException(cve);
 +</code>
 +
 +==== org.hibernate.exception.ConstraintViolationException ====
 +Esta excepción la vamos a reconvertir en una ''BussinessException''. En el siguiente apartado se explica en qué consiste ''BussinessException''.
 +<code java>
 +throw new BussinessException(cve);
 +</code>
 +
 +==== 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. 
 +<code java>
 +throw ex;
 +</code>
 +
 +
 +
 +<note tip>
 +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.
 +</note>
 +
 +===== Mejoras con BusinessException =====
 +En el apartado anterior hemos visto cómo tratamos las siguientes excepciones transformándolas en ''BussinessException'':
 +  * <javadoc jee6>javax.validation.ConstraintViolationException</javadoc>
 +  * <javadoc h41>org.hibernate.exception.ConstraintViolationException</javadoc>
 +
 +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 <javadoc jdk7>java.lang.Exception</javadoc> 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]]
 +  * [[#Simplificar el uso de javax.validation.ConstraintViolationException]]
 +  * [[#Mejorar los mensajes de error]]
 +
 +==== 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 <javadoc jee6>javax.validation.ConstraintViolationException</javadoc> y <javadoc h41>org.hibernate.exception.ConstraintViolationException</javadoc>.
 +
 +<note tip>
 +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.
 +</note>
 +
 +==== Simplificar el uso de javax.validation.ConstraintViolationException ====
 +El uso de <javadoc jee6>javax.validation.ConstraintViolationException</javadoc> puede ser un poco raro de usar debido a las propiedades de la clase <javadoc jee6>javax.validation.ConstraintViolation</javadoc>.
 +
 +¿Qué hace el método <javadoc jee6>javax.validation.ConstraintViolation#getPropertyPath()|getPropertyPath()</javadoc>?
 +¿Y <javadoc jee6>javax.validation.ConstraintViolation#getLeafBean()|getLeafBean()</javadoc>?
 +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 ''BussinessException''contendrá una lista de objetos de ''BussinessMessage''. Esta estructura es similar a la que encontramos entre <javadoc jee6>javax.validation.ConstraintViolationException</javadoc> y <javadoc jee6>javax.validation.ConstraintViolation</javadoc>
 +
 +En el siguiente diagrama UML podemos ver las similitudes y diferencias:
 +
 +<uml>
 +class BussinessException
 +BussinessException : Set<BussinessMessage> getBussinessMessages()
 +
 +class BussinessMessage
 +
 +BussinessMessage : getFieldName()
 +BussinessMessage : getMessage()
 +
 +BussinessException "1" -- "1..n" BussinessMessage : bussinessMessages
 +
 +class ConstraintViolationException
 +ConstraintViolationException : Set<ConstraintViolation> 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
 +
 +</uml>
 +
 +Como podemos ver , aunque hayamos perdido cierta funcionalidad que debe encontrase en <javadoc jee6>javax.validation.ConstraintViolation</javadoc>, 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 <javadoc jee6>javax.validation.ConstraintViolation</javadoc> 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
 +
 +<note tip>
 +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 <javadoc jee6>javax.validation.ConstraintViolation</javadoc> 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.
 +</note>
 +
 +===== El código de BussinessException =====
 +Ya hemos visto las ventajas y el diseño de ''BussinessException'', veamos ahora el código que los implementa.
 +
 +<code java 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;
 +    }
 +}
 +</code>
 +
 +Podemos ver que el código es bastante sencillo. Consta principalmente de varios constructores y el método ''getBussinessMessages()''.
 +  * Línea 3: Un <javadoc>java.util.TreeSet</javadoc> donde guardar todos los ''BussinessMessage''. Se usa un <javadoc>java.util.TreeSet|TreeSet</javadoc> 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 <javadoc jdk7>java.lang.Exception|Exception</javadoc>. Eso permite mostrar al usuario un mensaje aunque sea un <javadoc jdk7>java.lang.Exception|Exception</javadoc>.
 +  * Líneas 13-15: Constructor al que se le pasa una <javadoc jee6>javax.validation.ConstraintViolationException</javadoc>.Este constructor creará un ''BussinessMessage'' por cada uno de los <javadoc jee6>javax.validation.ConstraintViolation</javadoc>.
 +  * Líneas 29-31: Constructor al que se le pasa una <javadoc h41>org.hibernate.exception.ConstraintViolationException</javadoc>. 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:
 +<code java 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"); 
 +        } 
 +    }
 +
 +    
 +}
 +</code>
 +
 +La clase ''BussinessMessage'' es también sencilla pero tiene la particularidad de implementar el interfaz <javadoc>java.lang.Comparable</javadoc>.
 +  * Línea 1: Implementar el interfaz <javadoc>java.lang.Comparable|Comparable</javadoc>. Se implementa este interfaz para que se pueda usar dentro de la clase <javadoc>java.util.TreeSet</javadoc> 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 <javadoc>java.lang.Comparable#compareTo(T)|compareTo(T)</javadoc> del interfaz <javadoc>java.lang.Comparable|Comparable</javadoc>. 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:
 +
 +<code java 1 | Caption.java>
 +@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
 +@Retention(RetentionPolicy.RUNTIME)
 +public @interface Caption {
 +    String value();
 +}
 +</code>
 +
 +Ahora nuestras clase de dominio pueden quedar de la siguiente forma:
 +<code java 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(){ 
 +    }
 +}
 +</code>
 +
 +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.
 +
 +<note tip>
 +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.
 +</note>
 +
 +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:
 +<code java 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;
 +    }
 +}
 +</code>
 +
 +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 <javadoc jee6>javax.validation.Path</javadoc> que puede contener varios nombres de campos. Se obtienen todos ellos separados por puntos.
 +
 +Por último debemos modificar la línea 22 de ''BussinessException'':
 +<code java>
 +fieldName=constraintViolation.getPropertyPath().toString();
 +</code>
 +para que quede de la siguiente forma:
 +<code java>
 +fieldName=getCaptions(constraintViolation.getRootBeanClass(), constraintViolation.getPropertyPath());
 +</code>
 +
 +<note>
 +Para la realización de éste código se han usado las clases <javadoc s31>org.springframework.util.ReflectionUtils</javadoc> y <javadoc s31>org.springframework.util.StringUtils</javadoc> 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.
 +</note>
unidades/07_arquitectura/02_excepciones.txt · Última modificación: 2023/04/07 21:26 por 127.0.0.1