Spring Boot y el sistema de logging

Logo Spring

Resumen del funcionamiento del sistema de logging en Spring Boot 1.4.0.RELEASE

Principales características

  • Spring Boot utiliza internamente Commons Logging
  • Existe configuraciones para Java Util Logging, Log4J2 y Logback
  • Por defecto se utiliza la Consola para mostrar los logs. Opcionalmente podemos escribir en fichero
  • Logback es el sistema de log utilizado si utilizamos los Spring Boot Starters (spring-boot-starter-parent, etc)
  • Logback tiene niveles ERROR, WARN, INFO, DEBUG o TRACE pero no tiene FATAL
  • Se permite extender el comportamiento de logback mediante extensiones
  • Por defecto se muestran los niveles ERROR, WARN e INFO

Activación del nivel DEBUG

Como cualquier aplicación de Spring Boot podemos activar el nivel de DEBUG como cualquier configuración más. Eso significa que podemos indicarselo como argumento al arrancar la aplicación, como variable de entorno, en el application.properties o application.yml, etc

Es importante mencionar que la activacion del nivel de DEBUG solo afecta internamente a Spring Boot, no a los logs de nuestra aplicación, los cuales los tenemos que configurar mediante otro mecanismo.

Por ejemplo,

En el arranque de la aplicación

$ java -jar my-application.jar --debug

En el fichero application.properties o application.yml

...
debug=true
...
debug: true

Escribir logs a fichero

Por defecto Spring Boot escribe por consola pero opcionalmente le podemos decir que escriba a fichero.

Al igual que la activación del nivel a DEBUG la configuración para activar el fichero de log sigue el mismo principo de configuración de Spring Boot.

Los ficheros rotan por defecto cada 10Mb.

Las propiedades para indicar el fichero donde escribir los logs son:

  • logging.file
  • logging.path

Modificar los niveles de logs de nuestra aplicación o de librerías de terceros

Siguiendo los mismos principios de configuración de Spring Boot la modificación de los nieves de log de nuestra aplicación o de librerías de terceros es bastante fácil.

Por ejemplo,

En el arranque de la aplicación

java -jar my-application.jar --logging.level.org.myapp=DEBUG

En el fichero de configuración application.properties o application.yml

logging.level.org.myapp=DEBUG
...
logging:
   level:
     org:
       myapp: DEBUG

Activar otros sistemas de log

Se acivan por defecto al incluirlos en el classpath.

Podemos forzar a Spring Boot a utilzar un sistema de logging diferente al por defecto, configurando la propiedad de sistema:

org.springframework.boot.logging.LoggingSystem=Nombre completo de la clase que implementa el sistema de logging

o bien desactivar por completo el sistema de logging

org.springframework.boot.logging.LoggingSystem=none

Configuración de estos sistemas de logging

Una vez hemos cambiado el sistema de logging, Spring Boot se encargaría de leer lo ficheros de configuración de esos sistemas, aunque se recomienda que se utilicen la versión para Spring Boot.

Es decir, para los sistemas de logging como

  • log4j2 el fichero de configuración es log4j2.xml
  • Para Java Util Logging el fichero de configuración es logging.properties

pero Spring boot recomienda que utilicemos la versión para Spring Boot que serían respectivamente:

  • log4j2-spring.xml
  • logging-spring.properties

El mecanismo de heartbeat en kafka 0.9

Kafka

Es el mecanismo seguido por Apache Kafka 0.9 para determinar si un consumidor no se encuentra disponible, bien por un fallo en la aplicación, por un fallo de red o por un fallo de la máquina donde se está ejecutnado.

Recordemos que un grupo de consumidores comienzan a consumir mensajes de los topics en los que se ha subscrito. Cada topic suele estar formado por 1 log de varias particiones cada uno, y el broker que hace de coordinador del grupo, le asigna a cada consumidor una sola partición de un topic.

Por ejemplo si 3 consumidores se subscriben a un topic de 5 particiones, cada consumidor estará consumiendo mensaje de 1 sola partición. Nunca podremos tener más de un consumidor consumiendo más de 1 partición de un mismo topic.

En el ejemplo podríamos tener:

Sigue leyendo

Implementación de un consumidor de Kafka basado en la versión 0.9

Kafka

En Kafka 0.9 se ha rediseñado la implementación de los consumidores. Ahora se catacterizan por

  • poder implementar consumidores simples y más complejos
  • reimplementados en Java, evitando la dependencia con Scala y Zookeeper
  • mejora en la seguridad, soportanto SSL y Sasl/Kerberos
  • nuevos protocolos para mejorar la tolerancia a fallos de un grupo de consumidores. Esto facilita también la implementación de clientes en otros lenguajes de programación

Conceptos básicos de Kafka

Topic

Un topic se encuentra dividido en un conjunto de logs llamados particiones. Sobre estos topic se escriben y se consumen los mensajes.

Productor

Un productor publica o escribe mensajes al final de estos logs

Broker

Un broker es un nodo del cluster de kafka.

Consumidor

Un consumidor consume o lee los mensajes de estos logs según necesite, comentanzo normalmente desde el comienzo de ellos o desde el último procesado con éxito

Grupo de consumidores o consumer group

Un consumer group es un grupo de consumidores identificados por un identificador único y que se encuentran consumiendo mensajes de un topic. Usando un grupo de consumidores es el mecanismo de escalado de lectura de topics en Kafka, ya que cada consumidor se encuentra consumiendo mensajes de una de las particiones del topic.

Sigue leyendo

Cómo detectar si un fichero ha sido creado, modificado o borrado utilizando Java

Java

Desde hace tiempo estamos acostumbrados a que nuestros IDEs y otros gestores de archivos nos avisen cuando un fichero ha cambiado.

La razón del cambio puede ser desde la modificación o creación del fichero, hasta el borrado del mismo.

La forma más eficiente de conseguir esto es recibiendo los eventos de estas situaciones directamente del sistema nativo de notificación de eventos del SO.

Pues bien, desde Java podemos conseguir este comportamiento utilizando la interface WatchService.

Veamos en qué consiste

Para empezar partimos de un directorio sobre el que queremos ser notificados de eventos que se produzcan sobre él. Partimos entonces de un objeto que implementa la interface

  • java.nio.file.Watchable.

como es el caso de los objetos de tipo

  • java.nio.file.Path.

Y partimos de los tipos de eventos que queremos ser notificados, como son los

  • StandardWatchEventKinds.ENTRY_CREATE
  • StandardWatchEventKinds.ENTRY_DELETE
  • StandardWatchEventKinds.ENTRY_MODIFY
  • StandardWatchEventKinds.OVERFLOW

Entonces creamos el objeto Watchable y le registramos el servicio WatchService junto con los eventos en los que estamos interesados.

Este proceso de registro devuelve un objeto WatchKey, que es sobre el que recibiremos todos los eventos que se produzcan. Todos los eventos que se produzcan serán encolados en este objeto y los recibiremos utilizando el método poll() o take() del servicio WatchService.

  • poll()
  • take()

Ambos recuperan el WatchKey con los eventos producidos. La diferencia entre ambos métodos es que take() espera a que exista al menos un WatchKey con eventos, mientras que poll() si no hay ninguno devolvería null.

Una vez hemos procesado todos los eventos encolados en el WatchKey debemos resetearlo para que pueda seguir recibiendo y encolando más eventos.

En el caso de que un sistema de ficheros sea más rápido informando de los eventos que la implementación que los procesa, cuando procesemos dichos eventos a través del WatchKey recibiremos un evento del tipo OVERFLOW.

El servicio WatchService se puede utilizar de forma concurrente por varios consumidores. El único cuidado que tenemos que tener en estos casos es llamar al método reset() solo después de que sus eventos del WatchEvent hayan sido todos procesados.

Finalmente llamaremos al método close() del servicio WatchService para finalizar con la monitorización del directorio en el que estábamos interesados.

Código de ejemplo

Sigue leyendo

Importar un Certificado de una CA en el cacerts de la Jdk

Java

Es habitual encontrarnos en situaciones donde una empresa tiene su propia autoridad de certificación (Certificate authority o CA) con la que firma los certificados de sus servidores o bien nos encontramos con servicios REST o web services de sites cuyo certificado esta firmado por una CA que no la tenemos en nuestro almacén de CAs de confianza.

En estos casos para que nuestras aplicaciones puedan establecer una conexión segura a estos servicios, necesitamos incorporar el certificado de la CA en nuestro almacén de CAs de confianza.

De esta forma, siempre y cuando nos encontremos con un certificado firmado por cualquiera de esas nuevas CAs, confiaremos automáticamente en ellos.

Como sabemos una CA firma los certificados que emite con su clave privada y es mediante el certificado de la CA con lo que podemos verificar la firma. Esta firma va incluida en el certificado emitido. Por ese motivo incorporamos el certificado de la CA entre los que confiamos.

En la Jdk el almacen de CAs de confianza lo vamos a encontrar en:

$JAVA_HOME/jre/lib/security/cacerts

Para incorporar el Certificado de la CA en el cacerts necesitamos importarlo.

Importar un certificado de una CA en nuestro almacén de CAs de confianza o cacerts

$JAVA_HOME/bin/keytool -import -alias NuevoCA -file NuevoCertificadoCA.crt -keystore $JAVA_HOME/jre/lib/security/cacerts

La contraseña por defecto del cacerts es: changeit.

Este comando nos pedirá confirmación de que queremos importar este nuevo certificado en el almacén de CAs.

Importar un certificado de una CA en un almacén de CAs de confianza

keytool -import -trustcacerts -alias NuevoCA -file NuevoCertificadoCA.crt -keystore cacerts_app

-trustcacerts: Se utiliza cuando el -keystore no es el cacerts, y queremos que al importar el certificado, keytool tenga en cuenta los Certificados de las CAs que tenemos en el fichero cacerts

Herramienta gráfica para administrar un almacén de certificados

Si no queremos utiliza la línea de comando siempre podemos recurrir a alguna de las herramientas gráficas existente para manejar y administrar certificados.

Portecle es una de ellas. Muy simple y sencilla de utilizar.

Portecle

Excepción habitual que pretendemos evitar al incorpotar el Certificado de la CA

Cuando accedemos a un servicio por conexión segura y el certificado es autofirmado o no tenemos el certificado de la CA que lo firma en nuestro cacerts, obtendremos la siguiente excepción:

Exception in thread "main" javax.xml.ws.WebServiceException: Fallo al acceder al WSDL en: https://localhost/myservice. Ha fallado con: 
    sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.
    at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.tryWithMex(RuntimeWSDLParser.java:250)
    at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.parse(RuntimeWSDLParser.java:231)
    at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.parse(RuntimeWSDLParser.java:194)
    at com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser.parse(RuntimeWSDLParser.java:163)
    ...
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1937)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
    ...

Incorporando el Certificado de la CA evitaremos este tipo de excepciones.

Convertir coordenadas en grados, minutos y segundos a su valor en grados decimales en Java

Java

Existen varias formas de convertir las coordenadas representadas en grados, minutos y segundos a su representación en grados decimales.

El objetivo es pasar de

  • latitud: 40° 20.2′ 55.68
  • longitud: 3° 21′ 3.29

a

  • latitud: 40.352133333333335
  • longitud: 3.350913888888889

Ejemplo de clase Java

public class LatLongUtils {

    public static Double[] toDecimal(String latitude, String longitude) {
        try {
            String[] lat = latitude.replaceAll("[^0-9.\\s-]", "").split(" ");
            String[] lng = longitude.replaceAll("[^0-9.\\s-]", "").split(" ");
            Double dlat = toDecimal(lat); 
            Double dlng = toDecimal(lng);
            return new Double[]{dlat, dlng};
        } catch(Exception ex) {
            log.error(String.format("Error en el formato de las coordenadas: %s %s", new Object[]{latitude, longitude}), ex);
            return null;
        }
    }

    public static Double toDecimal(String latOrLng) {
        try {
            String[] latlng = latOrLng.replaceAll("[^0-9.\\s-]", "").split(" ");
            Double dlatlng = toDecimal(latlng); 
            return dlatlng;
        } catch(Exception ex) {
            log.error(String.format("Error en el formato de las coordenadas: %s ", new Object[]{latOrLng}), ex);
            return null;
        }
    }

    public static Double toDecimal(String[] coord) {
        double d = Double.parseDouble(coord[0]);
        double m = Double.parseDouble(coord[1]);
        double s = Double.parseDouble(coord[2]);
        double signo = 1;
        if (coord[0].startsWith("-"))
            signo = -1;
        return signo * (Math.abs(d) + (m / 60.0) + (s / 3600.0));
    }

    public static void main(String[] args) {
        Double[] coord = toDecimal("40° 20.2' 55.68\"", "3° 21' 3.29\"");
        System.out.println(coord[0] + " " + coord[1]);
    }

}

Eliminar acentos y diacríticos de un String en Java

Java

Todos nos hemos encontrado en la situación de tener que eliminar las tildes y diacrípticos de una cadena de texto o String en una aplicación Java. Los motivos por los que hacemos esto pueden ser muy diversos:

  • para realizar algún tipo de comparación entre cadenas de texto
  • para intentar normalizar de alguna manera la cadena de texto antes de guardarlos en una base de datos
  • etc

En cualquier caso el código que nos resuelve esto es muy sencillo

Código Java

package example.string;

import java.text.Normalizer;

public class Norm {

    public static void main(String[] args) throws Exception {
        String cadena = "áéíóú";
        System.out.println(cadena + " = " + cleanString(cadena));
        cadena = "àèìòù";
        System.out.println(cadena + " = " + cleanString(cadena)); 
        cadena = "äëïöü";
        System.out.println(cadena + " = " + cleanString(cadena));
        cadena = "âêîôû";
        System.out.println(cadena + " = " + cleanString(cadena));
        cadena = "2² + 2³";
        System.out.println(cadena + " = " + cleanString(cadena));
    }

    public static String cleanString(String texto) {
        texto = Normalizer.normalize(texto, Normalizer.Form.NFD);
        texto = texto.replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
        return texto;
    }
}

Resultado de la ejecución

áéíóú = aeiou
àèìòù = aeiou
äëïöü = aeiou
âêîôû = aeiou

Normalizer.normalize(texto, Normalizer.Form.NFD);

Según el API de la Jdk los métodos normalize de java.text.Normalizer transforman un texto Unicode en su representación compuesta o descompuesta, lo que nos permite poder buscar y ordenar textos de una forma más sencilla después de la transformación.

La transformación está recogida en un estándar de Unicode que lo podemos consultar en Unicode Standard Annex #15 Unicode Normalization Forms.

De este estándar nos interesa la transformación llamada Canonical decomposition.

Esta transformación transforma los caracteres con tildes y diacríticos separandolos en dos caracteres. Por ejemplo

á = a´
ä = a¨
â = a^

y con lo que el código

Normalizer.normalize(texto, Normalizer.Form.NFD);

convertimos los textos a su forma Canonical decomposition que utilizaremos en el siguiente paso para lograr nuestro objetivo.

\p{InCombiningDiacriticalMarks}

Lo utilizamos en expresiones regulares, y es independiente del lenguaje, por lo que lo podemos utilizar desde Java, Javascript, Php, Perl, Node.js y todo aquel lenguaje con una implementación básica de expresiones regulares.

Esta expresión representa los caracteres UTF-8 comprendidos entre

  • U+0300 hasta U+036F

entre los que se encuentran las tildes y todos los diacríticos en los que estamos interesados.

Entonces el código

texto = texto.replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");

lo que hace es eliminar todos esos caracteres del texto, consiguiendo finalmente el resultado esperado.

Tabla con los caracteres comprendidos entre U+0300 y U+036F

U+0300  ̀   ̀   ̀   ̀   ̀   COMBINING GRAVE ACCENT
U+0301  ́   ́   ́   ́   ́   COMBINING ACUTE ACCENT
U+0302  ̂   ̂   ̂   ̂   ̂   COMBINING CIRCUMFLEX ACCENT
U+0303  ̃   ̃   ̃   ̃   ̃   COMBINING TILDE
U+0304  ̄   ̄   ̄   ̄   ̄   COMBINING MACRON
U+0305  ̅   ̅   ̅   ̅   ̅   COMBINING OVERLINE
U+0306  ̆   ̆   ̆   ̆   ̆   COMBINING BREVE
U+0307  ̇   ̇   ̇   ̇   ̇   COMBINING DOT ABOVE
U+0308  ̈   ̈   ̈   ̈   ̈   COMBINING DIAERESIS
U+0309  ̉   ̉   ̉   ̉   ̉   COMBINING HOOK ABOVE
U+030A  ̊   ̊   ̊   ̊   ̊   COMBINING RING ABOVE
U+030B  ̋   ̋   ̋   ̋   ̋   COMBINING DOUBLE ACUTE ACCENT
U+030C  ̌   ̌   ̌   ̌   ̌   COMBINING CARON
U+030D  ̍   ̍   ̍   ̍   ̍   COMBINING VERTICAL LINE ABOVE
U+030E  ̎   ̎   ̎   ̎   ̎   COMBINING DOUBLE VERTICAL LINE ABOVE
U+030F  ̏   ̏   ̏   ̏   ̏   COMBINING DOUBLE GRAVE ACCENT
U+0310  ̐   ̐   ̐   ̐   ̐   COMBINING CANDRABINDU
U+0311  ̑   ̑   ̑   ̑   ̑   COMBINING INVERTED BREVE
U+0312  ̒   ̒   ̒   ̒   ̒   COMBINING TURNED COMMA ABOVE
U+0313  ̓   ̓   ̓   ̓   ̓   COMBINING COMMA ABOVE
U+0314  ̔   ̔   ̔   ̔   ̔   COMBINING REVERSED COMMA ABOVE
U+0315  ̕   ̕   ̕   ̕   ̕   COMBINING COMMA ABOVE RIGHT
U+0316  ̖   ̖   ̖   ̖   ̖   COMBINING GRAVE ACCENT BELOW
U+0317  ̗   ̗   ̗   ̗   ̗   COMBINING ACUTE ACCENT BELOW
U+0318  ̘   ̘   ̘   ̘   ̘   COMBINING LEFT TACK BELOW
U+0319  ̙   ̙   ̙   ̙   ̙   COMBINING RIGHT TACK BELOW
U+031A  ̚   ̚   ̚   ̚   ̚   COMBINING LEFT ANGLE ABOVE
U+031B  ̛   ̛   ̛   ̛   ̛   COMBINING HORN
U+031C  ̜   ̜   ̜   ̜   ̜   COMBINING LEFT HALF RING BELOW
U+031D  ̝   ̝   ̝   ̝   ̝   COMBINING UP TACK BELOW
U+031E  ̞   ̞   ̞   ̞   ̞   COMBINING DOWN TACK BELOW
U+031F  ̟   ̟   ̟   ̟   ̟   COMBINING PLUS SIGN BELOW
U+0320  ̠   ̠   ̠   ̠   ̠   COMBINING MINUS SIGN BELOW
U+0321  ̡   ̡   ̡   ̡   ̡   COMBINING PALATALIZED HOOK BELOW
U+0322  ̢   ̢   ̢   ̢   ̢   COMBINING RETROFLEX HOOK BELOW
U+0323  ̣   ̣   ̣   ̣   ̣   COMBINING DOT BELOW
U+0324  ̤   ̤   ̤   ̤   ̤   COMBINING DIAERESIS BELOW
U+0325  ̥   ̥   ̥   ̥   ̥   COMBINING RING BELOW
U+0326  ̦   ̦   ̦   ̦   ̦   COMBINING COMMA BELOW
U+0327  ̧   ̧   ̧   ̧   ̧   COMBINING CEDILLA
U+0328  ̨   ̨   ̨   ̨   ̨   COMBINING OGONEK
U+0329  ̩   ̩   ̩   ̩   ̩   COMBINING VERTICAL LINE BELOW
U+032A  ̪   ̪   ̪   ̪   ̪   COMBINING BRIDGE BELOW
U+032B  ̫   ̫   ̫   ̫   ̫   COMBINING INVERTED DOUBLE ARCH BELOW
U+032C  ̬   ̬   ̬   ̬   ̬   COMBINING CARON BELOW
U+032D  ̭   ̭   ̭   ̭   ̭   COMBINING CIRCUMFLEX ACCENT BELOW
U+032E  ̮   ̮   ̮   ̮   ̮   COMBINING BREVE BELOW
U+032F  ̯   ̯   ̯   ̯   ̯   COMBINING INVERTED BREVE BELOW
U+0330  ̰   ̰   ̰   ̰   ̰   COMBINING TILDE BELOW
U+0331  ̱   ̱   ̱   ̱   ̱   COMBINING MACRON BELOW
U+0332  ̲   ̲   ̲   ̲   ̲   COMBINING LOW LINE
U+0333  ̳   ̳   ̳   ̳   ̳   COMBINING DOUBLE LOW LINE
U+0334  ̴   ̴   ̴   ̴   ̴   COMBINING TILDE OVERLAY
U+0335  ̵   ̵   ̵   ̵   ̵   COMBINING SHORT STROKE OVERLAY
U+0336  ̶   ̶   ̶   ̶   ̶   COMBINING LONG STROKE OVERLAY
U+0337  ̷   ̷   ̷   ̷   ̷   COMBINING SHORT SOLIDUS OVERLAY
U+0338  ̸   ̸   ̸   ̸   ̸   COMBINING LONG SOLIDUS OVERLAY
U+0339  ̹   ̹   ̹   ̹   ̹   COMBINING RIGHT HALF RING BELOW
U+033A  ̺   ̺   ̺   ̺   ̺   COMBINING INVERTED BRIDGE BELOW
U+033B  ̻   ̻   ̻   ̻   ̻   COMBINING SQUARE BELOW
U+033C  ̼   ̼   ̼   ̼   ̼   COMBINING SEAGULL BELOW
U+033D  ̽   ̽   ̽   ̽   ̽   COMBINING X ABOVE
U+033E  ̾   ̾   ̾   ̾   ̾   COMBINING VERTICAL TILDE
U+033F  ̿   ̿   ̿   ̿   ̿   COMBINING DOUBLE OVERLINE
U+0340  ̀   ̀   ̀   ̀   ̀   COMBINING GRAVE TONE MARK
U+0341  ́   ́   ́   ́   ́   COMBINING ACUTE TONE MARK
U+0342  ͂   ͂   ͂   ͂   ͂   COMBINING GREEK PERISPOMENI
U+0343  ̓   ̓   ̓   ̓   ̓   COMBINING GREEK KORONIS
U+0344  ̈́   ̈́   ̈́  ̈́  ̈́  COMBINING GREEK DIALYTIKA TONOS
U+0345  ͅ   ͅ   ͅ   ͅ   ͅ   COMBINING GREEK YPOGEGRAMMENI
U+0346  ͆   ͆   ͆   ͆   ͆   COMBINING BRIDGE ABOVE
U+0347  ͇   ͇   ͇   ͇   ͇   COMBINING EQUALS SIGN BELOW
U+0348  ͈   ͈   ͈   ͈   ͈   COMBINING DOUBLE VERTICAL LINE BELOW
U+0349  ͉   ͉   ͉   ͉   ͉   COMBINING LEFT ANGLE BELOW
U+034A  ͊   ͊   ͊   ͊   ͊   COMBINING NOT TILDE ABOVE
U+034B  ͋   ͋   ͋   ͋   ͋   COMBINING HOMOTHETIC ABOVE
U+034C  ͌   ͌   ͌   ͌   ͌   COMBINING ALMOST EQUAL TO ABOVE
U+034D  ͍   ͍   ͍   ͍   ͍   COMBINING LEFT RIGHT ARROW BELOW
U+034E  ͎   ͎   ͎   ͎   ͎   COMBINING UPWARDS ARROW BELOW
U+034F  ͏   ͏   ͏   ͏   ͏   COMBINING GRAPHEME JOINER
U+0350  ͐   ͐   ͐   ͐   ͐   COMBINING RIGHT ARROWHEAD ABOVE
U+0351  ͑   ͑   ͑   ͑   ͑   COMBINING LEFT HALF RING ABOVE
U+0352  ͒   ͒   ͒   ͒   ͒   COMBINING FERMATA
U+0353  ͓   ͓   ͓   ͓   ͓   COMBINING X BELOW
U+0354  ͔   ͔   ͔   ͔   ͔   COMBINING LEFT ARROWHEAD BELOW
U+0355  ͕   ͕   ͕   ͕   ͕   COMBINING RIGHT ARROWHEAD BELOW
U+0356  ͖   ͖   ͖   ͖   ͖   COMBINING RIGHT ARROWHEAD AND UP ARROWHEAD BELOW
U+0357  ͗   ͗   ͗   ͗   ͗   COMBINING RIGHT HALF RING ABOVE
U+0358  ͘   ͘   ͘   ͘   ͘   COMBINING DOT ABOVE RIGHT
U+0359  ͙   ͙   ͙   ͙   ͙   COMBINING ASTERISK BELOW
U+035A  ͚   ͚   ͚   ͚   ͚   COMBINING DOUBLE RING BELOW
U+035B  ͛   ͛   ͛   ͛   ͛   COMBINING ZIGZAG ABOVE
U+035C  ͜   ͜   ͜   ͜   ͜   COMBINING DOUBLE BREVE BELOW
U+035D  ͝   ͝   ͝   ͝   ͝   COMBINING DOUBLE BREVE
U+035E  ͞   ͞   ͞   ͞   ͞   COMBINING DOUBLE MACRON
U+035F  ͟   ͟   ͟   ͟   ͟   COMBINING DOUBLE MACRON BELOW
U+0360  ͠   ͠   ͠   ͠   ͠   COMBINING DOUBLE TILDE
U+0361  ͡   ͡   ͡   ͡   ͡   COMBINING DOUBLE INVERTED BREVE
U+0362  ͢   ͢   ͢   ͢   ͢   COMBINING DOUBLE RIGHTWARDS ARROW BELOW
U+0363  ͣ   ͣ   ͣ   ͣ   ͣ   COMBINING LATIN SMALL LETTER A
U+0364  ͤ   ͤ   ͤ   ͤ   ͤ   COMBINING LATIN SMALL LETTER E
U+0365  ͥ   ͥ   ͥ   ͥ   ͥ   COMBINING LATIN SMALL LETTER I
U+0366  ͦ   ͦ   ͦ   ͦ   ͦ   COMBINING LATIN SMALL LETTER O
U+0367  ͧ   ͧ   ͧ   ͧ   ͧ   COMBINING LATIN SMALL LETTER U
U+0368  ͨ   ͨ   ͨ   ͨ   ͨ   COMBINING LATIN SMALL LETTER C
U+0369  ͩ   ͩ   ͩ   ͩ   ͩ   COMBINING LATIN SMALL LETTER D
U+036A  ͪ   ͪ   ͪ   ͪ   ͪ   COMBINING LATIN SMALL LETTER H
U+036B  ͫ   ͫ   ͫ   ͫ   ͫ   COMBINING LATIN SMALL LETTER M
U+036C  ͬ   ͬ   ͬ   ͬ   ͬ   COMBINING LATIN SMALL LETTER R
U+036D  ͭ   ͭ   ͭ   ͭ   ͭ   COMBINING LATIN SMALL LETTER T
U+036E  ͮ   ͮ   ͮ   ͮ   ͮ   COMBINING LATIN SMALL LETTER V
U+036F  ͯ   ͯ   ͯ   ͯ   ͯ   COMBINING LATIN SMALL LETTER X 

Java 8, implementación y utilización de java.util.stream.Collector

Java

Una de las nuevas interfaces que ha traído Java 8 es la interface

java.util.stream.Collector<T,A,R>

Muchos nos preguntaremos para qué sirve, y como podemos utilizarla, y para ello no hay nada mejor que entender y comprender cómo funciona.

La interface java.util.stream.Collector

Un Collector los vamos a utilizar en un Stream de elementos T para construir una nueva Colección (List, Set, etc) R, para reducir los elementos del Stream en una nueva colección R más pequeña o para obtener un único resultado R.

En esta interface tenemos que entender qué es cada elemento que lo forma.

  • T, representa cada uno de los elementos del Stream que va siendo consumido o entrando en el Collector.

  • A, representa una estructura de datos dentro del Collector donde iremos almacenando los elementos T del Stream. Este tipo de dato puede ser del mismo tipo que R, pero puede ser cualquier otro tipo de estructrura o clase.

  • R, representa la estructura de datos final con el resultado del Collector.

Y para lograr este resultado, la clase que implementa esta interface debe implementar los siguientes métodos abstractos.

  • supplier(), devuelve una función que crea el acumulador A para almacenar los elementos T del Stream.

  • accumulator(), devuelve una función que coge el acumulador A y un elemento T y lo añade en el acumulador A.

  • combiner(), devuelve una función que combina dos acumuladores A en uno solo. Se utiliza cuando el Stream es ejecutado en paralelo.

  • finisher(), devuelve una función que transforma el acumulador A en el resultado R esperado.

  • characteristics(), informa de las características que nuestra implementación Collector va a tener. Estas pueden ser cualquiera de las definidas en java.util.stream.Collector.Characteristics, que son CONCURRENT, IDENTITY_FINISH, UNORDERED.

Ejemplo

Y como siempre para comprender mejor estos conceptos vamos a crear un ejemplo. Sabemos que la misma funcionalidad implementada en este ejemplo puede ser implementada sin hacer uso de un Collector.

El ejemplo consiste en, partiendo de una lista de Personas (clase User) obtener una lista clasificada de departamentos (clase Departament), donde veamos el número de empleados de cada uno de ellos y quien es su CEO.

Clase User

package sample.collectors;

public class User {
    private String name;
    private String departament;
    private Boolean ceo;
    public String getName() {
        return name;
    }
    public User setName(String name) {
        this.name = name;
        return this;
    }
    public String getDepartament() {
        return departament;
    }
    public User setDepartament(String departament) {
        this.departament = departament;
        return this;
    }
    public Boolean getCeo() {
        return ceo;
    }
    public User setCeo(Boolean ceo) {
        this.ceo = ceo;
        return this;
    }   
}

Clase Departament

package sample.collectors;

public class Departament {
    private String name;
    private User chiefExecutiveOfficer;
    private Integer employeeNumber;

    public User getChiefExecutiveOfficer() {
        return chiefExecutiveOfficer;
    }
    public Departament setChiefExecutiveOfficer(User chiefExecutiveOfficer) {
        this.chiefExecutiveOfficer = chiefExecutiveOfficer;
        return this;
    }
    public Integer getEmployeeNumber() {
        return employeeNumber;
    }
    public Departament setEmployeeNumber(Integer employeeNumber) {
        this.employeeNumber = employeeNumber;
        return this;
    }
    public String getName() {
        return name;
    }
    public Departament setName(String name) {
        this.name = name;
        return this;
    }   
}

Implementación de la interface Collector

package sample.collectors;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class DepartamentCollector implements Collector<User, Map<String, List<User>>, List<Departament>> {

    @Override
    public BiConsumer<Map<String, List<User>>, User> accumulator() {
        return (acc, user) -> {
            if (acc.get(user.getDepartament()) == null ) {
                acc.put(user.getDepartament(), new ArrayList<User>());
            }
            acc.get(user.getDepartament()).add(user);
        };
    }

    @Override
    public Set<java.util.stream.Collector.Characteristics> characteristics() {
        return EnumSet.of(Characteristics.UNORDERED);
    }

    @Override
    public BinaryOperator<Map<String, List<User>>> combiner() {
        //
        // Implementar este metodo para soportar Stream en paralelo. La implemetacion
        // devuelve una interface funcional responsable de combinar dos
        // estructuras de tipo Map<String, List<User>> en una sola
        //
        return null;
    }

    @Override
    public Function<Map<String, List<User>>, List<Departament>> finisher() {
        return (acc) -> {        
            List<Departament> departaments = new ArrayList<Departament>();
            Iterator<String> depts = acc.keySet().iterator();
            while(depts.hasNext()) {
                String dptoName = depts.next();
                Departament dpto = new Departament();
                dpto.setName(dptoName);
                Optional<User> uu = acc.get(dptoName).stream().filter((u)->u.getCeo()).findFirst();
                if (uu.isPresent()) {
                    dpto.setChiefExecutiveOfficer(uu.get());
                }
                dpto.setEmployeeNumber(acc.get(dptoName).size());
                departaments.add(dpto);
            }
            return departaments;
        };
    }

    @Override
    public Supplier<Map<String, List<User>>> supplier() {
        return () -> new HashMap<String, List<User>>();
    }   
}

Clase principal

package sample.collectors;

import java.util.ArrayList;
import java.util.List;

public class FilterUsers {

    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User().setName("Jordan").setDepartament("recursos humanos").setCeo(true));
        users.add(new User().setName("Larry").setDepartament("recursos humanos").setCeo(false));
        users.add(new User().setName("Jose").setDepartament("recursos humanos").setCeo(false));
        users.add(new User().setName("Cooper").setDepartament("ventas").setCeo(true));
        users.add(new User().setName("Eduardo").setDepartament("ventas").setCeo(false));        
        users.add(new User().setName("Antonio").setDepartament("ventas").setCeo(false));
        users.add(new User().setName("Anthony").setDepartament("ventas").setCeo(false));
        users.add(new User().setName("Michael").setDepartament("contabilidad").setCeo(true));
        users.add(new User().setName("Anderson").setDepartament("contabilidad").setCeo(false));
        users.add(new User().setName("Alan").setDepartament("contabilidad").setCeo(false));

        List<Departament> dptos = users.stream().collect(new DepartamentCollector());
        dptos.stream().forEach((dd) -> System.out.println(dd.getName() + " (" + dd.getEmployeeNumber()+") ceo: " + dd.getChiefExecutiveOfficer().getName() ));

    }

}

Y eso es todo. Podemos crear implementaciones de Collector más simples que esta o más complejas. Todo depende de la necesidad o de lo cómodo que nos sintamos con la utilización de esta interface.

java.lang.ClassNotFoundException

Java

Esta excepción esta muy relacionada con el artículo anterior java.lang.NoClassDefFoundError, Caused by: java.lang.ClassNotFoundException.

A menudo nos encontraremos ambas excepciones, pero en ocasiones solo nos encontraremos con java.lang.ClassNotFoundException.

El motivo de esta excepción es prácticamente el mismo. En la documentación nos dice que esta excepción se produce cuando el ClassLoader intenta leer la descripción de una clase, y la definición de dicha clase no es encontrada.

Estamos ante el mismo motivo visto en el artículo anterior. La definición de la clase no se encuentra debido a que la librería que lo contiene no esta en el classpath de la aplicación, o bien, si estamos ante un servidor de aplicaciones, la librería no está o el classloader que intenta cargarla no tiene visible esa librería. En cualquier caso tenemos que repasar la configuración de nuestras aplicaciones para solucionar este tipo de errores.

En los servidores de aplicaciones es más común encontrarnos con estas excepciones, ya que existen varios ClassLoader diferentes para una aplicación, y distinta políticas de carga de clases. Tenemos un ClassLoader para el servidor, uno para librerías comunes a todas las aplicaciones, y un ClassLoader para cada aplicación. La jerarquía normalmente es esta aunque depende del servidor de aplicaciones. Así por ejemplo si se delega la carga de clases al classloader del servidor, y la librería con definición de la clase se encuentra en la aplicación (es decir en el WEB-INF/lib) al final obtendremos esta excepción o la que comentábamos en el artículo anterior.

Vamos a reproducir esta excepción y así veremos como es el código que provoca estos errores.

Creación de la estructura del proyecto

mkdir a
mkdir b
mkdir main

Creación de la clase A.java

La creamos en el directorio o paquete a.

package a;
public class A {
    public void echo() {
        System.out.println("Hello from A !");
    }
}

Creación de la clase B.java

La creamos en el directorio o paquete b.

package b;
public class B {
    public void echo() {
        System.out.println("Hello from B !");
    }
}

Creación de la clase MainClass.java

Esta clase hace uso de las dos anteriores, y la creamos en el directorio o paquete main.

package main;

import b.*;
import a.*;
public class MainClass {
    public static final void main(String argv[]) {
        A a = new A();
        a.echo();
        try {
            Class bclass = Class.forName("b.B");
            B b = (B) bclass.newInstance();
            b.echo();
        } catch(Throwable ex) {
            ex.printStackTrace();
        }
    }
}

Compilamos los fuentes

Nos situamos en el directorio raiz y compilamos

$ javac a*.java b*.java main*.java

Ejecutamos la clase principal

$ java -cp . main.MainClass
Hello from A !
Hello from B !

Provocamos la excepción java.lang.ClassNotFoundException

La forma mas sencilla es renombrar el directorio b por otro nombre, y así la clase B ya no es encontrara por la Jvm, ya que deja de existir el package b.

Ejecutamos de nuevo la aplicación y obtendremos la excepción esperada.

$ java -cp . main.MainClass
Hello from A !
java.lang.ClassNotFoundException: b.B
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown S
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Unknown Source)
        at main.MainClass.main(MainClass.java:10)

En esta ocasión el error no se produce en la creación de la clase (new B()), si no cuando intentamos leer su definición

Class bclass = Class.forName("b.B");

java.lang.NoClassDefFoundError, Caused by: java.lang.ClassNotFoundException

Java

Según la documentación la excepción java.lang.NoClassDefFoundError se produce cuando la Jvm o el ClassLoader intenta cargar la definición de una clase y no es capaz de encontrarla.

Es una excepción que se produce en tiempo de ejecución o Runtime, y no de compilación.

Nuestro código es capaz de compilar perfectamente pero en tiempo de ejecución no es capaz de encontrar la definición de la clase.

Esta excepción es un claro ejemplo de un problema en el classpath. Por lo tanto lo primero que tenemos que hacer es verificar que tenemos todas las librerías en el clasapath de nuestra aplicación y de que nadie lo ha modificado.

Es muy frecuente encontrarnos con estos problemas en un servidor de aplicaciones. Se producen cuando cambiamos la configuración de los classloader del servidor o bien hemos eliminado la referencia a la librería donde se encuentra la clase a la que hacemos referencia.

La solución pasa por repasar la configuración del classpath y de los classloader de nuestro servidor de aplicaciones o de nuestra aplicación.

Vamos a provocar esta misma excepción en una aplicación muy simple para que entendamos que efectivamente el problema se debe a que no se ha encontrado la definición de la clase.

Creación de la estructura del proyecto

mkdir a
mkdir b
mkdir main

Creación de la clase A.java

La creamos en el directorio o paquete a.

package a;
public class A {
        public void echo() {
                System.out.println("Hello from A !");
        }
}

Creación de la clase B.java

La creamos en el directorio o paquete b.

package b;
public class B {
        public void echo() {
                System.out.println("Hello from B !");
        }
}

Creación de la clase MainClass.java

Esta clase hace uso de las dos anteriores, y la creamos en el directorio o paquete main.

package main;

import b.*;
import a.*;
public class MainClass {
        public static final void main(String argv[]) {
                A a = new A();
                a.echo();
                try {
                        B b = new B();
                        b.echo();
                } catch(Throwable ex) {
                        ex.printStackTrace();
                }                        
        }
}

Compilamos los fuentes

Nos situamos en el directorio raiz y compilamos

$ javac a*.java b*.java main*.java

Ejecutamos la clase principal

$ java -cp . main.MainClass
Hello from A !
Hello from B !

Provocamos la excepción java.lang.NoClassDefFoundError

Como hemos visto, la excepción se produce cuando la Jvm o ClassLoader no es capaz de encontrar la definición de la clase. Por lo tanto para provocar esta excepción modificamos el classpath de la aplicación.

La forma mas sencilla es renombrar el directorio b por otro nombre, y así la clase B ya no es encontrada por la Jvm, ya que deja de existir el package b.

Ejecutamos de nuevo la aplicación y obtendremos la excepción esperada.

$ java -cp . main.MainClass
Hello from A !
java.lang.NoClassDefFoundError: b/B
        at main.MainClass.main(MainClass.java:10)
Caused by: java.lang.ClassNotFoundException: b.B
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 1 more

Para que la Jvm pueda crear la instancia de la clase B necesita tener la definición de la clase. Para ello busca en el classpath la clase B y al no encontrarla lanza la excepción ClassNotFoundException. Esto provoca que la definición de la clase no pueda ser recuperada y la Jvm nos avisa con la excepción NoClassDefFoundError.

Ambas excepciones están relacionadas y normalmente las veremos siempre juntas.

Solución

Como hemos visto la solución pasa por repasar el classpath de nuestra aplicación y añadir la librería que nos falta. Si estamos en un servidor de aplicaciones tendremos igualmente que verificar si tenemos incluida la librería en el classpath y si aun así la tenemos incluida, debemos revisar la configuración de los classloaders ya que el que esta intentando leer la definición de la clase no tiene visible la lectura de esa librería.