1. Dos tareas en la capa de soporte
Si hasta ahora su ContextFlow se percibía como un “servicio de aprendizaje honesto”, hoy agregaremos dos cosas que se parecen mucho a lo que se encuentra en proyectos reales. La primera: elegir la implementación correcta según la configuración sin propagar if/else por los servicios. La segunda: diagnóstico técnico del contexto, pero sin que el código de negocio aprenda a “vivir del contenedor”.
Imaginemos dos tareas en lenguaje humano.
Primera tarea: tenemos una interfaz ReportFormatter y dos implementaciones — digamos, “de texto” y “similar a CSV”. En diferentes perfiles y entornos queremos inyectar un formato de informe diferente, pero de modo que ReportingService siga siendo lo más simple posible: debe depender del contrato, no de la lógica de selección de la implementación.
Segunda tarea: a veces queremos ver rápidamente en qué modo se ha iniciado el contexto. ¿Qué perfiles están activos? ¿Cuál es el nombre de la aplicación? ¿Quizás incluso qué formato de informe se eligió? Y queremos hacerlo de modo que no tengamos que escribir en cada servicio “dame el Environment… y ahora getProperty(...)…”. Para esto, crearemos un pequeño bean de diagnóstico al que el contenedor le proporcionará la información de servicio necesaria.
A continuación, un pequeño “mapa” de soluciones para que se vea quién es responsable de qué:
| Qué queremos hacer | Dónde vive | Por qué exactamente allí |
|---|---|---|
| Elegir la implementación de ReportFormatter según la propiedad contextflow.report.format | support.factory + config.reporting | Es lógica de construcción/creación de objetos, no un escenario de negocio |
| Mostrar un resumen breve sobre el contexto (nombre del bean, perfiles, un par de propiedades) | support.diagnostics + config.core | Es diagnóstico técnico, y se le permite conocer el entorno Spring |
2. ReportFormatter y elección del formato
Antes de agregar el FactoryBean, es importante repetir una idea simple: el servicio debe hacer su trabajo, no jugar a ser el “ensamblador de la aplicación”. Cuando ReportingService comienza a decidir qué implementación del formateador elegir, asume una responsabilidad extra. Al principio parece “qué más da, un if”, y luego de repente se convierte en “cinco if, tres propiedades, dos perfiles y un error que solo aparece los viernes”.
Supongamos que el contrato del formateador se ve así (si ya lo tiene, simplemente compárelo en cuanto al significado):
package com.example.contextflow.domain.ports;
import com.example.contextflow.domain.model.DailyReport;
public interface ReportFormatter {
// Formateamos el informe de dominio en una representación de cadena (el formato concreto lo decide la implementación)
String format(DailyReport report);
}
Las implementaciones normalmente se encuentran en algún lugar de la infraestructura o cerca de la parte de reporting. Por ejemplo, que sea así:
package com.example.contextflow.infrastructure.reporting;
import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;
public class TextReportFormatter implements ReportFormatter {
@Override
public String format(DailyReport report) {
// Formato simple “humano”: apto para logs y consola
return "INFORME DIARIO: " + report;
}
}
Y la segunda:
package com.example.contextflow.infrastructure.reporting;
import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;
public class CsvReportFormatter implements ReportFormatter {
@Override
public String format(DailyReport report) {
// Estilo CSV: primero el encabezado, luego los datos del informe
return """
fecha,numeroPedidos,numeroCancelados,montoTotal
""" + report;
}
}
Ahora el punto clave. ReportingService debería verse más o menos así: recibe un ReportFormatter y simplemente lo usa, sin decidir “qué formateador es exactamente este”. Ejemplo (simplificado al máximo):
package com.example.contextflow.application.reporting;
import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;
public class ReportingService {
private final ReportFormatter formatter;
public ReportingService(ReportFormatter formatter) {
// Importante: la dependencia es del contrato, no de una implementación concreta
this.formatter = formatter;
}
public String format(DailyReport report) {
// Código de negocio “aburrido”: simplemente invoca el contrato
return formatter.format(report);
}
}
Si ahora siente una ligera decepción del tipo “¿y eso es todo?” — felicidades, acaba de ver el objetivo de un buen wiring: el código de negocio se vuelve aburrido. El código de negocio aburrido suele ser un cumplido, no un insulto.
3. ReportFormatterFactoryBean
Ahora nos acercaremos a FactoryBean no como una “API complicada”, sino como una forma de ocultar la lógica de creación/selección donde debe estar: en la infraestructura. Imagine que ReportFormatterFactoryBean es un barista. El barista no es el café en sí, pero es él quien decide si le servirá un latte o un espresso, y entrega el resultado, no a sí mismo.
Crearemos la fábrica en support.factory. Dependerá solo de los contratos y las implementaciones de los formateadores, no de los servicios de negocio.
package com.example.contextflow.support.factory;
import org.springframework.beans.factory.FactoryBean;
import com.example.contextflow.domain.ports.ReportFormatter;
import com.example.contextflow.infrastructure.reporting.CsvReportFormatter;
import com.example.contextflow.infrastructure.reporting.TextReportFormatter;
public class ReportFormatterFactoryBean implements FactoryBean<ReportFormatter> {
// Valor por defecto: si no se especifica la propiedad, se usa “text”
private String format = "text";
public void setFormat(String format) {
// Spring establecerá este valor desde properties a través de la configuración
this.format = format;
}
@Override
public ReportFormatter getObject() {
// Aquí vive la lógica de selección de la implementación concreta (no en el servicio)
return "csv".equalsIgnoreCase(format)
? new CsvReportFormatter()
: new TextReportFormatter();
}
@Override
public Class<?> getObjectType() {
// Informamos al contenedor el tipo del “producto” que produce la fábrica
return ReportFormatter.class;
}
@Override
public boolean isSingleton() {
// Un solo formateador para toda la aplicación — lógico para este caso
return true;
}
}
Aquí hay dos cosas importantes.
Primero, el consumidor sigue viendo solo ReportFormatter: el FactoryBean permanece en support, y hacia afuera el contenedor entrega el resultado de getObject().
Segundo, el contenedor necesita información de servicio sobre el objeto producido — para eso existen getObjectType() e isSingleton(). Gracias a esto, ReportingService no conoce ni TextReportFormatter, ni CsvReportFormatter, ni el propio ReportFormatterFactoryBean. Recibe un único contrato y sigue adelante.
Este es todo el sentido de la construcción: la lógica de selección se ha ido a la infraestructura, y el código de negocio sigue siendo aburrido. Y el código de negocio aburrido, como ya sabemos, suele vivir más tiempo y romperse con menos frecuencia. Y sí, no hace falta escribir un FactoryBean para todo; aquí es apropiado precisamente porque la lógica de selección se ha convertido en una responsabilidad separada de la infraestructura.
4. Configuración y diferencia entre fábrica y producto
Ahora debemos integrar la fábrica en la configuración de modo que el formato se seleccione desde properties. Aquí usamos una técnica ya conocida: @Configuration, @Bean y @Value con valor por defecto. Lo principal es elegir cuidadosamente el nombre del bean, porque en el mundo de FactoryBean el nombre se vuelve especialmente notable.
Crearemos el módulo config.reporting.ReportingConfig:
package com.example.contextflow.config.reporting;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.contextflow.support.factory.ReportFormatterFactoryBean;
@Configuration
public class ReportingConfig {
@Bean
public ReportFormatterFactoryBean reportFormatter(
@Value("${contextflow.report.format:text}") String format) {
// Creamos la fábrica y le pasamos la configuración del formato desde properties
ReportFormatterFactoryBean factory = new ReportFormatterFactoryBean();
factory.setFormat(format);
return factory;
}
}
Observe el nombre del método reportFormatter(...). Para Spring, este nombre se convierte en el nombre del bean. Y aquí comienza lo interesante: desde el punto de vista del contenedor, el bean con nombre reportFormatter es un FactoryBean, pero desde el punto de vista de los consumidores, reportFormatter se comporta como un ReportFormatter.
Es decir, el siguiente código funcionará (y devolverá el objeto producido):
// Obtenemos el producto de la fábrica (el ReportFormatter real), no la fábrica en sí
ReportFormatter formatter = context.getBean("reportFormatter", ReportFormatter.class);
Pero si desea obtener el objeto de la fábrica en sí (por ejemplo, solo para diagnóstico), se usa un prefijo especial &. Sí, es precisamente ese “ampersand mágico” que se necesita exactamente en un lugar: cuando quiere el objeto de la fábrica, no el objeto producido.
// Obtenemos precisamente el objeto de la fábrica (FactoryBean), no el formateador producido
Object factory = context.getBean("&reportFormatter");
Para sentir esto con las manos, podemos hacer una pequeña verificación directamente en main() en la fase de inicio (y este es precisamente el caso en el que getBean() es aceptable: estamos en código de bootstrap/diagnóstico, no en un servicio de negocio):
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.contextflow.config.core.ContextFlowAppConfig;
import com.example.contextflow.domain.ports.ReportFormatter;
try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
// Por el nombre "reportFormatter" obtenemos el producto de la fábrica (TextReportFormatter o CsvReportFormatter)
ReportFormatter formatter = context.getBean("reportFormatter", ReportFormatter.class);
System.out.println(formatter.getClass().getSimpleName()); // TextReportFormatter
// Con el prefijo "&" obtenemos el objeto de la fábrica en sí
Object factory = context.getBean("&reportFormatter");
System.out.println(factory.getClass().getSimpleName()); // ReportFormatterFactoryBean
}
Ahora sobre properties. En el archivo contextflow.properties base (o en un archivo específico de perfil), aparecerá la configuración:
# Por defecto usamos el formato de texto
contextflow.report.format=text
Y, por ejemplo, en contextflow-demo.properties podemos poner:
# En el perfil demo queremos CSV
contextflow.report.format=csv
Y obtendrá algo simple y agradable: el mismo ReportingService permanece sin cambios, mientras que la composición y el comportamiento de la aplicación en cuanto al formateo del informe cambian mediante la configuración.
5. ContextDiagnosticsBean para diagnóstico
Con el diagnóstico mantenemos la misma disciplina. Para el proyecto, fijaremos un infrastructure-bean explícito, registrado a través de config: así, Aware permanece en la capa support y no parece un @Component casual que simplemente fue encontrado por el escaneo.
Esto es útil cuando la aplicación se vuelve profile-aware y property-heavy: al iniciar, ve inmediatamente en qué modo se ha levantado el contexto y qué configuración se ha aplicado realmente. Este bean no debe procesar pedidos ni buscar dependencias. Su función es observar e informar.
Crearemos la clase en support.diagnostics:
package com.example.contextflow.support.diagnostics;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
public class ContextDiagnosticsBean implements BeanNameAware, EnvironmentAware {
private String beanName;
private Environment environment;
@Override
public void setBeanName(String name) {
// El contenedor comunica al bean su nombre
this.beanName = name;
}
@Override
public void setEnvironment(Environment environment) {
// El contenedor proporciona acceso a Environment (profiles + properties)
this.environment = environment;
}
public String summary() {
// Tomamos un par de propiedades con valores por defecto — solo para diagnóstico rápido
String appName = environment.getProperty("contextflow.app-name", "ContextFlow");
String format = environment.getProperty("contextflow.report.format", "text");
String profiles = String.join(",", environment.getActiveProfiles());
if (profiles.isBlank()) {
// Si no hay perfiles activos, consideramos que trabajamos en modo por defecto
profiles = "default";
}
// Devolvemos una cadena, adecuada para el log en el inicio
return "[" + beanName + "] appName=" + appName
+ ", profiles=" + profiles
+ ", report.format=" + format;
}
}
Aquí hemos utilizado dos interfaces Aware que suelen ser las más útiles en el diagnóstico.
BeanNameAware permite al contenedor decirle al bean: “tu nombre es este”. Es cómodo para no perderse en configuraciones grandes (especialmente si se usan alias o nombres explícitos). EnvironmentAware nos proporciona Environment, que conocemos bien desde los días de properties, profiles y configuración externa. Es decir, no nos “hemos metido en el contenedor”, simplemente hemos recibido el objeto de entorno oficialmente proporcionado.
Registraremos el bean de diagnóstico a través de config (de forma explícita, para que se vea que es infraestructura, no un “componente casual encontrado por escaneo”):
package com.example.contextflow.config.core;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.contextflow.support.diagnostics.ContextDiagnosticsBean;
@Configuration
public class DiagnosticsConfig {
@Bean
public ContextDiagnosticsBean contextDiagnosticsBean() {
// Creamos explícitamente el bean de diagnóstico (infraestructura/soporte)
return new ContextDiagnosticsBean();
}
}
En el proyecto fijamos exactamente esta variante: un support-bean separado + registro explícito a través de DiagnosticsConfig.
Y ahora, en el código de bootstrap podemos hacer esto:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.contextflow.config.core.ContextFlowAppConfig;
import com.example.contextflow.support.diagnostics.ContextDiagnosticsBean;
try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
// Esto es código bootstrap/diagnóstico: getBean() es adecuado aquí
ContextDiagnosticsBean diag = context.getBean(ContextDiagnosticsBean.class);
System.out.println(diag.summary());
// [contextDiagnosticsBean] appName=ContextFlow, profiles=dev, report.format=text
}
Esta línea al inicio de la ejecución es como una etiqueta en la caja: “dentro está exactamente lo que quería”. Y sí, a veces esto ahorra más tiempo que el depurador más inteligente: el depurador es bueno, pero no puede leer sus pensamientos sobre qué perfil ha “activado definitivamente”.
6. Panorama general y estructura de paquetes
Ahora reunamos todo en una imagen clara: dónde están estas clases, quién conoce a quién, y por qué esto no convierte a ContextFlow en una “aplicación que solo piensa en Spring”. En este punto, es útil ver el proyecto como un mapa de la ciudad: las calles de negocio están en domain y application, las comunicaciones técnicas en support y config.
Si nos basamos en la estructura objetivo del curso, las partes que nos interesan se ven así:
com.example.contextflow
├── application
│ └── reporting
│ └── ReportingService
├── domain
│ └── ports
│ └── ReportFormatter
├── infrastructure
│ └── reporting
│ ├── TextReportFormatter
│ └── CsvReportFormatter
├── support
│ ├── factory
│ │ └── ReportFormatterFactoryBean
│ └── diagnostics
│ └── ContextDiagnosticsBean
└── config
├── core
│ ├── DiagnosticsConfig
│ └── ContextFlowAppConfig
└── reporting
└── ReportingConfig
Para que esto se asiente aún mejor, aquí hay un pequeño diagrama de dependencias (sin “flechas innecesarias”):
flowchart TD RS["ReportingService
(application)"] --> RF["ReportFormatter
(port)"] subgraph Support["capa support"] FBF["ReportFormatterFactoryBean
(FactoryBean)"] -->|"getObject()"| RF DIAG["ContextDiagnosticsBean
(Aware)"] end subgraph Config["capa config"] RC["ReportingConfig"] --> FBF DC["DiagnosticsConfig"] --> DIAG end ENV["Environment
(properties + profiles)"] --> RC ENV --> DIAG
Observe el útil “silencio” en este diagrama: ReportingService no conoce ni la fábrica, ni Environment, ni ApplicationContext. Solo conoce ReportFormatter, y ese es precisamente el estado al que aspiramos.
Y ContextDiagnosticsBean conoce Environment, pero esto es normal, porque su trabajo es el diagnóstico del entorno. No elige el formato del informe, lo muestra. La elección la hace la configuración + la fábrica.
En la práctica, verá un efecto similar al siguiente: al iniciar la aplicación en el perfil dev, el diagnóstico dirá report.format=text, y al iniciar en demo, report.format=csv. Y ReportingService seguirá siendo la misma clase, sin una sola línea de “si demo, entonces…”.
Esta es la instantánea funcional de ContextFlow: la selección del formateador y el diagnóstico viven en support/config, mientras que ReportingService sigue siendo un consumidor normal de ReportFormatter.
7. Errores típicos con FactoryBean y Aware
Error n.º 1: hacer que ReportingService dependa de ReportFormatterFactoryBean.
Es muy tentador: “ya que la fábrica sabe hacer un formateador, voy a inyectarla”. Pero entonces su servicio comienza a conocer el mecanismo del contenedor. Aparecerá el conocimiento “tengo una fábrica”, y por lo tanto perderá el principal beneficio de FactoryBean: el consumidor debe ver solo el objeto producido (ReportFormatter) y vivir una vida tranquila.
Error n.º 2: no entender que por el nombre del bean se obtiene el objeto producido, no la fábrica.
Situación clásica: escribe context.getBean("reportFormatter"), espera la fábrica, y obtiene TextReportFormatter. Esto no es que “Spring se haya roto”, es su lógica normal para FactoryBean. Si realmente necesita el objeto de la fábrica, use &reportFormatter. Y trate de que este truco se quede en el diagnóstico/infraestructura, no se traslade a la capa de negocio.
Error n.º 3: olvidar implementar correctamente getObjectType() y obtener extraños errores de wiring.
Si getObjectType() devuelve un tipo demasiado general, null o algo incorrecto, al contenedor le resulta más difícil realizar el autowiring y el diagnóstico. En un proyecto de aprendizaje, esto se manifestará como “por qué el contenedor no ve mi ReportFormatter”, y en un proyecto real, como “por qué esto solo funciona en un perfil”. Mantenga este método honesto: si la fábrica produce un ReportFormatter, que lo diga así.
Error n.º 4: usar Aware como sustituto de la inyección por constructor.
Aware no es una “inyección más potente”, es un canal especial para información del contenedor. En cuanto empiece a usar ApplicationContextAware en un servicio normal “para no escribir un constructor”, se estará cortando la transparencia de las dependencias y volviendo a un grafo oculto. Al final, la clase parece simple, pero por dentro tiene acceso oculto al mundo.
Error n.º 5: convertir ContextDiagnosticsBean en un “servicio de negocio con derechos de administrador”.
A veces, el bean de diagnóstico comienza a acumular métodos como “vamos a crear un pedido”, “vamos a cambiar el formato del informe sobre la marcha”. Eso ya no es diagnóstico, es una segunda capa de la aplicación con una responsabilidad poco clara. El bean de diagnóstico debe ser aburrido: leyó un par de propiedades, devolvió una cadena, como máximo verificó la presencia de alguna condición.
GO TO FULL VERSION