mardi 25 novembre 2008

Injection d'une DataSource dans un contexte Spring

Cet article a pour but de décrire comment injecter une datasource dans un contexte Spring existant et surtout de décrire les solutions que j'ai envisagé.

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.
Sachant que la partie cliente est dépendante du framework Spring, son utilisation dans ce cas de figure semblait naturel. Ainsi, la partie ORM du framework permet de coder plus rapidement les accès à la base et le fichier de contexte permet de résoudre les problèmes d'injection de dépendance, de transaction et permet d'éviter l'écriture de fabriques.
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.
Le DrvierManagerDataSource est une classe qui permet d'obtenir ces informations en utilisant de simples setter, ce qu'un grand nombre d'implémentation de l'interface DataSource ne font pas.
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">