Spring 4 : ‘Bean Aliasing’ ou comment personnaliser des beans de composants externes

logo-spring-highres

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".

  • Test unitaire

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

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.