La problématique
Dans le cadre du développement d'une API devant interroger une base puis traiter ces données, les contraintes techniques suivantes sont apparues :
- une seule classe doit servir de façade pour utiliser l'API,
- celle-ci doit prendre une DataSource définit par le client,
- la partie cliente ne doit pas avoir connaissance du contexte Spring de l'API.
Les beans d'accès aux données ainsi que les beans gérant les transactions ont besoin d'une DataSource pour pouvoir fonctionner.
Comment faire pour créer le bean d'entrée de la librairie, donc charger le contexte, sans qu'une exception soit lancé par Spring? Comment faire pour que Spring s'occupe de l'injection de la DataSource alors que celle-ci n'est connu qu'au moment d'utiliser la librairie?
Quelques solutions
Injecter manuellement la DataSource
L'idée ici est de créer le bean façade de l'API, d'y injecter les dépendances et d'injecter manuellement la DataSource.
Cette solution oblige le développeur à connaitre le nom des beans dépendants de la DataSource. Toute modification du contexte implique alors d'intervenir dans le code.
Injection de la DataSource par Spring
Cette fois ci, il s'agit de laisser Spring injecter la DataSource dans les beans.
La méthode d'initialisation du bean:
public void init(DriverManagerDataSource driverDataSource) throws EngineException {
// premièrement, création d'un contexte parent
ConfigurableApplicationContext genericContext = new GenericApplicationContext();
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) genericContext.getBeanFactory();
// création du bean datasource et injection de ses propriétés
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(DriverManagerDataSource.class); beanDefinitionBuilder.addPropertyValue("url", driverDataSource.getUrl());
beanDefinitionBuilder.addPropertyValue("password", driverDataSource.getPassword());
beanDefinitionBuilder.addPropertyValue("username", driverDataSource.getUsername());
beanDefinitionBuilder.addPropertyValue("driverClassName", "oracle.jdbc.OracleDriver");
// enregistrement du bean dans le contexte parent
beanFactory.registerBeanDefinition("parentDataSource", beanDefinitionBuilder.getBeanDefinition());
genericContext.refresh();
// création du contexte de l'API comme contexte enfant
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "engine-context.xml", "service-context.xml", "dao-context.xml" }, true, genericContext);
// déclare le bean courant comme autowired
context.getAutowireCapableBeanFactory().autowireBeanProperties(this,AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
// initialise le bean courant en y injectant les beans
context.getAutowireCapableBeanFactory().initializeBean(this,"modeDataEngine");
}
Contexte Spring
La ligne suivante définit une dataSource dans le contexte de l'API.
<bean id="datasource" parent="parentDataSource">
Quelques remarques
La datasource utilisée est une DriverManagerDataSource fournie par le framework Spring. Pourquoi est-ce la seule utilisable?
Pour définir une datasource dans un contexte, nous avons besoin de lui fournir plusieurs données pour s'initialiser:
- l'URL de connexion,
- la classe du driver JDBC,
- le nom d'utilisateur,
- Le mot de passe.
L'objectif est de proposer une API, ce qui implique qu'elle ne doit prendre en entrée, pour rester flexible, que des interfaces ou des classes définies par l'API elle-même.
Utilisation d'une factory
Une autre solution consiste à créer une factory de DataSource.
L'idée est de fournir une dataSource, n'importe quelle implémentation, à la méthode d'initialisation de l'API, qui ensuite, chargera le contexte Spring. Dans ce contexte, une factory doit être définit. Elle doit définir une méthode d'initialisation, qui ira chercher la dataSource dans l'instance de l'API, puis la fournira aux autres beans.
Contexte Spring
<bean id="dataSourceFactory" class="DataSourceFactory" method="init">
<bean id="dataSource" bean="dataSourceFactory" method="getDataSource">