Utiliser les services Spring IOC dans des Actions Struts 2.1 avec l’Autocablage (Autowiring)

Nous allons voir ici comment injecter un service Spring dans une Action Struts. Avant de tomber dans le technique pur, nous allons d’abord rappeler l’utilité de ces deux briques et pourquoi est il utile de les faire travailler ensemble. Ensuite, je vous montrerai le code que j’ai écris pour mettre en place un projet Java EE avec Struts/Spring/Hibernate/JPA/MySQL/Log4J.

Apache Struts 2.1

Struts + Webwork = Struts 2Struts est un framework taillé pour concevoir des applications web basé sur une architecture Model-View-Controler (ou MVC). L’utilisation d’une telle architecture permet d’avoir une séparation du code entre les vues qui s’affichent, le magasin de données et la partie logique pure. Cette séparation apporte une meilleure maintenabilité et modularité de l’application. Struts est aussi une boite à outils pour développeur, il facilite notamment la gestion et la validation des formulaires web.

La programmation de Struts tourne principalement autour de classes appelées Action. En effet une Action est partie logique du framework MVC. Elle fait appel au magasin de données et fournit les éléments que la vue doit afficher.

Struts (géré par la fondation Apache) a beaucoup évolué ces derniers temps. Un gros split technique a été effectué entre la version de Struts 1.2 et de Struts 2.x. En effet celle version est née de la fusion des projets Struts 1.x et Webworks. Lorsque vous rechercher des infos sur Internet sur Struts, vérifiez bien la version, car l’utilisation des différentes versions est assez différente.

Spring IOC

SpringSpring est certainement le framework java open source le plus en vue du moment. Spring est divisé en plusieurs modules, mais son plus utilisé et plus célèbre est Spring Inversion Of Control.

L’inversion de contrôle (Inversion of Control, IoC) est un patron d’architecture commun à tous les frameworks (ou cadre de développement et d’exécution). Il fonctionne selon le principe que le flot d’exécution d’un logiciel n’est plus sous le contrôle direct de l’application elle-même mais du framework ou de la couche logicielle sous-jacente.
L’inversion de contrôle est un terme générique. Selon la problématique, il existe différentes formes, ou représentation d’IoC. Le plus connu étant l’injection de dépendances (dependency injection) qui est un patron de conception permettant, en programmation orientée objet, de découpler les dépendances entre objets.
Source: Wikipedia

Pourquoi faire travailler Spring et Struts ensemble?

Work TogetherSpring offre une couche d’abstraction pour accéder aux données, notamment JPA, Hibernate, iBATIS, ou directement JDBC. Cette couche d’abstraction Spring permet une meilleure modularité des couches « purement » techniques de notre projet et réduit les dépendances entre modules.

Travailler avec un service Spring permettra au développeur de faire abstraction des propriétés techniques du magasin de données sous jacent.

Intégrer Struts & Spring… Nous y voila

Prérequis

Struts et Spring doivent déjà être configuré séparément. Vous avez déjà du mettre à jour web.xml (le fichier de configuration propre à Java EE), struts-config.xml (le fichier de configuration propre à Struts) et application-context.xml (le fichier de configuration propre à Spring).

Par exemple dans mon projet Struts/Spring/Hibernate/MySQL/Log4J, je dispose des JAR suivants:

list of package

Mon fichier web.xml contient:

Attention, l’ordre de déclaration des différents filtres est important dans les fichiers web.xml.

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="diiloncswebserver" version="2.5" xmlns="http://java.sun.com/xml/ns/j2ee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd">
	<display-name>DiiLoNcsWebServer</display-name>
 
 
	<!-- Log4J -->
	<context-param>
		<param-name>log4jConfigLocation</param-name>
		<param-value>/WEB-INF/log4j.xml</param-value>
	</context-param>
	<listener>
		<listener-class>org.springframework.web.util.Log4jConfigListener
		</listener-class>
	</listener>
 
 
	<!--
		Spring/Jpa: We can use Lazy Loading with JPA (declare before Strut2)
	-->
	<filter>
		<filter-name>jpaFilter</filter-name>
		<filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
		</filter-class>
	</filter>
 
	<filter-mapping>
		<filter-name>jpaFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
 
	<!-- Struts2 -->
	<filter>
		<filter-name>struts2</filter-name>
		<filter-class>org.apache.struts2.dispatcher.FilterDispatcher
		</filter-class>
	</filter>
 
	<filter-mapping>
		<filter-name>struts2</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
 
 
	<!-- Spring -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener
		</listener-class>
	</listener>
 
	<!-- Tiles 2 -->
	<listener>
		<listener-class>org.apache.struts2.tiles.StrutsTilesListener
		</listener-class>
	</listener>
	<context-param>
		<param-name>org.apache.tiles.impl.BasicTilesContainer.DEFINITIONS_CONFIG
		</param-name>
		<param-value>/WEB-INF/tiles/tiles-backoffice.xml
        </param-value>
	</context-param>
 
	<!-- It redirects to a default action -->
	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>
 
</web-app>

Mon fichier struts.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
 
	<!-- Link Struts to Spring -->
	<constant name="struts.objectFactory" value="spring" />
 
 
	<!-- On definira ici nos Actions Struts -->
</struts>

On notera surtout l’activation de Spring via:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Mon fichier applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
 
	<!-- Load the Jdbc Configuration File, the configuration of the database is in a file named jdbc.properties -->
	<bean id="propertyConfigurer"
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<list>
				<value>classpath:jdbc.properties</value>
			</list>
		</property>
	</bean>
 
	<!-- traduction des exceptions -->
	<bean
		class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
 
 
	<!--  -->
	<bean
		class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
 
	<!-- Declare The Services -->
	<!-- Nous ne déclarons qu'un Service: accountService -->
	<bean id="accountService"
		class="com.diilo.ncs.skeleton.commondata.entities.implementations.AccountImpl">
		<property name="entityManagerFactory" ref="entityManagerFactory" />
	</bean>
 
 
	<!-- Database connection -->
	<bean id="entityManagerFactory"
		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="jpaVendorAdapter">
			<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
				<property name="database" value="MYSQL" />
				<property name="showSql" value="false" />
				<property name="generateDdl" value="true" />
			</bean>
		</property>
	</bean>
 
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
	</bean>
 
	<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
		<property name="entityManagerFactory" ref="entityManagerFactory" />
	</bean>
 
	<tx:annotation-driven transaction-manager="transactionManager" />
 
</beans>

Notre fichier jdbc.properties a la forme

#Driver JDBC
jdbc.driverClassName=org.gjt.mm.mysql.Driver
#Mysql Server
jdbc.url=jdbc:mysql://server/database
#MySQL Account
jdbc.username=notre login
#MySQL Password
jdbc.password=notre password

N’oubliez pas déclarer aussi le fichier persistence.xml, voir orm.xml

L’appel de service Spring dans une Action.

C’est là que ca devient le plus drôle. Car il n’y a vraiment rien à faire! Il y a une longue étape de réglages qui n’est pas facile à mettre en place.

Par défaut, l’injection du service se fait par « Autowiring by name » ou « Autocablage par nom » dans la langue de Moliere. L’autocâblage par nom
fonctionne en faisant correspondre un bean géré par Spring avec avec un champ exposé par des getters/setters dans une Action.

Par exemple, nous avons défini un service Spring accountService dans notre fichier applicationContext.xml. Celui sert à gérer des comptes d’utilisateurs.

Un cas classique de définition de service ressemblerait à celui ci.

/**
 * Service to access AccountBean data.
 * @author Eric VIALLE
 */
public interface AccountService {
 
	/** Find a account in function of the login. */
	public AccountBean findByLogin(String login);
 
	/** Find a account in function of the email. */
	public AccountBean findByEmail(String email);
 
	/** Create a new account in the database. */
	public void save(final AccountBean account);
 
	/** Update an account in the database. */
	public AccountBean update(final AccountBean account);
 
	/** Delete an account in the database. */
	public void delete(final AccountBean account);
 
	/** Refresh the bean with the data of the database. */
	public void refresh(final AccountBean account);
}

Un exemple classique d’implementation Java d’un service Spring.

/**
 *
 */
package com.diilo.ncs.skeleton.commondata.entities.implementations;
 
import java.util.List;
 
import org.springframework.orm.jpa.support.JpaDaoSupport;
import org.springframework.transaction.annotation.Transactional;
 
import com.diilo.ncs.skeleton.commondata.entities.beans.AccountBean;
import com.diilo.ncs.skeleton.commondata.entities.service.AccountService;
 
/**
 * Implementation of the Service to access Account data.
 *
 * @author Eric VIALLE
 */
@Transactional
public class AccountImpl extends JpaDaoSupport implements AccountService {
 
	@Override
	public void delete(final AccountBean account) {
		getJpaTemplate().remove(account);
	}
 
	@Override
	public AccountBean findByLogin(final String login) {
		return getJpaTemplate().find(AccountBean.class, login);
	}
 
	@Override
	public void save(final AccountBean account) {
		getJpaTemplate().persist(account);
	}
 
	@Override
	public AccountBean update(final AccountBean account) {
		return getJpaTemplate().merge(account);
	}
 
	@Override
	public AccountBean findByEmail(final String email) {
		List accountList = getJpaTemplate().find("select a from AccountBean a where a.email = ?1", email);
		if ((accountList == null) || (accountList.size() == 0)) {
			return null;
		} else {
			return accountList.get(0);
		}
	}
 
	@Override
	public void refresh(final AccountBean account) {
		getJpaTemplate().refresh(account);
 
	}
 
}

Et notre action

import org.apache.struts2.interceptor.validation.SkipValidation;
 
import com.diilo.ncs.skeleton.commondata.entities.beans.AccountBean;
import com.diilo.ncs.skeleton.commondata.entities.service.AccountService;
import com.opensymphony.xwork2.ActionSupport;
 
/**
 * Example of Application with Spring's injection of accountService
 * 
 * @author Eric VIALLE
 * 
 */
public class LoginExampleAction extends ActionSupport {
 
	/**
	 * Spring Service supplying access to Account. Nous avons déclarer ce nom
	 * dans applicationContext.xml
	 */
	private AccountService	accountService;
 
	/**
	 * @return the accountService
	 */
	public AccountService getAccountService() {
		return accountService;
	}
 
	/**
	 * @param accountService
	 *            the accountService to set
	 */
	public void setAccountService(AccountService accountService) {
		this.accountService = accountService;
	}
 
	/**
	 * Main method of this action. We just show the formular
	 * 
	 * @return INPUT
	 */
	@SkipValidation
	public String execute() {
		// Le service AccountService a été
		// automatiquement injecté lors de l'instanciation de classe
		// LoginExampleAction
		AccountBean account = getAccountService().findByLogin("eric");
 
		return SUCCESS;
	}
 
}

Le framework Struts a reconnu automatiquement le champ accountService comme un service Spring et a appelé son setter afin d’injecter le bon service.

Pour conclure

L’Autocablage est extremement pratique fait gagner beaucoup de temps au développeur. Néanmoins, si ce n’est pas vous qui faite le travail, il faut bien que quelqu’un le fasse. En l’occurrence, il s’agit du framework. Cette méthode d’autocablage est donc moins couteuse en ressource humaine mais plus couteuse en resources materielle qu’une méthode de cablage manuel.

Sources: