Pages

mardi 23 septembre 2014

Comment migrer une application Spring configurée en XML à une configuration en Java

J’ai fait dernièrement une présentation pour migrer des applications Spring configurées en XML vers une configuration en Java. J’ai déjà parler de Java Config Spring dans un article mais je publie ici des exemples où je compare ce que l’on fait en XML et ce que l’on peut faire en Java. Tous ces exemples sont disponibles sous Github.


Posons-nous la question de savoir quel est l’intérêt de déporter la configuration en Java ?
Le XML est un langage de description et non un langage de programmation. Configurer une application en Java permet de bénéficier de tous les petits plus du langage. Les objets de configuration sont des classes et nous pouvons donc profiter pleinement des fonctionnalités orientées objet du langage. Une classe de configuration peut dériver d’une autre, surcharger des méthodes de déclaration de beans,...

Quand les annotations sont arrivées en Java, Spring framework en a profité pour simplifier la manière de configurer une application Spring. Par exemple avec le scan des ressources et les annotations @Repository, @Service, @Component… nous n’avions plus besoin de définir tous les beans en XML. La configuration des applications Spring s’est donc répartie au fur et à mesure entre le code Java et le XML. La Java Config permet de réunifier le tout et de ne faire plus que du Java.

Les travaux sur la configuration Java ont débuté en 2005 chez Spring, peu de temps après la sortie de Java 5. Pour rappel Java 5 apportait les annotations et les generics. Ce sous projet a été intégré dans le coeur de Spring dès Spring 3.0 et depuis la version 4.0 et l’arrivée de Spring Boot, je vous conseille fortement de migrer.

Configurer un bean Spring
Sans Java Config vous devez déclarer un fichier XML

<?xml version="1.0" encoding="UTF-8"?>
<beans ... >
    <bean name="articleBlog" class="com.javamind.dto.ArticleBlog"/>
</beans>

Ensuite pour charger ce bean dans votre code vous devez charger le context spring comme ci-dessous.

ApplicationContext ctx = new ClassPathXmlApplicationContext("/spring.xml");
       assertThat(ctx.getBean(ArticleBlog.class)).isNotNull();


Maintenant vous pouvez changer le fichier XML par une classe annotée par @Configuration. Les méthodes annotées par @Bean permettent d’initialiser vos beans.

@Configuration
public class JavaConfigSimple {
    @Bean
    public ArticleBlog articleBlog(){
        return new ArticleBlog();
    }
}

Au niveau du chargement du contexte nous faites de cette manière

ApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfigNameAlias.class);

Scan de packages
Nous avons pris l’habitude dans les applications Spring, d’annoter nos différents beans avec les annotations @Component, @Repository, @Service…

Au niveau XML nous devons indiquer à Spring quels packages scannés

<beans ... >
    <context:component-scan base-package="com.javamind"/>
</beans>

En Javaconfig vous pouvez utiliser l’annotation @ComponentScan

@Configuration
@ComponentScan("com.javamind.autoscan")
public class JavaConfigAutoScan {

}

Importer d’autres configurations
Vous pouvez découper votre configuration en plusieurs classes. Un bon découpage peut vous aider par exemple à mieux tester unitairement chaque couche de votre application. Si je souhaite importer les deux premiers beans de configuration dans un troisième.

En XML nous faisons comme ci dessous

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation=
               "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <import resource="classpath:/spring.xml"/>
    <import resource="classpath:/spring-scan.xml"/>
</beans>


En Java config nous devons utiliser l’annotation @Import

@Configuration
@Import({JavaConfigSimple.class, JavaConfigAutoScan.class})
public class JavaConfigImport {

}

Mais vous pouvez aussi importer de la Javaconfig dans un fichier de context XML

<beans ...>
    <context:annotation-config/>
    <import resource="classpath:/spring.xml"/>
    <bean class="com.javamind.JavaConfigAutoScan"/>
</beans>

et inversement un fichier xml dans un bean de configuration grace à l’annotation @ImportResource

@Configuration
@Import({JavaConfigSimple.class})
@ImportResource(value = "classpath:spring-scan.xml")
public class JavaConfigImportCroise {

}

Modifier le scope des beans
Par défaut les beans créés par Spring sont des Singletons. Si vous voulez avoir une nouvelle instance à chaque fois que vous faites un @Autowired vous devez changer le scope par défaut.

En XML

<beans ...>
    <bean name="articleBlog" class="com.javamind.dto.ArticleBlog" scope="prototype"/>
    <bean name="blog" class="com.javamind.dto.Blog"/>
</beans>

En Java config

@Configuration
public class JavaConfigScope {
    @Bean
    @Scope(value = BeanDefinition.SCOPE_PROTOTYPE)
    public ArticleBlog articleBlog(){
        return new ArticleBlog();
    }

    @Bean
    public Blog blog(){
        return new Blog();
    }
}

Injection par nom de beans
Spring est réputé pour pouvoir faire de l’injection par type ou par nom. Nous pouvons utiliser des alias

<beans ...>
    <bean name="articleBlog" class="com.javamind.dto.ArticleBlog"/>
    <alias name="articleBlog" alias="monalias"/>
</beans>


Pour définir un nom à un bean vous pouvez utiliser la propriété name de l’annotation @Bean. Vous pouvez spécifier plusieurs noms. Le premier est le nom principal et ceux d’après sont des alias.

@Configuration
public class JavaConfigNameAlias {
    @Bean(name={"articleBlog", "monalias"})
    public ArticleBlog articleBlog(){
        return new ArticleBlog();
    }
}

Injection de dépendances
Nous ne pouvons pas parler de Spring sans parler de l’injection des indépendances. Tous les beans que nous définissons peuvent être injectés dans nos objets via les annotations @Autowired, @Resource, @Inject…

Mais quand est-il des dépendances dans nos beans de configuration. Pour initialiser un bean nous avons parfois besoin de plusieurs dépendances. Prenons un exemple

public class FavoriteArticle {
    private ArticleBlog articleBlog;
    private ArticleBlog articleBlogSecond;

    public FavoriteArticle(ArticleBlog articleBlog) {
        this.articleBlog = articleBlog;
    }

    public ArticleBlog getArticleBlog() {
        return articleBlog;
    }

    public ArticleBlog getArticleBlogSecond() {
        return articleBlogSecond;
    }

    public void setArticleBlogSecond(ArticleBlog articleBlogSecond) {
        this.articleBlogSecond = articleBlogSecond;
    }
}

En XML nous ferions de cette manière pour initier le bean

<beans ...>

    <bean name="articleBlog" class="com.javamind.dto.ArticleBlog" scope="prototype"/>

    <!-- Injection par constructeur -->
    <bean name="favorite" class="com.javamind.dto.FavoriteArticle">
        <constructor-arg ref="articleBlog"></constructor-arg>
        <!-- Injection par setter -->
        <property name="articleBlogSecond" ref="articleBlog"/>
    </bean>


</beans>

En Java Config si le bean est construit dans la même classe nous pouvons procéder de cette manière

@Configuration
public class JavaConfigDependance {
    @Bean
    @Scope(value = BeanDefinition.SCOPE_PROTOTYPE)
    public ArticleBlog articleBlog(){
        return new ArticleBlog();
    }

    @Bean
    public FavoriteArticle favoriteArticle(){
        FavoriteArticle article = new FavoriteArticle(articleBlog());
        article.setArticleBlogSecond(articleBlog());
        return article;
    }
}

S’il est défini dans un autre bean de configuration nous pouvons par exemple écrire

@Configuration
@Import(JavaConfigScope.class)
public class JavaConfigDependanceExterne {

    @Bean
    public FavoriteArticle favoriteArticle(ArticleBlog article1, ArticleBlog article2){
        FavoriteArticle article = new FavoriteArticle(article1);
        article.setArticleBlogSecond(article2);
        return article;
    }
}

Les profils
Lorsque nous avons des spécificités propres à des contextes spécifiques (environnement, test…), Spring met à disposition les profils.

Regardons comment un profil est défini en XML. Ici un nous avons deux définitions de bean en fonction d'un profil dev ou prod

<beans ...>

    <beans profile="dev">
        <bean name="articleBlogDev" class="com.javamind.dto.ArticleBlog">
            <property name="title" value="Mon article de dev"/>
        </bean>
    </beans>

    <beans profile="prod">
        <bean name="articleBlogProd" class="com.javamind.dto.ArticleBlog">
            <property name="title" value="Mon article de prod"/>
        </bean>
    </beans>
</beans>

En configuration Java nous devons utiliser l’annotation @Profile

@Configuration
public class JavaConfigProfile {
    @Bean
    @Profile("dev")
    public ArticleBlog articleBlogDev(){
        return new ArticleBlog().setTitle("Mon article de dev");
    }

    @Bean
    @Profile("prod")
    public ArticleBlog articleBlogProd(){
        return new ArticleBlog().setTitle("Mon article de prod");
    }
}

Ensuite c’est au niveau du chargement du contexte Spring, que nous devons agir en indiquant quel profil charger par défaut. Vous pouvez passer les profils en ligne de commande via la propriété -Dspring.profiles.active="dev"

System.setProperty("spring.profiles.active", "prod");
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfigProfile.class);
assertThat(ctx.getBean(ArticleBlog.class).getTitle()).isEqualTo("Mon article de prod");

ou aussi les préciser à l’initialisation du contexte.

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(JavaConfigProfile.class);
ctx.getEnvironment().addActiveProfile("prod");
ctx.refresh();

Dans le code ci dessus nous n’avons pas utiliser le constructeur de AnnotationConfigApplicationContext pour injecter les beans de configuration. En effet nous ne pouvons faire qu’un seul refresh de context et il y en a un par défaut dans le constructeur. Si vous ne respectez pas cette syntaxe vous aurez une exception

java.lang.IllegalStateException: GenericApplicationContext does not support multiple refresh attempts

Récupérer des variables
Lors de la phase de configuration nous avons souvent besoin d’aller chercher des variables définies dans un fichier de ressources, ou définies au niveau de l’environnement. Spring propose un objet Environment qui va vous faciliter la vie.

Vous pouvez attacher un fichier contenant des propriétés à votre bean de configuration en utilisant l’annotation @PropertySource

@Configuration
@PropertySource(value="classpath:application.properties")
public class JavaConfigProperty {
    @Resource
    private Environment env;

    @Bean
    public ArticleBlog articleBlog(){
        return new ArticleBlog()
                .setTitle(env.getRequiredProperty("article.name"))
                .setVersion(env.getProperty("article.version", "1.0"));
    }
}

Dans ce code je charge le fichier de propriétés application.properties et je récupère la variable obligatoire (erreur si non présente) nommée article.name et une autre facultative nommée article.version. Notez qu’une valeur par défaut est précisée si la propriété non obligatoire n’est pas spécifiée. Ce procédé vous permet de proproser une convention de configuration qui ne sera surchargée qu’au besoin.

Pour aller plus loin
J’espère avoir montrer que le passage d’un type de configuration à un autre n’est pas très compliqué. Vous pouvez le faire au fur et à mesure sans avoir à tout changer d’un coup. Pour plus d’iinformation vous pouvez lire la documentation de Spring. Si vous voulez aussi vous passez du fichier web.xml je vous laisse lire mon article sur le sujet.

Toutes les sources montrées ici et les tests unitaires associés sont disponibles sous Github.

Aucun commentaire:

Enregistrer un commentaire

Remarque : Seul un membre de ce blog est autorisé à enregistrer un commentaire.