
Hola habr
Este libro describe la nueva generación de Java EE. Se embarcará en un viaje a través de Java EE en el contexto del mundo moderno de microservicios y contenedores. Esta no es una guía de referencia para la sintaxis de API: los conceptos y técnicas presentados aquí reflejan la experiencia real de una persona que recientemente ha recorrido este camino, prestando mucha atención a los obstáculos que surgen y está listo para compartir su conocimiento. En diversas situaciones, comenzando con la creación de un paquete para pruebas y uso en la nube, este libro será un compañero ideal tanto para principiantes como para desarrolladores experimentados que buscan comprender más que solo una API y ayudarlos a reconstruir sus mentes para crear una arquitectura de aplicación moderna en Java EE .
Secuencia de ejecución
Los procesos comerciales implementados en aplicaciones empresariales describen flujos de procesos específicos. Para los escenarios de negocios involucrados, este es un proceso de solicitud y respuesta síncrono, o el procesamiento asíncrono de un proceso iniciado.
Los escenarios empresariales se llaman en subprocesos separados, un subproceso por solicitud o llamada. El contenedor crea las secuencias y las coloca en la unidad para su reutilización después de que la llamada se haya procesado correctamente. Por defecto, los procesos empresariales definidos en las clases de aplicación, así como las tareas transversales como las transacciones, se ejecutan secuencialmente.
Ejecución sincrónica
Un escenario típico cuando una solicitud HTTP requiere una respuesta de la base de datos se implementa de la siguiente manera. Un subproceso procesa la solicitud que llega al bucle, por ejemplo, JAX-RS UsersResource, invirtiendo el principio de control; El contenedor llama al método de recurso JAX-RS. El recurso implementa y usa el UserManagement EJB, que también es llamado implícitamente por el contenedor. Todas las operaciones son realizadas por intermediarios sincrónicamente. El usuario EJB utilizará el administrador de la entidad para almacenar la nueva entidad, y tan pronto como finalice el método comercial que inició la transacción actualmente activa, el contenedor intentará confirmar la transacción en la base de datos. Dependiendo del resultado de la transacción, el método de recursos del circuito reanuda la operación y genera una respuesta para el cliente. Todo sucede sincrónicamente, en este momento el cliente está bloqueado y esperando una respuesta.
La ejecución sincrónica incluye el procesamiento de eventos CDI sincrónicos. Separan la activación de eventos de dominio de su procesamiento, sin embargo, los eventos se procesan sincrónicamente. Existen varios métodos para monitorear las transacciones. Si se indica una etapa de transacción, entonces el evento se puede procesar en esta etapa, durante la reparación de la transacción, antes de su finalización, después de la finalización, en caso de transacción fallida o exitosa. Por defecto, o si la transacción está inactiva, los eventos CDI se procesan inmediatamente cuando ocurren. Esto permite a los ingenieros implementar soluciones complejas, por ejemplo, utilizando eventos que ocurren solo después de agregar entidades a la base de datos. Sea como fuere, en todos los casos, el procesamiento se realiza sincrónicamente.
Ejecución asincrónica
La ejecución sincronizada de tareas satisface los requisitos de muchos escenarios comerciales, pero hay momentos en los que necesita un comportamiento asincrónico. Existen varias restricciones sobre el uso de subprocesos por el entorno Java EE. El contenedor gestiona recursos y flujos y los coloca en la unidad. Las utilidades de control de concurrencia externa se encuentran fuera del contenedor y no tienen conocimiento de estas transmisiones. Por lo tanto, el código de la aplicación no debe ejecutarse y controlar sus hilos. Para hacer esto, utiliza las características de Java EE. Hay varias API con soporte asíncrono incorporado.
Métodos asincrónicos de EJBLa forma más fácil de implementar un comportamiento asincrónico es utilizar la anotación @Asynchronous para un método de negocios de clase EJB o EJB. Las llamadas a estos métodos regresan de inmediato, a veces con una respuesta de tipo Futuro. Se ejecutan en un hilo separado controlado por el contenedor. Este método funciona bien para escenarios simples, pero está limitado a EJB:
@Asynchronous @Stateless public class Calculator { public void calculatePi(long decimalPlaces) {
Servicio de gestión del rendimientoPara la ejecución asincrónica de tareas en objetos CDI administrados o utilizando utilidades de control de concurrencia de Java SE, Java EE incluye versiones administradas por contenedor de las funciones ExecutorService y ScheduledExecutorService. Se utilizan para implementar tareas asincrónicas en subprocesos controlados por contenedores. Las instancias ManagedExecutorService y ManagedScheduledExecutorService están integradas en el código de la aplicación. Se pueden usar para ejecutar su propia lógica, pero son más efectivos cuando se combinan con las utilidades de control de concurrencia de Java SE, como los valores futuros complementados. El siguiente ejemplo muestra cómo crear valores futuros rellenados utilizando subprocesos controlados por contenedor:
import javax.annotation.Resource; import javax.enterprise.concurrent.ManagedExecutorService; import java.util.Random; import java.util.concurrent.CompletableFuture; @Stateless public class Calculator { @Resource ManagedExecutorService mes; public CompletableFuture<Double> calculateRandomPi(int maxDecimalPlaces) { return CompletableFuture.supplyAsync(() -> new Random().nextInt(maxDecimalPlaces) + 1, mes) .thenApply(this::calculatePi); } private double calculatePi(long decimalPlaces) { … } }
El objeto Calculadora devuelve el valor futuro complementado del tipo doble, que aún se puede calcular cuando se reanuda el contexto de llamada. Se puede solicitar cuando se completan los cálculos, así como combinarse con cálculos posteriores. No importa dónde se requieran nuevos subprocesos en la aplicación empresarial, debe usar la funcionalidad Java EE para administrarlos.
Eventos CDI asincrónicosLos eventos CDI también se pueden procesar de forma asincrónica. En este caso, el contenedor también proporciona una secuencia para manejar eventos. Para describir un controlador de eventos asíncrono, el método se anota con @ObservesAsync, y el evento se activa usando el método fireAsync (). Los siguientes fragmentos de código demuestran eventos CDI asíncronos:
@Stateless public class CarManufacturer { @Inject CarFactory carFactory; @Inject Event<CarCreated> carCreated; public Car manufactureCar(Specification spec) { Car car = carFactory.createCar(spec); carCreated.fireAsync(new CarCreated(spec)); return car; } }
El controlador de eventos se llama en su propio subproceso administrado por contenedor:
import javax.enterprise.event.ObservesAsync; public class CreatedCarListener { public void onCarCreated(@ObservesAsync CarCreated event) {
Por razones de compatibilidad con versiones anteriores, los eventos CDI sincrónicos también se pueden procesar en el método EJB asíncrono. Por lo tanto, los eventos y controladores se definen como síncronos, y el método de controlador es un método de negocio EJB con anotación @Asynchronous. Antes de que se introdujeran eventos asincrónicos en el estándar CDI para Java EE 8, esta era la única forma de implementar esta característica. Para evitar confusiones en Java EE 8 y versiones posteriores, es mejor evitar esta implementación.
Ámbitos de procesamiento asincrónicoComo el contenedor no tiene información sobre cuánto tiempo se pueden realizar tareas asincrónicas, el uso de ámbitos en este caso es limitado. Los objetos con un alcance dentro de la solicitud o sesión que estaban disponibles cuando se lanzó la tarea asincrónica no estarán necesariamente activos durante su implementación: la solicitud y la sesión pueden finalizar mucho antes de su finalización. Por lo tanto, los subprocesos que realizan tareas asincrónicas, como las proporcionadas por el servicio ejecutor programado o los eventos asincrónicos, pueden no tener acceso a instancias de entidades administradas dentro del alcance de la solicitud o sesión que estuvieron activas durante la llamada. Lo mismo ocurre con el acceso a enlaces a instancias incrustadas, por ejemplo, en métodos lambda que son parte de la ejecución sincrónica.
Esto debe tenerse en cuenta al modelar tareas asincrónicas. Toda la información sobre una llamada en particular debe proporcionarse en el momento en que comienza la tarea. Sin embargo, una tarea asincrónica puede tener sus propias instancias de objetos administrados con un alcance limitado.
Establecer tiempo de ejecuciónLos escenarios empresariales pueden llamarse no solo desde el exterior, por ejemplo, a través de una solicitud HTTP, sino también desde la aplicación, una tarea que se ejecuta en un momento específico.
En el mundo de Unix, la funcionalidad para ejecutar trabajos periódicos es popular: estas son las tareas del planificador. Los EJB proporcionan capacidades similares utilizando temporizadores EJB. Los temporizadores invocan métodos comerciales a intervalos específicos o después de un tiempo específico. El siguiente ejemplo describe un temporizador cíclico que comienza cada diez minutos:
import javax.ejb.Schedule; import javax.ejb.Startup; @Singleton @Startup public class PeriodicJob { @Schedule(minute = "*/10", hour = "*", persistent = false) public void executeJob() {
Cualquier EJB (singletones, objetos gestionados con o sin persistencia de estado) puede crear temporizadores. Sin embargo, en la mayoría de los escenarios tiene sentido crear temporizadores solo para singleton. El retraso se establece para todos los objetos activos. Por lo general, es necesario iniciar las tareas programadas a tiempo, por lo que se usa en singleton. Por el mismo motivo, en este ejemplo, el objeto EJB debe estar activo cuando se inicia la aplicación. Esto asegura que el temporizador comience a funcionar de inmediato.
Si describe el temporizador como una constante, su vida útil se extiende a todo el ciclo de vida de la JVM. El contenedor es responsable de almacenar temporizadores persistentes, generalmente en la base de datos. Los temporizadores permanentes, que deberían funcionar mientras la aplicación no está disponible, se activan al inicio. También le permite usar los mismos temporizadores con múltiples instancias del objeto. Los temporizadores constantes con una configuración de servidor adecuada son una solución adecuada si necesita ejecutar un proceso comercial exactamente una vez en varios servidores.
Los temporizadores que se crean automáticamente mediante la anotación
Schedule se describen mediante expresiones cron similares a Unix. Para una mayor flexibilidad temporizadores programa EJB-describe utilizando el servicio proporcionado por el temporizador de contenedor que genera los Temporizadores métodos de devolución de llamada y
de tiempo de espera .
Las tareas periódicas y diferidas también se pueden describir fuera de los EJB utilizando el servicio de planificador administrado por contenedor. Una instancia de ManagedScheduledExecutorService que realiza tareas después del retraso especificado o en intervalos especificados se implementa en los componentes administrados. Estas tareas se implementarán en subprocesos controlados por contenedores:
@ApplicationScoped public class Periodic { @Resource ManagedScheduledExecutorService mses; public void startAsyncJobs() { mses.schedule(this::execute, 10, TimeUnit.SECONDS); mses.scheduleAtFixedRate(this::execute, 60, 10, TimeUnit.SECONDS); } private void execute() { … } }
Llamar al método startAsyncJobs () ejecutará la función execute () en el subproceso administrado diez segundos después de la llamada y luego cada diez segundos después del primer minuto.
Asincronía y reactividad en JAX-RSJAX-RS admite el comportamiento asíncrono para no bloquear innecesariamente los flujos de solicitud del lado del servidor. Incluso si una conexión HTTP está esperando una respuesta, la secuencia de solicitud puede continuar procesando otras solicitudes mientras se ejecuta un proceso prolongado en el servidor. Los flujos de solicitud se agregan en un contenedor, y este repositorio de solicitud es de cierto tamaño. Para no desperdiciar la secuencia de solicitud, los métodos de recursos asíncronos JAX-RS crean tareas que se ejecutan cuando la secuencia de solicitud regresa y se puede reutilizar. La conexión HTTP se reanuda y da una respuesta después de completar la tarea asincrónica o después de un tiempo de espera. El siguiente ejemplo muestra el método de recurso asíncrono JAX-RS:
@Path("users") @Consumes(MediaType.APPLICATION_JSON) public class UsersResource { @Resource ManagedExecutorService mes; … @POST public CompletionStage<Response> createUserAsync(User user) { return CompletableFuture.supplyAsync(() -> createUser(user), mes); } private Response createUser(User user) { userStore.create(user); return Response.accepted().build(); } }
Para mantener el flujo de solicitudes ocupado durante demasiado tiempo, el método JAX-RS debe completarse rápidamente. Esto se debe al hecho de que el método de recursos se llama desde el contenedor mediante inversión de control. El resultado obtenido en la etapa de finalización se utilizará para reanudar la conexión del cliente al final del procesamiento.
Las etapas de retorno de finalización son una tecnología relativamente nueva en la API JAX-RS. Si necesita describir el retraso y al mismo tiempo proporcionar una mayor flexibilidad con una respuesta asincrónica, puede incluir el tipo AsyncResponse en el método. Este enfoque se demuestra en el siguiente ejemplo:
import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.container.Suspended; @Path("users") @Consumes(MediaType.APPLICATION_JSON) public class UsersResource { @Resource ManagedExecutorService mes; … @POST public void createUserAsync(User user, @Suspended AsyncResponse response) { response.setTimeout(5, TimeUnit.SECONDS); response.setTimeoutHandler(r -> r.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).build())); mes.execute(() -> response.resume(createUser(user))); } }
Gracias a los tiempos de espera creados, la solicitud del cliente no esperará indefinidamente, sino solo hasta que se reciba el resultado o caduque el tiempo de espera de la llamada. Sin embargo, los cálculos continuarán a medida que se realicen de forma asincrónica. Para los recursos JAX-RS implementados como EJB, puede aplicar la anotación @Asynchronous para que no llame explícitamente a los métodos comerciales asincrónicos a través del ejecutor de servicios.
El cliente JAX-RS también admite comportamiento asíncrono. Dependiendo de los requisitos, tiene sentido no bloquearlo durante las llamadas HTTP. El ejemplo anterior muestra cómo establecer demoras para las solicitudes de los clientes. Para llamadas de sistema externo de larga duración y especialmente paralelas, es mejor usar un comportamiento asíncrono y reactivo.
Considere varias aplicaciones de servidor que proporcionan información meteorológica. El componente del cliente accede a todas estas aplicaciones y calcula el pronóstico del tiempo promedio. Idealmente, podría hacer que el acceso a los sistemas sea paralelo:
import java.util.stream.Collectors; @ApplicationScoped public class WeatherForecast { private Client client; private List<WebTarget> targets; @Resource ManagedExecutorService mes; @PostConstruct private void initClient() { client = ClientBuilder.newClient(); targets = … } public Forecast getAverageForecast() { return invokeTargetsAsync() .stream() .map(CompletableFuture::join) .reduce(this::calculateAverage) .orElseThrow(() -> new IllegalStateException(" ")); } private List<CompletableFuture<Forecast>> invokeTargetsAsync() { return targets.stream() .map(t -> CompletableFuture.supplyAsync(() -> t .request(MediaType.APPLICATION_JSON_TYPE) .get(Forecast.class), mes)) .collect(Collectors.toList()); } private Forecast calculateAverage(Forecast first, Forecast second) { … } @PreDestroy public void closeClient() { client.close(); } }
El método invokeTargetsAsync () llama a los objetos disponibles de forma asincrónica, invocando el servicio del ejecutor programado. Los descriptores CompletableFuture se devuelven y se utilizan para calcular los resultados promediados. El inicio del método join () se bloqueará hasta que se complete la llamada y se reciban los resultados.
Los objetos que se llaman asíncronamente comienzan y esperan una respuesta de varios recursos a la vez, posiblemente más lentamente. En este caso, esperar las respuestas de los recursos del servicio meteorológico lleva tanto tiempo como esperar la respuesta más lenta, y no todas las respuestas juntas.
La última versión de JAX-RS tiene soporte incorporado para las etapas de finalización, lo que reduce el código estereotipado en las aplicaciones. Al igual que con los valores rellenados, la llamada devuelve inmediatamente el código de fase de finalización para referencia futura. El siguiente ejemplo muestra las funciones de cliente reactivas JAX-RS utilizando la llamada rx ():
public Forecast getAverageForecast() { return invokeTargetsAsync() .stream() .reduce((l, r) -> l.thenCombine(r, this::calculateAverage)) .map(s -> s.toCompletableFuture().join()) .orElseThrow(() -> new IllegalStateException(" ")); } private List<CompletionStage<Forecast>> invokeTargetsAsync() { return targets.stream() .map(t -> t .request(MediaType.APPLICATION_JSON_TYPE) .rx() .get(Forecast.class)) .collect(Collectors.toList()); }
En el ejemplo anterior, no necesita buscar el servicio de los ejecutores programados; el cliente JAX-RS lo administrará por sí mismo. Antes de que apareciera el método rx (), los clientes usaban una llamada async () explícita. Este método se comportó de manera similar, pero solo devolvió objetos futuros. El uso de un enfoque reactivo en los clientes es óptimo para la mayoría de los proyectos.
Como puede ver, Java EE utiliza un servicio de artista administrado por contenedor.
Conceptos y principios de diseño en Java EE moderno
La API Java EE se basa en convenciones y principios de diseño que se detallan como estándares. Los ingenieros de software encontrarán plantillas API conocidas y enfoques de desarrollo de aplicaciones en él. El objetivo de Java EE es promover el uso consistente de la API.
El principio principal de las aplicaciones centradas principalmente en la implementación de escenarios empresariales es: la tecnología no debe interferir. Como ya se mencionó, los ingenieros deberían poder concentrarse en implementar la lógica empresarial sin pasar la mayor parte del tiempo en problemas tecnológicos y de infraestructura. Idealmente, la lógica de dominio se implementa en Java simple y se complementa con anotaciones y otras propiedades compatibles con el entorno corporativo, sin afectar el código de dominio o complicarlo. Esto significa que la tecnología no requiere mucha atención de los ingenieros y no impone restricciones demasiado grandes. El entorno J2EE solía requerir muchas soluciones muy complejas. Para implementar las interfaces y extender las clases base, tuvimos que usar objetos administrados y objetos de almacenamiento persistente. Esto complicó la lógica del área temática y dificultó las pruebas.
En Java EE, la lógica de dominio se implementa en forma de clases Java simples equipadas con anotaciones, según las cuales el contenedor resuelve ciertas tareas corporativas durante la ejecución de la aplicación. La práctica de crear código limpio a menudo implica escribir código que sea más hermoso que conveniente para su reutilización. Java EE admite este enfoque. Si por alguna razón necesita eliminar la tecnología y dejar la lógica pura del área temática, esto se hace simplemente eliminando las anotaciones correspondientes.
Como veremos en el Capítulo 7, este enfoque de programación implica la necesidad de realizar pruebas, porque para los programadores, la mayoría de las especificaciones Java EE no son más que anotaciones.
A lo largo de la API, se ha adoptado un principio de diseño llamado inversión de control (IoC); en otras palabras, "no nos llame, nos llamaremos a nosotros mismos". Esto es especialmente notable en los circuitos de aplicación, como los recursos JAX-RS. Los métodos de recursos se describen utilizando anotaciones de métodos Java, que luego son llamados por el contenedor en el contexto apropiado. Lo mismo es cierto para la inyección de dependencia, en la que debe elegir generadores o tener en cuenta tareas transversales como los interceptores. Los desarrolladores de aplicaciones pueden centrarse en implementar la lógica y describir las relaciones, dejando la implementación de detalles técnicos en un contenedor. Otro ejemplo, no tan obvio, es la descripción de convertir objetos Java a JSON y viceversa a través de anotaciones JSON-B. Los objetos se transforman no solo en una forma explícita y programada, sino también implícitamente, en un estilo declarativo.
Otro principio que permite a los ingenieros aplicar esta tecnología de manera efectiva es la programación por acuerdo. Por defecto, Java EE define un comportamiento específico que coincide con la mayoría de los escenarios de uso. Si no es suficiente o no cumple con los requisitos, el comportamiento puede redefinirse, a menudo en varios niveles.
Hay muchos ejemplos de programación de convenciones. Uno de ellos es el uso de métodos de recursos JAX-RS que convierten la funcionalidad de Java en respuestas HTTP. Si el comportamiento estándar de JAX-RS con respecto a las respuestas no cumple los requisitos, puede aplicar el tipo de respuesta Respuesta. Otro ejemplo es la especificación de objetos gestionados, que generalmente se implementa mediante anotaciones. Para cambiar este comportamiento, puede usar el descriptor XML beans.xml. Es muy conveniente para los programadores que en el mundo moderno de Java EE, las aplicaciones empresariales se desarrollen de una manera pragmática y de alto rendimiento que generalmente no requiere un uso intensivo de XML como antes.
En cuanto a la productividad de los programadores, otro principio importante de desarrollo en Java EE es que esta plataforma requiere integración en el contenedor de varios estándares. Debido a que los contenedores admiten un conjunto específico de API, y si se admite toda la API Java EE, ese es exactamente el caso, también requiere implementaciones de API para proporcionar una integración perfecta de otras API. La ventaja de este enfoque es la capacidad de usar recursos JAX-RS de conversión JSON-B y tecnología de validación de frijoles sin configuración explícita adicional, con la excepción de las anotaciones. En los ejemplos anteriores, vimos cómo las funciones definidas en estándares individuales se pueden usar juntas sin esfuerzo adicional. Esta es una de las mayores ventajas de la plataforma Java EE. Una especificación genérica garantiza una combinación de estándares individuales. Los programadores pueden confiar en ciertas características e implementación proporcionadas por el servidor de aplicaciones.
Código de alta calidad fácil de usar
Los programadores generalmente aceptan que debes esforzarte por escribir código de alta calidad. Sin embargo, no todas las tecnologías son igualmente adecuadas para esto.
Como se mencionó al principio del libro, el enfoque en el desarrollo de aplicaciones debe ser la lógica empresarial. En caso de cambios en la lógica de negocios o la aparición de nuevos conocimientos, es necesario actualizar el modelo de dominio, así como el código fuente. Se requiere una refactorización iterativa para crear y mantener un modelo de dominio de alta calidad y un código fuente en su conjunto. Los esfuerzos para profundizar la comprensión del área temática se describen en el concepto de diseño orientado a problemas.
Hay mucha literatura sobre refactorización a nivel de código. - , , . , . , .
- . , , — , , - . — , , . . , , .
, . , .
, , , . , , - . , , , - , . , . 7.
, . , , , . Java EE : , , . .
. , , , . . 6 , .
. , , , . , . , , . , . . , . - .
Sobre el autor
(Sebastian Daschner) — Java-, , Java (EE). JCP, Java EE, JSR 370 374 . Java Java - Oracle.
IT-, JavaLand, JavaOne Jfokus. JavaOne Rockstar JavaOne 2016. Java (Steve Chin) Java, . JOnsen — Java, .
(Melissa McKay) — 15- , . Java-, . , , .
JCrete () JOnsen . IT- , JavaOne4Kids JCrete4Kids. JavaOne 2017 Denver Java User Group.
»Se puede encontrar más información sobre el libro en
el sitio web del editor»
Contenidos»
Extracto20% —
Java EE