Integrar Struts con Spring - TeorÃa
Bueno, ya tenemos un proyecto con Spring - MVC (holamundo) y otro con Struts (holamundoStruts138). Cuál utilizaremos para desarrollar ? Eso lo dejo para otra discusión.
Aquí quiero señalar que existe otra solución, y es integrar Struts con Spring, es decir, utilizar Struts aprovechando sus puntos fuertes como MVC (madurez, documentación, podemos extender el framework con Struts-Validator y Tiles…) y Spring para sus puntos fuertes (como AOP, IoC, DI…).
El framework de Struts realiza una serie de operaciones transparentes para el desarrollador para facilitarle la vida. Entre ellas mantiene un HashMap que contiene referencias a objetos Action ya instanciados. Ante cada petición mira primero este HashMap para ver si contiene la clase Action solicitada. Si es así recupera el objeto a partir de la referencia almacenada en el HashMap; y si no instancia e inicializa la clase Action y guarda su referencia en el HashMap. Al manejar una única instancia de cada Action se ahorra bastante tiempo y el rendimiento de la aplicación mejora notablemente.
Este mecanismo podría ser más eficiente, ya que cada Action invoca a uno o varios objetos de negocio, y con Struts se deben instanciar e inicializar en cada llamada. La integración con Spring perfecciona este mecanismo aprovechando IoC y DI, ya que estos objetos de negocio (utilizados como singletons) ya están instanciados e inicializados en el contendor de Spring.
Si aplicamos IoC y DI también a los Action de Struts conseguimos que las instancias de los Action manejen ya tengan instanciados e inicializados los objetos de negocio, mejorando considerablemente el rendimiento de nuestra aplicación: al levantar la aplicación será un poco lenta pero después irá como un tiro.
Pues manos a la obra. Debemos declarar un bean en Spring que represente nuestro Action para que sea Spring el que le instancie y le inyecte sus dependencias (los objetos de negocio).
Donde gestiona Struts la creación de sus clases Action ? En el método processActionCreate de su controlador, RequestProcessor. Vale, pues sobre escribamos esta clase con nuestro propio controlador, AppRequestProcessor, para que pida la instancia del bean a Spring (la rama true del if) en lugar de crearla Struts (la rama del else). Lo que está en negrita es la clave, sólo un if.
Si queremos utilizar Tiles debemos extender de TilesRequestProcessor.
public class AppRequestProcessor extends RequestProcessor {
. . .
protected Action processActionCreate(HttpServletRequest request,
HttpServletResponse response, ActionMapping mapping)
throws IOException {
// Acquire the Action instance we will be using (if there is one)
String className = mapping.getType();
if (log.isDebugEnabled()) {
log.debug(" Looking for Action instance for class " + className);
}
// If there were a mapping property indicating whether
// an Action were a singleton or not ([true]),
// could we just instantiate and return a new instance here?
Action instance;
//Acceso sincronizado al HasMaps de actions
synchronized (actions) {
// Return any existing Action instance of this class
instance = (Action) actions.get(className);
if (instance != null) {
if (log.isTraceEnabled()) {
log.trace(" Returning existing Action instance");
}
return (instance);
}
// Create and return a new Action instance
if (log.isTraceEnabled()) {
log.trace(" Creating new Action instance");
}
try {
//String beanName = determineActionBeanName(mapping);
String beanName = mapping.getType();
if (getWebApplicationContext().containsBean(beanName))
instance = (Action)getWebApplicationContext().getBean(beanName, Action.class);
else
instance = (Action) RequestUtils.applicationInstance(className);
// Maybe we should propagate this exception
// instead of returning null.
} catch (Exception e) {
log.error(getInternal().getMessage("actionCreate",
mapping.getPath()), e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
getInternal().getMessage("actionCreate", mapping.getPath()));
return (null);
}
actions.put(className, instance);
}
if (instance.getServlet() == null) {
instance.setServlet(this.servlet);
}
return (instance);
}
. . .
}
Es decir, buscamos si el nombre del bean (beanName) existe en el contexto de Spring, y si es así lo obtenemos de ahí. El nombre lo podemos obtener de la ruta (path) mediante determineActionBeanName(mapping), el método comentado. O bien lo podemos obetener del "type", es decir la clase.
En el contexto de Spring debemos declarar nuestros actions. En el primer caso el nombre del bean será la ruta introducida en el navegador. Ojo, que todos los beans del mismo action deben declarar los mismos BO's porque la instancia que se almacenará será la que se instancia primero, es decir por la primera ruta a la que se acceda:
<bean name="parentAction" class="es.xxx.xxx.control.action.base.AppBaseAction" abstract="true" />
<bean name="/editarDatos" class="es.xxx.xxx.control.action.DatosAction" singleton="false" parent="parentAction">
<property name="datosBO"><ref bean="datosBO" /></property>
</bean>
<bean name="/guardarDatos" class="es.xxx.xxx.control.action.DatosAction" singleton="false" parent="parentAction">
<property name="datosBO"><ref bean="datosBO" /></property>
</bean>
<bean name="/validarDatos" class="es.xxx.xxx.control.action.DatosAction" singleton="false" parent="parentAction">
<property name="datosBO"><ref bean="datosBO" /></property>
</bean>
En el segundo caso el nombre del bean debe ser el "tipo" del action :
<bean name="parentAction" class="es.xxx.xxx.control.action.base.AppBaseAction" abstract="true" />
<bean name="es.xxx.xxx.control.action.DatosAction" class="es.xxx.xxx.control.action.DatosAction" singleton="false" parent="parentAction">
<property name="datosBO"><ref bean="datosBO" /></property>
</bean>
En ambos casos se apoyarían sobre un bean de negocio
<bean name="baseBO" class="es.xxx.xxx.negocio.base.BaseBOImpl" abstract="true"/>
<bean id="datosBO" class="es.xxx.xxx.negocio.impl.DatosBOImpl" parent="baseBO">
<property name="datosDAO" ref="datosDAO" />
</bean>
que se apoya sobre otro de persistencia
<!– SqlMap setup for iBATIS Database Layer –>
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="configLocation">
<value>classpath:/es/xxx/xxx/persistencia/impl/xml/sql-map-config.xml</value>
</property>
<property name="dataSource" ref="dataSource"/>
</bean>
<!– Generic Dao - can be used when doing standard CRUD –>
<bean id="dao" class="es.xxx.xxx.persistencia.base.BaseDaoImpl" abstract="true">
<property name="sqlMapClient" ref="sqlMapClient"/>
</bean>
<bean id="datosDAO" class="es.xxx.xxx.persistencia.impl.DatosProyectoDAOImpl" parent="dao"/>
Qué fácil, una vez que se sabe !