CodeGym /Cursos /Spring Core /Eventos a través de ApplicationEventPublisher

Eventos a través de ApplicationEventPublisher

Spring Core
Nivel 17, Lección 2
Disponible

1. El punto publishEvent()

Si añade eventos a una aplicación por primera vez, a menudo la mano tiende a insertar publishEvent() «en algún lugar temprano», para que todos los handlers puedan reaccionar a tiempo. Y aquí comienza la tragicomedia clásica: publica OrderCreatedEvent, pero luego resulta que el pedido no se guardó, el precio se recalculó incorrectamente o incluso se lanzó una excepción. El evento ya se ha dispersado, pero el hecho empresarial prácticamente no existe. Es como escribir en un chat grupal «ya estoy de vacaciones» y luego recordar que no envió la solicitud.

Para no encontrarse con esos errores de «vacaciones» en producción (y también en el proyecto de aprendizaje), mantengamos una regla simple: publicamos el evento solo después de que el paso principal del escenario realmente se haya completado.

En términos de ContextFlow esto suena así:

  • «Pedido creado» significa que el pedido ya se ha reunido y guardado en OrderStore y tiene un identificador al que pueden referirse las reacciones.
  • «Pedido cancelado» significa que el estado se cambió a CANCELLED, la razón de la cancelación está registrada (en el modelo de dominio) y el estado final se guardó en OrderStore.

Es útil imaginar el escenario como una breve secuencia de pasos, donde el evento es un marcador ordenado entre el núcleo y las reacciones:

flowchart TD
    %% publishEvent se coloca después de la fijación del estado, pero antes de salir del método del escenario
    A["Servicio de escenario: place/cancel"] --> B["Núcleo: cambiamos estado + guardamos"]
    B --> C["publishEvent(Hecho empresarial)"]
    C --> D["Reacciones: auditoría, notificación, estadísticas (oyentes)"]

Tenga en cuenta: no analizamos los oyentes en detalle; esa es la próxima lección. Ahora nos importa el punto publishEvent(): está después del cambio de estado y antes de salir del método del escenario, para que el evento sea un «hecho» honesto, no una promesa.

2. ApplicationEventPublisher en lugar de ApplicationContext

Cuando ve la palabra «publisher», es fácil decidir: «Bueno, ya que los eventos son parte de Spring, entonces ahora inyectaré ApplicationContext y desde allí publicaré todo». Técnicamente funciona, pero metodológicamente (y arquitectónicamente) casi siempre es un paso hacia el antipatrón localizador de servicios. Hoy es importante hacerlo correctamente: el servicio de escenarios debe depender de un contrato estrecho que necesita, no «de todo el contenedor de una vez».

Y precisamente para eso Spring proporciona la interfaz ApplicationEventPublisher. Dice una cosa simple: «sé publicar un evento». No «sé darte cualquier bean», no «sé gestionar el ciclo de vida», sino exactamente lo que se necesita en ese momento.

Mire la mini comparación: fija bien por qué en el servicio de escenarios tiramos del publisher:

Qué inyectamos Qué significa en la práctica Por qué es (no) bueno
ApplicationEventPublisher «Sé publicar eventos» Estrecho, honesto, no provoca getBean()
ApplicationContext «Lo sé todo sobre el contenedor» Demasiado amplio, tienta a convertir el servicio en un «buscador de beans»
OrderEventsGateway (interfaz propia) «Publico hechos de dominio a través de mi propio puerto» A menudo es bueno en producción, pero en un curso de aprendizaje añade una capa extra de abstracción

En nuestro proyecto de aprendizaje elegimos la primera opción: inyectamos ApplicationEventPublisher a través del constructor (mediante la conocida inyección por constructor) para que la dependencia sea visible y mínima.

Un pequeño matiz importante que se puede aceptar como un hecho: el propio ApplicationContext sabe ser un publisher, por lo que Spring es capaz de proporcionar ApplicationEventPublisher como dependencia sin configuración adicional. Es una «capacidad incorporada» del contexto, no algo que debamos registrar manualmente con @Bean.

3. Publicación de OrderCreatedEvent

Ahora hagamos aquello por lo que se empezó todo: añadamos la publicación del evento en el escenario de creación del pedido. Es importante comenzar con la «geografía de responsabilidades» correcta. El servicio de escenarios (OrderPlacementService) es precisamente el lugar que sabe que la creación del pedido realmente ocurrió: inicia la acción, guarda el resultado y devuelve el control al exterior. Por lo tanto, también debe declarar el hecho como un evento.

Ni la entidad de dominio Order, ni OrderStore, ni ninguna clase «utility» son un buen punto de publicación. Si metemos la publicación dentro de Order, obtendremos un modelo de dominio que depende de Spring (y el dominio debe vivir en el mundo Java, no en org.springframework.*). Si metemos la publicación en OrderStore, obtendremos un «almacén con efectos secundarios» donde el evento no se publica por el significado del escenario, sino «por el hecho de guardar», y eso rápidamente se convierte en confusión.

A continuación se muestra un fragmento mínimo del servicio que publica el evento después de guardar:

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {
    // Inyectamos un contrato estrecho: el servicio solo necesita publicar eventos, no todo el contexto
    private final ApplicationEventPublisher publisher;

    public OrderPlacementService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void place(Order order) {
        // Aquí se muestra deliberadamente solo la mecánica de publishEvent — sin guardar (ver el siguiente ejemplo)
        // En el evento colocamos los datos mínimamente suficientes para las reacciones: id del pedido e id del cliente
        publisher.publishEvent(new OrderCreatedEvent(this, order.getId(), order.getCustomer().getId()));
    }
}

Sí, aquí todavía no hay orderStore.save(order) — mostré deliberadamente la «mecánica pura» de la publicación para que no se ahogue en los detalles de negocio. En el código real de ContextFlow, el evento debe aparecer después de guardar.

Hagamos el ejemplo más cercano a la realidad, pero aún compacto. Supongamos que el servicio guarda el pedido y luego publica OrderCreatedEvent:

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {
    // El almacén registra el resultado del escenario (en el proyecto de aprendizaje es in-memory, pero el sentido es el mismo)
    private final OrderStore orderStore;

    // Publisher — el punto de declaración del hecho empresarial hacia el exterior
    private final ApplicationEventPublisher publisher;

    public OrderPlacementService(OrderStore orderStore, ApplicationEventPublisher publisher) {
        this.orderStore = orderStore;
        this.publisher = publisher;
    }

    public void place(Order order) {
        // 1) Primero fijamos el estado: el pedido realmente se ha creado y guardado
        orderStore.save(order);

        // 2) Luego declaramos el hecho: «pedido creado» (no «planeamos crearlo»)
        publisher.publishEvent(new OrderCreatedEvent(this, order.getId(), order.getCustomer().getId()));
    }
}

Tenga en cuenta dos cosas.

Primera: ApplicationEventPublisher se inyecta como una dependencia normal. Ni rastro de ApplicationContext, ni de «deme todo el contenedor, ya me las arreglaré». Es un importante «reflejo muscular»: le pide a Spring exactamente lo que necesita.

Segunda: OrderCreatedEvent se crea directamente en el lugar de la publicación, porque aquí tenemos a mano los datos necesarios. Y esto es normal. Cuando empieza, quiere sacar la «creación del evento» a una clase separada OrderEventFactory, pero en un proyecto de aprendizaje esto a menudo genera más ruido que beneficio. Sacaremos la creación a un método separado solo si vemos que el código realmente comienza a hincharse.

source: valor y elección de this

ApplicationEvent tiene un source obligatorio. En la mayoría de los escenarios de aplicación, no se usa source en la lógica de negocio en absoluto, pero es útil para el diagnóstico y la lectura de registros/depuración: se puede entender quién publicó exactamente el evento. Por lo tanto, this (es decir, el propio OrderPlacementService) es una elección absolutamente normal.

Si siente un dolor interno de «no quiero exponer el servicio como source», puede pasar algo más abstracto (por ejemplo, la cadena "order-placement"), pero para la transparencia educativa, «this» es la opción más simple: sabe exactamente de dónde salió este evento.

4. OrderCancelledEvent en OrderCancellationService

La cancelación de un pedido es el segundo escenario principal del proyecto. Y es más interesante en cuanto al «punto de publicación», porque la cancelación no es solo «algo que sucedió», sino un cambio de estado y, a menudo, la presencia de una razón. Para un principiante, este es un ejemplo ideal para sentir que el evento es el resultado del escenario, no un «paso interno».

El servicio de cancelación normalmente hace dos cosas: cambia el estado del pedido (a través del modelo de dominio) y guarda el resultado (a través de OrderStore). Y solo después de eso publica el evento «pedido cancelado».

Forma mínima:

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderCancellationService {
    private final OrderStore orderStore;
    private final ApplicationEventPublisher publisher;

    public OrderCancellationService(OrderStore orderStore, ApplicationEventPublisher publisher) {
        this.orderStore = orderStore;
        this.publisher = publisher;
    }

    public void cancel(Order order, String reason) {
        // 1) Primero cambiamos el estado de dominio
        order.cancel(reason);

        // 2) Luego registramos el resultado (para que el evento refleje un hecho ya guardado)
        orderStore.save(order);

        // 3) Y solo entonces publicamos el evento con la carga mínima necesaria (id + razón)
        publisher.publishEvent(new OrderCancelledEvent(this, order.getId(), reason));
    }
}

Aquí es importante que publishEvent() va después de order.cancel(reason) y después de guardar. Para nuestro OrderStore en memoria, «save» parece casi de juguete, pero en el modelo mental del curso juega el papel de «fijación del resultado del escenario». Si tuviéramos una base de datos, también pensaríamos en transacciones, pero eso ya es territorio de los siguientes cursos (y no entramos allí para no hacer una «excursión a disciplinas vecinas»).

Otro matiz: el evento debe llevar datos mínimamente suficientes para que los handlers no tengan que adivinar qué sucedió. Para la cancelación, la razón es una parte importante del hecho. Puede ir a la auditoría y a la notificación. Por lo tanto, poner reason dentro del evento es lógico.

5. Legibilidad del escenario: un hecho — un publishEvent()

Cuando se le coge el gusto a los eventos, aparece la tentación de publicarlos en cada micro-paso. «Guardamos el pedido — evento. Calculamos el precio — evento. Elegimos el canal de notificación — evento. Pasamos al siguiente método — evento». Parece «flexible», pero muy rápidamente se convierte en una «sopa de eventos» en la que es imposible entender qué es realmente un hecho empresarial.

Para mantener el sistema dentro de lo razonable, nos es útil mantener una disciplina simple: en un método de escenario se publica un evento por cada hecho completado. En place() — OrderCreatedEvent. En cancel() — OrderCancelledEvent. Y ya está.

Incluso se puede escribir como un micro-patrón de método de escenario:

Parte del método Qué ocurre aquí ¿Publicamos evento?
Preparación verificación/recolección de datos no
Núcleo del escenario cambio de estado + guardado no (aún es pronto)
Declaración del hecho publishEvent(...) sí
Finalización return / salida no

Si ve dos publishEvent() seguidos en un mismo método, no es necesariamente un error, pero casi siempre es una razón para detenerse y preguntarse: «¿Estoy publicando dos hechos diferentes, o simplemente estoy dividiendo un hecho en pedazos?»

Pequeño truco para la legibilidad: un método privado separado para la publicación

Si la línea publisher.publishEvent(new ...) comienza a interferir con la lectura (por ejemplo, el payload es complejo), se puede extraer a un método privado. Esto no hace magia, solo mejora el «flujo visual» del escenario:

public void place(Order order) {
    // 1) Fijamos el estado
    orderStore.save(order);

    // 2) La publicación la sacamos a un método separado para que place() se lea linealmente
    publishCreated(order);
}

private void publishCreated(Order order) {
    // publishEvent — marcador de un hecho completado
    publisher.publishEvent(new OrderCreatedEvent(this, order.getId(), order.getCustomer().getId()));
}

Este truco es especialmente útil cuando el método del escenario ya contiene varios pasos del núcleo (por ejemplo, pricing, generación de id, guardado) y desea que publishEvent() se lea como un «marcador» separado del hecho completado.

6. Refactorización: reacciones a través de eventos

Ahora damos un importante giro arquitectónico. Antes de los eventos, un servicio típico de creación de pedidos se veía como una «orquesta que toca todos los instrumentos a la vez»: guarda, escribe la auditoría y envía notificaciones. Sí, es más fácil empezar así, pero escala mal: cada nueva reacción añade una nueva dependencia y una nueva línea de código al servicio.

Ejemplo de «cómo era» (muy simplificado, pero reconocible):

public void place(Order order) {
    // El escenario lo sabe todo de inmediato: sobre el almacén, la auditoría y las notificaciones
    orderStore.save(order);

    // Las reacciones están «integradas» en el escenario — el servicio se expande con dependencias
    auditService.recordCreated(order.getId());
    notificationDispatchService.sendCreated(order.getId());
}

Después de introducir los eventos, comenzamos a movernos hacia una forma en la que «el servicio registra el hecho y lo declara», y las reacciones viven por separado:

public void place(Order order) {
    // 1) Fijamos el resultado del escenario
    orderStore.save(order);

    // 2) Declaramos el hecho: luego las reacciones se ejecutarán en oyentes separados
    publisher.publishEvent(new OrderCreatedEvent(this, order.getId(), order.getCustomer().getId()));
}

Tenga en cuenta: en este paso, el servicio de escenarios se vuelve más corto y más honesto. Deja de saber qué reacciones existen en absoluto. Hoy son auditoría y notificaciones, mañana estadísticas, pasado mañana «actualización de un informe simple», y el servicio de escenarios no cambia por eso.

Sí, después de esta refactorización, un principiante tiene una pregunta natural: «¿Dónde están ahora la auditoría y las notificaciones? ¿Han desaparecido?» No han desaparecido. Se mudarán a los listeners, es decir, a beans separados que se suscribirán a OrderCreatedEvent y harán su trabajo. Esto será en la próxima lección, donde comenzaremos a escribir handlers a través de ApplicationListener<T>.

7. Errores típicos de publicación

Error nº1: publicar el evento antes de completar el paso principal.
El problema más común es poner publishEvent() antes de que el pedido se haya guardado realmente o el estado haya cambiado realmente. Desde fuera parece que «todo funciona», hasta que llega una excepción en medio del método. Entonces parte de las reacciones podría haber tenido tiempo de ejecutarse, pero el hecho empresarial no se produjo. Por lo tanto, es mejor mantener la regla de hierro: el evento se publica después de que el núcleo del escenario se haya completado.

Error nº2: inyectar ApplicationContext en lugar de ApplicationEventPublisher.
Para un principiante parece conveniente «tomar el contexto, y luego ya veremos», pero esto es casi siempre el comienzo de un pensamiento de localizador de servicios. En una semana, aparecerá context.getBean(...) en el servicio; en dos, la selección manual de implementaciones; en tres, comenzará a curar la arquitectura «buscando beans». En el servicio de escenarios, inyectamos un publisher estrecho y nos alegramos de que el código se mantenga decente.

Error nº3: publicar el evento desde el modelo de dominio (Order).
A veces da la tentación de hacerlo bonito: «el pedido mismo sabe que está cancelado, que publique él mismo el evento». El problema es que entonces Order tendría que conocer Spring y tener acceso al publisher. Como resultado, el modelo de dominio se vuelve dependiente del framework, peor de probar y peor de portar. En nuestro curso, el dominio sigue siendo Java plano, y la publicación es tarea de los servicios de aplicación.

Error nº4: el evento arrastra «todo» (incluyendo el Order mutable).
Si se coloca un objeto de pedido completo en el evento, y ese objeto luego se modifica en alguna parte, los oyentes podrían ver un «pedido ya diferente» o un estado que cambia durante el proceso. Para un primer curso, es mejor mantener el evento como una pequeña instantánea inmutable del hecho: identificadores y los campos necesarios, sin peso adicional.

Error nº5: varios publishEvent() en un mismo escenario «por si acaso».
Esto suele comenzar con buenas intenciones: «que haya OrderSavedEvent, OrderCreatedEvent y OrderPricedEvent». Pero al final, el lector del código pierde la comprensión de dónde está el hecho empresarial real y dónde solo un paso técnico. En el ContextFlow educativo, publicamos eventos solo para hechos realmente significativos: pedido creado, pedido cancelado. Todo lo demás lo dejamos como código directo dentro del núcleo del escenario.

1
Tarea
Spring Core, nivel 17, lección 2
Bloqueada
Publicación del evento después del guardado del certificado
Publicación del evento después del guardado del certificado
1
Tarea
Spring Core, nivel 17, lección 2
Bloqueada
RecordingEventPublisher como punto de publicación observable
RecordingEventPublisher como punto de publicación observable
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION