Skip to content

Spring Cloud Netflix (3)

Continuando con el ejemplo práctico de uso de Spring Cloud Netflix, el siguiente paso es montar los proyectos que arranquen el servidor de configuración y el de descubrimiento de servicios.

cloud-greeter-config

Este proyecto es un servidor de configuración basado en Spring Cloud Config. Suministra la configuración al resto de servicios de una forma centralizada.

Cuando un servicio arranca se conecta con este servidor y le pide sus propiedades de configuración. Las propiedades se pueden configurar para obtenerse desde distintas fuentes, como por ejemplo desde un servidor git, lo que permite tener un control de versiones sobre los ficheros de configuración de igual forma que se tiene habitualmente sobre el código fuente.

El servidor se implementa como una aplicación de Spring Boot. Y de hecho esto es una constante para todos los módulos. Cada módulo es una aplicación de Spring Boot. Lo que quiere decir que cada uno de ellos es una aplicación independiente que contiene embebido su propio servidor de aplicaciones. Una vez compilados se pueden arrancar y parar de forma individual, de forma que al final se tendrán tantos servidores de aplicaciones levantados como servidores y servicios se tengan.

El fichero pom.xml del proyecto se limita a heredar del proyecto padre y añadir las dependencias de Spring Boot y Spring Cloud Config:

La dependencia spring-boot-starter-undertow hace que se utilice Undertow como servidor de aplicaciones, un servidor embebible que rinde bien con pocos recursos, algo necesario para este proyecto que levanta muchos servicios de forma local. Aunque se podría utilizar cualquier otro servidor, como Jetty o Apache por ejemplo.

La dependencia spring-boot-starter-actuator es una utilidad de Spring Boot que expone una serie de servicios REST para la aplicación de forma automática. Estos servicios implementan tareas habituales, evitando que los desarrolladores tengan que implementarlos por su cuenta, como por ejemplo un servicio que atiende la ruta “/health” que se puede utilizar para comprobar si el servidor está levantado y retorna un JSON con información acerca del sistema.

La única clase del proyecto es el punto de entrada a la aplicación:

El servidor se arranca como una aplicación de Spring Boot ordinaria gracias a la anotación @SpringBootApplication. Y el servidor de configuración se activa con la anotación @EnableConfigServer. Así de simple, no hay más. No es de extrañar que Spring Boot se haya convertido en un referente para la construcción de microservicios para las empresas con equipos de desarrolladores Java que necesitan obtener resultados de una forma rápida.

El único fichero de configuración del proyecto es application.yml, que es el propio fichero de configuración cargado por defecto por Spring Boot:

En el fichero se configura el puerto en que se debe levantar el servidor de aplicaciones, el nombre de la aplicación, la ubicación de los ficheros de logs, y el repositorio con los ficheros de configuración de los servicios. La ubicación del repositorio se realiza mediante una variable de entorno, de forma que pueda cambiarse fácilmente. Otra forma más habitual de hacerlo es utilizar varios profiles y definir el nombre del profile a utilizar en una variable de entorno.

El servidor se puede arrancar como cualquier otra aplicación de Spring Boot. Por ejemplo desde línea de comandos, en el directorio donde se encuentre el módulo, utilizando el plugin de Spring Boot para Maven:

El servidor arrancará en el puerto 8100 y estará disponible a través de la ruta http://localhost:8100, pudiéndose comprobar su estado a través de la ruta expuesta por Spring Boot Actuator:

  • http://localhost:8100/health

cloud-greeter-eureka

Este proyecto es un servidor de descubrimiento de servicios basado en Eureka. Mantiene el registro centralizado de los servicios disponibles y garantiza una alta disponibilidad de los mismos, o al menos una alta posibilidad de encontrar una instancia de los mismos en funcionamiento a pesar de las incidencias que puedan producirse, como cortes de red o caídas de servidores.

La estructura del proyecto es idéntica que la del anterior. En el fichero pom.xml únicamente cambian las dependencias principales:

La dependencia spring-cloud-config-client es para utilizar el cliente de Spring Cloud Config. Es decir, que el servidor Eureka también recoge su configuración del servidor de configuración. De hecho, la idea es que todas las aplicaciones que se levanten, tanto los servicios que implementen lógica de negocio, como los servidores que les dan soporte, utilicen el servidor de configuración.

La única clase del proyecto es el punto de entrada a la aplicación:

El servidor se arranca como una aplicación de Spring Boot ordinaria gracias a la anotación @SpringBootApplication. Y el servidor Eureka se activa con la anotación @EnableEurekaServer.

El único fichero de configuración del proyecto es boostrap.yml, que es el propio fichero de configuración cargado por defecto por Spring Boot cuando una aplicación se configura para obtener sus propiedades desde un servidor de configuración:

En el fichero se configura el nombre de la aplicación y la URL donde se encuentra el servidor de configuración.

El servidor Eureka obtiene su fichero de configuración del servidor de configuración. Fichero que se llama cloud-greeter-eureka.yml y se encuentra en el repositorio del proyecto cloud-greeter-config junto con el resto de ficheros de configuración. El servidor de configuración simplemente casa el nombre de la aplicación con el nombre del fichero. Si existe un fichero con el nombre de la aplicación supone que es el fichero de configuración de dicha aplicación y se lo sirve.

En el fichero se configura el puerto en que se debe levantar el servidor de aplicaciones, la ubicación de los ficheros de logs, y los parámetros propios del servidor Eureka. Notar que uno de los parámetros sirve para evitar que el propio servidor se registre contra si mismo.

El servidor se puede arrancar como cualquier otra aplicación de Spring Boot. Por ejemplo desde línea de comandos, en el directorio donde se encuentre el módulo, utilizando el plugin de Spring Boot para Maven:

El servidor arrancará en el puerto 8101 y estará disponible a través de la ruta http://localhost:8101. Al acceder a la URL se abre un panel de control con los servicios registrados. A medida que se levanten nuevos servicios estos irán apareciendo en el listado.

En buena lógica, los servidores pueden configurarse en cluster para asegurar su disponibilidad y que no representen un punto único de fallo.

Spring Cloud Netflix (2)

Hace ya un tiempo desarrollé un ejemplo mostrando un caso de uso sencillo de Spring Cloud Netflix. El proyecto acabó sirviendo de referencia e introducción a esta tecnología para los equipos de desarrollo de una empresa para la que trabajaba y el código fuente publicado en GitHub en dos repositorios, uno con los fuentes de los servicios y otro con los ficheros de configuración:

El ejemplo se compone de una estructura muy sencilla, sin distracciones, con la intención de fomentar las buenas prácticas en el uso de Maven para la configuración de proyectos con múltiples módulos, de forma que las dependencias comunes están declaradas en un proyecto padre del que todos heredan y cada proyecto sólo incluye las dependencias propias.

El objetivo del proyecto es mostrar las piezas mínimas necesarias para realizar una instalación con Spring Cloud Netflix, desarrollando tanto los servicios propios, que ejecutan la lógica de negocio, como los servicios de la plataforma, necesarios para que los servicios propios se puedan configurar de forma remota, se puedan invocar directamente entre ellos, o de forma externa a través de una ruta pública, se puedan ejecutar de forma balanceada, y se puedan monitorizar.

El directorio raíz del proyecto contiene el fichero pom.xml que incluye todos los módulos en los que se encuentra estructurado:

  • cloud-greeter-parent: Proyecto padre del que todos heredan. Contiene la configuración común y declara las dependencias compartidas por todos.
  • cloud-greeter-config: Proyecto correspondiente al servicio de configuración. Arranca una instancia del servidor Spring Cloud Config que todos los servicios utilizan para obtener su configuración de forma remota.
  • cloud-greeter-eureka: Proyecto que arranca el servidor Eureka utilizado para el registro y descubrimiento de servicios.
  • cloud-greeter-service: Este proyecto contiene un servicio de ejemplo. Expone un servicio REST que recibe un JSON con un nombre y retorna un JSON con una cadena de texto que contiene el nombre recibido dentro del típico “Hello <<nombre>>!”.
  • cloud-greeter-api: Este proyecto contiene un servicio que invoca al servicio del proyecto anterior. La idea es de que este servicio es expuesto de forma pública mientras el anterior sólo se puede invocar de forma interna.
  • cloud-greeter-zuul: Proyecto que arranca el servidor Zuul que actúa como enrutador. Recibe las peticiones de llamadas a los servicios y las enruta hacia los servicios levantados aplicando balanceo de carga.
  • cloud-greeter-turbine: Proyecto que arranca el servidor Turbine que expone una aplicación web a través de la que se puede monitorizar el estado de los servicios.

cloud-greeter-parent

Como ya se ha comentado, este proyecto contiene las partes comunes, un fichero pom.xml con la configuración del proyecto y las dependencias compartidas:

Las únicas dependencias declaradas son Spring Boot, Spring Cloud y Springfox. Spring Boot facilita la creación de microservicios, Spring Cloud la creación de servicios en la nube, y Springfox la documentación de los servicios REST.

Springfox quizás sea menos conocido, pero es simplemente una capa que se coloca por encima de Swagger y tiene el añadido de que es capaz de recuperar información de las anotaciones de librerías como Spring o Jackson, en vez de forzar a los desarrolladores a utilizar las anotaciones de Swagger, evitando así la dependencia y la duplicación de esfuerzos.

Spring Cloud Netflix (1)

Todas las empresas desarrollan una o varias actividades que representan su fuente principal de ingresos. Y esos ingresos se dividen en partidas de dinero que deben dedicarse a gastos e inversiones. Para las empresas de gran tamaño una de esas partidas habitualmente se destina al mantenimiento de sus sistemas de información. Sistemas que tradicionalmente han gestionado en una infraestructura propia. Sus propias máquinas, sus propias aplicaciones, sus propios equipos, sus propios problemas.

Para algunas empresas la información es parte fundamental de su actividad. Mantener esa información en un entorno propio les resulta algo natural. No obstante, se plantea la cuestión de si realmente necesitan hacerse responsable de gestionar absolutamente todos sus sistemas. No sólo los de misión crítica, sino los de uso más común. El esfuerzo humano, material y económico necesario para mantener en funcionamiento toda una infraestructura propia puede llegar a ser bastante elevado.

Desde hace más de dos décadas, gigantes como Amazon, Google y Microsoft comenzaron a ofrecer sus infraestructuras para que pudieran ser explotadas por otras empresas. Una oferta que se ha materializado en distintos servicios. Servicios de almacenamiento, servicios de computación, servicios de aplicaciones, … La idea es que las empresas no tengan que invertir en sistemas virtuales, distribuidos, replicados, escalables, tolerantes a fallos y con alta disponibilidad. Conceptos técnicos todos ellos que no se encuentran habitualmente dentro del dominio de su negocio.

Aunque el paradigma ha ido evolucionando con los años, por lo general se acepta que si un sistema o información no se encuentra alojado dentro de una infraestructura propia se dice que está en la nube. Si se almacena un archivo, como una fotografía, en un servidor ajeno se dice que está en la nube. Si se crea un fichero, como un documento, a través de una aplicación alojada en un servidor ajeno se dice que se usa un servicio en la nube. Si se ejecuta una aplicación propia en un servidor ajeno se dice que usa la infraestructura de la nube. Y en general, simplificando bastante, si se realiza cualquier acción que implica el acceso a un servidor ajeno a través de una red se considera que se está trabajando en la nube. Entendiendo en todo caso que dicho acceso se realiza normalmente a través de Internet, e independientemente de que algunas empresas mantengan grandes infraestructuras propias que denominan nubes privadas.

Una de las razones que motivó la aparición de la nube fue la necesidad de disponer de una gran capacidad de cómputo por parte de las empresas que empezaban a ofertar servicios para millones de potenciales clientes conectados a un mismo tiempo. Las empresas necesitaban arquitecturas con una alta escalabilidad horizontal bajo demanda utilizando las capacidades de una infraestructura externa ofrecida por algún proveedor. Las soluciones que surgieron de esa necesidad originó la creación de arquitecturas que se han popularizado hasta convertirse en el marco de referencia para el desarrollo de aplicaciones hoy en día. Evolucionando desde aplicaciones monolíticas basadas en base de datos relaciones hasta arquitecturas de microservicios y base de datos NoSQL.

Una de las compañías que tuvo que acometer la tarea de desarrollar una arquitectura en la nube fue Netflix, el popular servicio de streaming de series, películas y documentales. El código fuente de los servicios básicos que componen su plataforma desarrollada en Java se encuentra disponible de forma abierta bajo licencia Apache 2.0. Sus soluciones se desarrollaron en una época temprana y con unas necesidades muy concretas para la infraestructura de Amazon sobre la que se ejecuta, por lo que no reflejan el estado actual del desarrollo de microservicios ejecutados sobre contenedores. No obstante, la plataforma abierta de Netflix supone un punto de entrada muy interesante para los desarrolladores que quieran conocer el principio de funcionamiento de este tipo de arquitecturas.

Netflix Open Source Software (Netflix OSS) es el conjunto de librerías y herramientas desarrollados por Netflix que posibilita la ejecución de servicios en la nube. Proporciona una serie de componentes básicos que exponen todas las características esperadas de un entorno en la nube.

Spring Cloud es un framework desarrollado en Java por Spring que expone los patrones comunes implementados por los sistemas distribuidos. Proporciona tanto una serie de capas de abstracción a través de interfaces genéricas como una serie de implementaciones concretas de dichas interfaces con librerías propias o de terceros.

Spring Cloud Netflix es una implementación de las interfaces propuestas por Spring Cloud utilizando las librerías de Netflix OSS y Spring Boot.

El objetivo de todos estos frameworks es que los desarrolladores puedan montar un sistema de servicios distribuidos ejecutándose sobre una o más máquinas. Permiten a los desarrolladores centrarse en la lógica de negocio y no en los detalles de la infraestructura necesaria para la ejecución de los servicios. Entendiendo por servicio una aplicación propia, preferentemente estructurada en microservicios. Es decir, dividida en componentes que realizan una única tarea dentro de un dominio específico y que pueden ser invocados por el resto de servicios independientemente del lenguaje de programación en el que se encuentren implementados.

En la práctica, de cara a explotar un conjunto de servicios, se consideran necesarias, o al menos deseables, una serie de características. Estas características tienen como propósito facilitar la configuración, despliegue, comunicación y monitorización de dichos servicios. El enfoque de Spring Cloud Netflix al respecto es que nos responsabilicemos de tener una aplicación distinta arrancada, o varias instancias de las mismas, para cubrir cada una de estas características. Además de responsabilizarnos de que nuestras aplicaciones se comuniquen con ellas para obtener provecho de dichas características. Es decir, somos responsables de todo el software, tanto de infraestructura como de nuestros propios servicios. Spring Cloud Netflix proporciona las aplicaciones necesarias para el servidor y las librerías necesarias para que nuestros servicios se comuniquen con ellas.

Algunas de las características cubiertas por Spring Cloud Netflix son las siguientes:

– Configuración. Cuando se levanta un servicio es habitual que necesite una serie de parámetros de configuración locales propios. La cuestión no es tanto que estos parámetros se distribuyan como parte del servicio, sino que se puedan establecer y modificar, incluso en tiempo de ejecución, para facilitar la explotación del sistema.

Spring Cloud proporciona un servidor de configuración propio que denomina Spring Cloud Config. Y Netflix OSS por su parte proporciona Archaius, su propio servidor de configuración. Utilizar uno u otro es indistinto, Spring Cloud Netflix actúa como puente presentando a las aplicaciones una misma interface para ambos.

– Descubrimiento. En un sistema distribuido se debe conocer el número de instancias levantadas de un servicio, y un servicio debe poder invocar una instancia de otro servicio independientemente de donde se encuentre ejecutándose.

Netflix OSS proporciona Eureka, un servidor de descubrimiento de servicios. Cuando un servicio se levanta lo notifica a Eureka, que lleva el registro de los servicios levantados y permite que los servicios puedan invocarse los unos a los otros a través de su registro.

– Enrutamiento. Los servicios propios de las aplicaciones normalmente son expuestos públicamente a través de determinadas rutas. Rutas que no deben venir impuestas por la infraestructura donde se encuentren en ejecución.

Netflix OSS proporciona Zuul, un servidor que, en otras muchas cosas, permite definir las rutas en las que los servicios son publicados. Cuando un servicio se invoca, Zuul resuelve la petición redirigiéndola a alguno de los servicios registrados en Eureka.

– Monitorización. En un sistema distribuido con múltiples servicios sobre múltiples máquinas es importante disponer de herramientas que muestren el estado en tiempo real de todos ellos.

Netflix OSS proporciona Turbine, una aplicación cliente que muestra en un cuadro de mando el estado de distintas métricas. Se puede configurar fácilmente para que muestre el estado de los distintos servicios registrados en Eureka. Aunque técnicamente es un agregador de streams de Server-Sent Event (SSE), por lo que es bastante abierto.

Por último, comentar que hoy en día el enfoque de Spring Cloud Netflix, que obliga a los desarrolladores a conocer todos los detalles del funcionamiento del servidor y ligar sus aplicaciones con un framework concreto, ha evolucionado hacia arquitecturas en las que los desarrolladores no tienen que responsabilizarse del software del servidor ni añadir dependencias de terceros en las aplicaciones propias, permitiendo centrarse exclusivamente en la lógica de negocio.

Collectors en Java

Las clases que implementan la interface Collector de Java son las encargadas de convertir en conjuntos finitos de información las secuencias de elementos, potencialmente infinitas, generadas por los streams. Son operaciones terminales de reducción dentro de la cadena de procesamiento de un stream. Acumulan los datos producidos y los almacenan en algún tipo de colección, o generan un único valor a partir de ellos. Se utilizan por ejemplo para crear listas a partir de streams, o calcular valores máximos o mínimos. En la práctica son más conocidas por “¡Ah, eso que se pone al final! La cosa esa del .collect(Collectors.toList())” (sic).

Los recolectores fueron introducidos en Java 8 y han sido ampliados en las versiones 9 y 10. La interface básica es java.util.stream.Collector<T, A, R>, que ofrece cuatro métodos básicos con los que se construyen los recolectores. Estos métodos hacen uso del concepto de interface funcional de Java y no implementan las acciones directamente, sino que retornan una función que es la que realmente implementa la acción. Es como ir a una ventanilla para hacer una gestión y que te respondan “sí, es aquí, pero vaya a esa otra ventanilla”.

Las funciones básicas expuestas por los recolectores son muy genéricas, indican cómo construir un contenedor intermedio, cómo añadir elementos a dicho contenedor, cómo combinar dos contenedores para generar un resultado parcial, y cómo calcular el resultado final a partir de los parciales:

  • Supplier<A> supplier(): Este método retorna una función que crea un contenedor intermedio de tipo A. Un contenedor puede ser de cualquier clase que se quiera. No tiene que implementar ninguna interface concreta. Puede ser una Collection clásica de Java, pero también puede ser un StringBuilder por ejemplo.
  • BiConsumer<A, T> accumulator(): Este método retorna la función que acumula elementos de entrada del tipo T en un contenedor intermedio de tipo A.
  • BinaryOperator<A> combiner(): Este método retorna la función que combina dos contenedores intermedios de tipo A y crea un resultado parcial también de tipo A.
  • Function<A, R> finisher(): Este método retorna la función que calcula el resultado final de tipo R a partir de los resultados parciales de tipo A. El tipo R no tiene que implementar ninguna interface concreta.

Una forma muy directa de implementar un recolector es utilizar uno de los dos métodos estáticos of de la interface Collector. Estos métodos admiten como parámetros de entrada las cuatro funciones básicas y las características del recolector. Siendo estas características una combinación de valores del enumerado Collector.Characteristics. Donde el valor CONCURRENT indica que el proceso de recolección puede ejecutarse de forma concurrente, el valor UNORDERER indica que el resultado no depende del orden original de los elementos de entrada, y el valor IDENTITY_FINISH indica que la función de finalización puede omitirse retornándose directamente el contenedor intermedio. Es el mismo principio de definición de características usado para los Spliterators, nombre inspirado seguramente en alguna de las películas de Parque Jurásico.

A modo de ejemplo, el recolector del código siguiente se crea especificando directamente las funciones como parámetros del método Collector.of. Su propósito es retornar la suma de todos los elementos de tipo Integer de una fuente de entrada. La primera función crea un array con un entero que se puede utilizar como contenedor intermedio, la segunda función suma a un contenedor intermedio un elemento de entrada, la tercera función suma al contenido de un contenedor el contenido de otro, y la cuarta función retorna el resultado final de un contenedor.

El recolector anterior puede aplicarse directamente en un stream de enteros para obtener la suma de todos los elementos:

Como es de suponer, los recolectores se pueden crear de una manera más tradicional definiendo una clase que implementa la interface Collector, sin necesidad de utilizar expresiones lambdas como en el código de ejemplo anterior. No obstante, normalmente no suele ser necesario escribirlos, ya que Java proporciona de forma nativa implementaciones de una gran cantidad de recolectores de uso habitual a través de métodos estáticos de la clase java.util.stream.Collectors.

Los recolectores más comunes de la clase Collectors son los que crean colecciones, como los clásicos métodos toList, toSet y toMap que existen desde la versión 8 de Java, más los que se añadieron en la versión 10 como toUnmodifiableList, toUnmodifiableSet y toUnmodifiableMap, alguno más específico como toConcurrentMap, o el más genérico toCollection:

Otros recolectores de gran utilidad ofrecidos por Collectors son los que realizan reducciones, es decir, calculan un único resultado a partir de los elementos de entrada. Ejemplos de este tipo de recolectores son counting, que cuenta el número de elementos, joining, que concatena todos los elementos como una cadena de texto,  maxBy y minBy, que calculan el máximo y mínimo, summingInt, summingLong y summingDouble, que calculan la suma de los elementos, y averagingInt, averagingLong y averagingDouble, que calculan la media de los elementos. Un caso particular es summarizingInt, summarizingLong y summarizingDouble, que calcula todos los valores anteriores a un mismo tiempo, es decir que retorna un objeto que contiene el total de elementos, el máximo, el mínimo y la media.

Otro tipo interesante de recolectores son los de agrupación, que como su propio nombre indica permiten agrupar los elementos de entrada en base a un determinado criterio. La salida de estos recolectores es un Map, donde la clave es el criterio de agrupación y el valor la colección de elementos agrupados.

La clase Collectors  expone más factorías de recolectores aparte de las aquí citadas, por lo que es recomendable echar un vistazo a la documentación del API.

Como nota final, comentar que he decidido traducir collector como recolector, en vez de dejarlo en el inglés original, como se suele hacer con otras palabras, como stream por ejemplo, por la similitud con la palabra en español, pero evitando traducirlo directamente como colector, que para mí tiene otro sentido.

¡Feliz día de la recolección!

Recorridos aleatorios en Java

La forma habitual de desordenar una lista en Java es utilizar el método Collections.shuffle. El inconveniente de este método es que modifica la lista original, un efecto lateral no deseable en el mundo actual donde predominan las colecciones inmutables. La alternativa más obvia es crear una nueva lista, a costa lógicamente de duplicar el consumo de memoria.

Un enfoque distinto es recorrer la lista de forma desordenada. La premisa de tal tipo de recorrido es que se retornen todos los elementos de la lista de manera aleatoria y que cada uno de ellos sólo se retorne una única vez. La condición es que la lista sea de tamaño fijo, preferentemente inmutable; lo contrario podría dar lugar a implementaciones con comportamientos indefinidos no predecibles. Y la restricción es que la operación se ejecute de forma secuencial, no paralela, ya que realizar particiones de la lista no permitiría acceder a todos los elementos de manera aleatoria.

Una implementación de tal tipo de recorrido implica generar un número aleatorio en cada iteración que indique el índice del elemento a retornar. Además de almacenar todos los índices generados para evitar duplicados. Para ello, para una lista de tamaño   n, se puede crear un array de n índices, inicializar dicho array secuencialmente con valores de  a n, en cada iteración i generar un número aleatorio r de i a n - 1, intercambiar en el array el valor en la posición  i por el valor en la posición  r, y retornar el elemento de la lista que se encuentra en la posición indicada por el valor almacenado en la posición  i del array. Es decir, sustituir el elemento de la iteración en curso por otro de forma aleatoria y que aún no se haya retornado.

Partiendo de que en la actualidad se prefiere consumir colecciones con streams, en vez de con bucles for, la solución pasa por la implementación de un iterador, y de forma más concreta de un Spliterator<T>. Esta clase se introdujo con Java 8 para facilitar la construcción de iteradores complejos que pudieran consumir elementos tanto de forma secuencial como paralela, además de tener la capacidad de describirse a ellos mismos indicando cuales son sus características.

SplitIterator<T> es una interface que requiere implementar varios métodos, pero para la mayoría de los casos de uso, que no requieran soportar paralelismo, se puede simplemente extender de Spliterators.AbstractSpliterator<T>. Esta clase sólo requiere implementar el método boolean tryAdvance(Consumer<? super T> action). Un método que es equivalente a los clásicos métodos hasNext y next de los iteradores de Java, ya que a un mismo tiempo consume el elemento en curso e indica si quedan más elementos por consumir.

Una posible implementación de tal algoritmo podría ser la siguiente:

En el código, elements almacena la lista original y size su tamaño,  indexes es el array de índices recorridos y current el número de la iteración en curso. El método tryAdvance consume el elemento en curso, y el método next realiza el proceso de generación del número aleatorio y actualización del array de índices. El código debería resultar bastante sencillo de entender con los comentarios.

Notar el uso de la clase SplittableRandom para la generación de números aleatorios. Esta clase se introdujo en Java 8, junto con SecureRandom cuando la aleatoriedad de la secuencia de números generados deba ser más estricta y cumplir criterios más estrictos como los requeridos en el mundo de la criptografía.

El iterador del código de ejemplo se puede probar creando una lista, una instancia del iterador, y procesando el conjunto como un stream con StreamSupport.stream:

En cada ejecución la salida será distinta, devolviéndose los elementos de la lista original en un orden aleatorio cada vez.

La variable characteristics establece las características de la lista para facilitar a los clientes del iterador la toma de decisiones. IMMUTABLE indica que es inmutable, SIZED que tiene un tamaño prederminado, y NONNULL que no contiene nulos.  De la forma acostumbrada, es recomendable revisar la documentación para ver todas las opciones disponibles. En la práctica sería recomendable diseñar el iterador para un determinado caso de uso evitando tener que pasar dichos parámetros cada vez.

De cualquier forma, la implementación propuesta funciona, pero es muy básica. Por ejemplo, el array de índices se inicializa a costa de recorrerlo completamente, algo que puede evitarse tratando el cero como caso especial, ya que ese el valor que por defecto asigna Java a las variables de tipo int. Si un índice es cero entonces se puede interpretar como que no se ha visitado todavía y que su valor es la propia posición que ocupa, ya que ese es el valor con el que se debería haber inicializado originalmente:

De igual forma sería interesante pensar en reutilizar los arrays de índices cuando se procesen múltiples listas del mismo tamaño de forma secuencial, para evitar la reserva de memoria necesaria para crear dichos arrays por cada lista. Para ello se podría utilizar una variable que almacenase el valor del índice utilizado como caso especial, en vez utilizar el valor constante cero. Al guardar un índice en el array se tendría que almacenar sumándole el valor de dicha variable y compararlo también contra dicho valor. Así, en el primer uso del array el valor de la variable sería cero y el funcionamiento sería el mismo que el del código anterior. Pero al terminar el primer recorrido, y posteriores, se le sumaría a la variable el tamaño de la lista recorrida. En el segundo recorrido, y posteriores, los índices se almacenarían sumándoles dicho valor y se compararían contra dicho valor. Si el índice es menor que dicho valor entonces se consideraría que no se ha visitado. Por seguridad, cada cierto número de recorridos se podría inicializar el array y la variable a cero.

Por último, comentar que en el caso de realmente querer recorrer una lista de forma aleatoria en paralelo, una posible solución sería generar primero el array con los índices desordenados, y luego tratar en paralelo este array de forma particionada, en vez de particionar la lista como normalmente se suele hacer.