
Dans certaines situations, nous souhaitons pouvoir redéfinir (décorer) un bean spring défini dans un composant externe non modifiable (ou non souhaitable de le modifier).
C'est le cas d'un projet complexe multi-modules, où nous voulons, pour les tests d'intégration, redéfinir des beans dataSource sans dupliquer ni le code ni xml.
Cela est possible avec spring via l'annotation @Bean (ou le tag <alias) en précisant plusieurs valeurs à l'attribut "name". Nous donnons plus de détails ci-après.
Cette notion est connue sous le nom "aliasing bean". Hélas elle n'est pas bien documentée dans la documentation de référence(§ 6.12.3)
Passons à un exemple concret afin de bien illustrer cette notion simple d'aliasing bean. Nous utilisons Spring 4 avec une configuration basée sur java, spring-boot 1.3.3.RELEASE et java 8.
Démarrons avec un projet spring-boot maven nommé "moduleExterne". C'est un projet java standard définissant un simple bean nommé "serviceCopyright" qui renvoie des informations de copyright.
L'exemple est loin d'être réaliste mais sert bien à illustrer la notion d'alias.
Le second projet spring-boot nommé "demoAliasSpring" ayant une dépendance sur "moduleExterne".
Notre but est de redéfinir (reconfigurer), dans le second projet, le bean "serviceCopyright" initialement configuré dans "moduleExetrne" sans, bien sûr, retoucher le code de "moduleExterne".
Pour ce faire, nous définissons un (client) Rest Controller faisant appel au service reconfiguré.
Le test d'intégration associé permet d'illustrer le nouveau comportement du bean une fois l'alias ajouté.
ACTE 1. PREMIER PROJET
Générons un projet spring-boot depuis la page initializr puis vérifiez que le pom correspond uniquement aux lignes ci-dessous.
- Pom du projet "moduleExterne"
?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.abdou</groupId>
<artifactId>moduleExterne</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>moduleExterne</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
pom spring-boot
La seule chose à noter en dehors de java 8 est la présence d'une seule dépendance "spring-boot-starter".
- Configurez le bean "serviceCopyright"
La classe java ConfigApp sert à configurer notre bean "serviceCopyright" comme suit:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfigApp {
@Bean(name="serviceCopyright")
public IServiceCopyright serviceCopyright(){
return new ServiceCopyrightImpl();
}
}
configApp
L'interface IServiceCopyright est très simple:
public interface IServiceCopyright {
void setCopyright(String infos);
String getCopyright();
}
Et son implémentation est ridicule puisqu'elle retourne une chaîne "Netapsys 2015".
import static fr.abdou.IConstantes.*;
public class ServiceCopyrightImpl implements IServiceCopyright{
private String copyright=COPYRIGHT_2015;
public String getCopyright() {
return copyright;
}
public void setCopyright(String copyright) {
this.copyright = copyright;
}
}
La constante COPYRIGHT_2015 est valorisée dans IConstantes à "Netapsys 2015".
Pour tester ce bean, nous écrivons un test unitaire simple comme suit:
import static fr.abdou.IConstantes.COPYRIGHT_2015;
import static org.junit.Assert.assertEquals;
import org.junit.*;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {ConfigApp.class})
public class ServiceCopyrightTest {
@Autowired IServiceCopyright serviceCopyright;
@Test
public void test() {
assertEquals(COPYRIGHT_2015,serviceCopyright.getCopyright());
}
}
Test Junit
Lancez mvn test pour constater que tout va bien.
La commande mvn install produit le jar nécessaire à l'étape suivante.
Nous allons maintenant utiliser ce jar comme dépendance dans le second projet.
ACTE 2. SECOND PROJET
C'est aussi un projet maven spring-boot dont voici son pom:
Pom projet
....
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
<relativePath/>
</parent>
<groupId>fr.abdou.alias</groupId>
<artifactId>demoAliasSpring</artifactId>
<packaging>war</packaging>
<name>demoAliasSpring</name>
<description>demoAliasSpring</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
pom
Dépendance supplémentaire
Nous complétons le pom avec cette dépendance sur moduleExterne précédent:
<dependency>
<groupId>fr.abdou</groupId>
<artifactId>moduleExterne</artifactId>
<version>1.0.0</version>
</dependency>
Configurer l'alias
Comme notre but est de redéfinir le bean serviceCopyright sans retoucher le code de modulExterne, nous allons, via l'annotation @Bean créer un bean alias, permettant de reconfigurer le comportement du bean.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import fr.abdou.IServiceCopyright;
import fr.abdou.ServiceCopyrightImpl;
@Configuration
public class ConfigAppAlias {
@Bean(name={"serviceCopyright2016","serviceCopyright"})
public IServiceCopyright serviceCopyright2016(){
IServiceCopyright serviceCopyright = new ServiceCopyrightImpl();
serviceCopyright.setCopyright("2016-2017 © Netapsys");
return serviceCopyright;
}
}
C'est ici la partie la plus intéressante de cet article.
L'annotation @Bean précise l'attribut name avec un tableau de chaînes.
A partir de là, toute référence à un bean ayant pour nom="serviceCopyright" sera (décoré) remplacé par le bean redéfini ici.
Et tout cela sans rien changer dans les différents jar du (gros) projet.
L'exemple typique que l'on trouve dans la documentation est la redéfinition de dataSource.
Passons maintenant à l'écriture d'un client (ici Rest Controller) du bean "serviceCopyright".
Rest Controller (client du service)
Le code de la classe est simple. L'url localhost:8080/services permet de tester la seule méthode du controller.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import fr.abdou.IServiceCopyright;
@RestController
@RequestMapping("/services")
public class DemoController {
@Autowired @Qualifier("serviceCopyright")
IServiceCopyright serviceCopyright;
@RequestMapping(method=RequestMethod.GET)
public String afficheAliasCopyright() throws SQLException {
return serviceCopyright.getCopyright();
}
Rest
Adapter la classe main
Avant de passer au test d'intégration, il faudrait penser à exécuter la classe main dont voici le code légèrement adapté en ajoutant la ligne @Import au code généré par spring-boot.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
import fr.abdou.ConfigApp;
@SpringBootApplication
@Import(value=ConfigApp.class)
public class DemoAliasSpringApplication {
public static void main(String[] args) {
SpringApplication.run(DemoAliasSpringApplication.class, args);
}
}
Main
Test d'intégration
Le code du test d'intégration est :
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.client.RestTemplate;
import fr.abdou.ConfigApp;
import fr.abdou.aliasspring.ConfigAppAlias;
import fr.abdou.aliasspring.DemoController;
import static fr.abdou.aliasspring.IConstantesAlias.*;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {ConfigApp.class, ConfigAppAlias.class,
DemoController.class, RestTemplate.class })
public class TestRestController4AliasBean {
private String port="8080";
private String host="localhost";
@Autowired
private RestTemplate restTemplate;
@Test
public void testRest() throws Exception {
String retour = restTemplate.getForObject("http://"+ host + ":" + port + "/services", String.class);
Assert.assertEquals(retour,COPYRIGHT_2016);
}
}
Test integ
Pour info, j'ai valorisé la constante COPYRIGHT_2016 = "2016-2017 © Netapsys".
CONCLUSION
Pour certaines situations, cette option permet de décorer le bean défini ailleurs (composants externes non modifiables) et d'écrire un code propre sans duplication s'adaptant facilement à la plateforme cible ou au contexte.
Combiné avec l'annotation conditionnelle @Profile, spring offre une solution complète permettant, sans peine, de cibler correctement les plateformes tout en réalisant des tests d'intégration nécessaires.
On peut appliquer l'alias pour (re)configurer les dataSources en s'appuyant sur la variable environment.
Il est possible aussi de configurer l'alias via xml (tag <alias).
@Enjoy