Spring JavaConfig Tips : Lazy Init Proxy

springicon

Spring permet depuis (probablement) toujours de déclarer une définition de bean “lazy” : l’idée étant que l’instance du bean n’est créé par Spring qu’au moment où il est demandé (ou nécessaire) et non de manière systématique dans le cadre de l’initialisation de l’ApplicationContext comme l’ensemble des beans en scope singleton (le scope par défaut).

Son utilisation reste rare mais cela peut-être utile dans certains cas : si la création du bean est particulièrement coûteuse en temps, s’il est nécessaire d’attendre l’initialisation complète de l’application (ou d’une autre application…) pour pouvoir créer le bean sans erreur, si la création d’un bean peut échouer et que cela ne doit pas empêcher le contexte Spring de démarrer, etc. Une gestion d’erreur adéquate sera alors généralement nécessaire.

En configuration XML, l’attribut lazy-init est utilisé pour cela. Cela donne :

<bean id="serviceTarget" class="example.MyService" lazy-init="true">
  ...
</bean>

Pour les configurations Java, l’annotation @Lazy a été introduite et joue exactement le même rôle. Elle est utilisable :

  • sur un composant annoté @Component (ou dérivée) qui sera découvert automatiquement par Spring dans le cadre d’un scan de composant
  • sur une méthode annotée @Bean généralement au sein d’une classe @Configuration (JavaConfig) :
@Bean
@Lazy
public MyService myService() {
  return new example.MyService();
}

Mais quel que soit son mode de configuration, activer ce paramètre ne suffit généralement pas…

En effet, si un bean singleton A nécessite l’injection en tant que dépendance d’un bean B marqué Lazy, l’initialisation du bean A au démarrage du contexte forcera Spring à aussi initialiser B afin de pouvoir l’injecter dans A, remettant entièrement en cause l’intérêt de l’initialisation retardée…

Sans autre configuration, l’usage des beans marqués Lazy se limite donc généralement à une récupération programmatique du bean en question au moment où il est nécessaire :

MyService service = applicationContext.getBean("myService");

Ce qui n’est pas très en phase avec la philosophie générale de Spring, prônant plutôt une utilisation systématique de l’injection de dépendance.

Les proxy Spring à la rescousse

La réponse de Spring pour éviter cela passe par l’utilisation d’un proxy. Si A nécessite une dépendance B marquée Lazy, il faut créer un proxy B’ qui sera injecté à la place de B dans A. Le proxy B’ est alors en capacité d’intercepter tous les appels de méthode émis par A. Lors du premier appel, il peut faire créer le bean B à Spring, et se contente ensuite de déléguer tous les appels vers ce bean B nouvellement créé.

Spring fournit tous les éléments nécessaires à la création d’un tel proxy : en particulier la classe LazyInitTargetSource. La javadoc de spring de cette classe montre l’utilisation de cette classe pour une configuration XML :

<bean id="serviceTarget" class="example.MyService" lazy-init="true">
  ...
</bean>

<bean id="service" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="targetSource">
    <bean class="org.springframework.aop.target.LazyInitTargetSource">
      <property name="targetBeanName" ref="serviceTarget"/>
    </bean>
  </property>
</bean>

Avec cette configuration, deux beans Spring sont créés :

  • le bean cible nommé “serviceTarget” qui est une instance de example.MyService
  • le bean proxy nommé “service”- Si la classe example.MyService implémente une ou plusieurs interfaces, un Dynamic Proxy du JDK implémentant ces interfaces sera utilisé
  • Dans le cas contraire, une sous-classe sera générée par Spring à l’aide de Cglib et sera utilisée

On notera qu’il sera alors nécessaire d’injecter “service” et non “serviceTarget” dans les objets ayant une dépendance sur example.MyService pour que “serviceTarget” soit réellement créé de manière Lazy, ce qui nécessitera une injection par nom et non par type.

(A noter, la notion de bean marqué primary (ou @Primary en java) qui permettrait l’utilisation de l’injection par type n’est pas utilisable ici car son utilisation entraîne malheureusement une initialisation de tous les beans candidats à l’injection. Un bug existe sur ce point : SPR-8343.)

La création du proxy en JavaConfig

Oui, mais voilà… Les configurations Spring en XML sont devenues barbantes… Il nous faut trouver l’équivalent de cette configuration en JavaConfig.

Je vous propose ici deux versions :

  • une version pré-Java 8 pour commencer, puisque nous sommes malheureusement encore beaucoup à ne pas pouvoir bénéficier des améliorations de la dernière version de Java
  • une version Java 8 utilisant une lambda expression pour être un peu plus dans la tendance du moment 😉

Ces versions JavaConfig ont l’avantage d’être plus fortement typées : la création du proxy référence directement la méthode créant le bean target, à la place du nom du bean dans le cas de l’utilisation de la configuration XML.

La version pré-Java 8 pour les malheureux

Tout d’abord, une TargetSource plus adaptée à une utilisation dans une configuration Spring JavaConfig est nécessaire :

package fr.ippon.blog.javaconfig;

import org.springframework.aop.TargetSource;

/** Basé sur {@link org.springframework.aop.target.LazyInitTargetSource} */
public abstract class JavaConfigLazyInitTargetSource implements TargetSource {

	public JavaConfigLazyInitTargetSource(Class<?> targetClass) {
		this.targetClass = targetClass;
	}

	private Class<?> targetClass;
	private Object target;

	@Override
	public void releaseTarget(Object target) throws Exception {
		// Nothing to do here.
	}

	@Override
	public boolean isStatic() {
		return true;
	}

	@Override
	public Class<?> getTargetClass() {
		return targetClass;
	}

	@Override
	public synchronized Object getTarget() throws Exception {
		if(target == null) {
			target = createTarget();
		}
		return target;
	}

	protected abstract Object createTarget();
}

Voici maintenant un exemple complet de configuration sous forme d’un test unitaire (sans les assertions ici pour ne pas alourdir le listing) :

package fr.ippon.blog.javaconfig;

import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class LazyInitTest {

	@Configuration
	public static class TestConfiguration implements ApplicationListener<ApplicationContextEvent> {
		@Bean
		public MyBean myBean() {
			System.out.println("Creating proxy");

			TargetSource targetSource = new JavaConfigLazyInitTargetSource(MyBean.class) {
				@Override
				protected Object createTarget() {
					return myBeanTarget();
				}
			};

			return (MyBean) ProxyFactory.getProxy(targetSource);
		}

		@Bean
		@Lazy
		public MyBean myBeanTarget() {
			System.out.println("Creating MyBean");
			return new MyBeanImpl();
		}

		@Override
		public void onApplicationEvent(ApplicationContextEvent event) {
			System.out.println("Application Context Event : " + event.getClass().getSimpleName());
		}
	}

	@Inject
	@Named("myBean")
	MyBean bean;

	@Test
	public void beanIsAProxy() {
		System.out.println("Before using MyBean");
		System.out.println("bean type : " + bean.getClass());
		System.out.println("bean.doIt returns : " + bean.doIt());
	}

	public static interface MyBean {
		public String doIt();
	}

	public static class MyBeanImpl implements MyBean {
		@Override
		public String doIt() {
			return "From implementation";
		}

		@PreDestroy
		public void destroy() {
			System.out.println("in MyBeanImpl.destroy");
		}
	}

}

On obtient alors cette sortie, montrant qu’effectivement le bean n’est créé que lorsque il est utilisé (“Creating MyBean” après “Before using MyBean”) :

Creating proxy Application Context Event : ContextRefreshedEvent Before using MyBean bean type : class com.sun.proxy.$Proxy14 Creating MyBean bean.doIt returns : From implementation Application Context Event : ContextClosedEvent in MyBeanImpl.destroy

Une version Java 8 plus moderne

Voici une solution un peu plus élégante utilisant une lambda expression introduite dans Java 8 :

package fr.ippon.blog.javaconfig;

import java.util.Objects;
import java.util.function.Supplier;

public class Java8ConfigLazyInitTargetSource extends JavaConfigLazyInitTargetSource {

	public Java8ConfigLazyInitTargetSource(Class<?> targetClass, Supplier<Object> targetCreator) {
		super(targetClass);
		this.targetCreator = Objects.requireNonNull(targetCreator);
	}

	private Supplier<Object> targetCreator;

	protected Object createTarget() {
		return targetCreator.get();
	}
}

La création du proxy devient alors :

@Bean
		public MyBean myBean() {
			System.out.println("Creating proxy");

			Supplier<Object> supplier = () -> myBeanTarget();
			TargetSource targetSource = new Java8ConfigLazyInitTargetSource(MyBean.class, supplier);

// Ou plus concis :	TargetSource targetSource = new Java8ConfigLazyInitTargetSource(MyBean.class, () -> myBeanTarget());

			return (MyBean) ProxyFactory.getProxy(targetSource);
		} 

Activation de l’auto proxying

La méthode décrite précédemment a l’avantage de ne pas être trop “magique” et, je l’espère, vous aura permis de comprendre les mécanismes mis en oeuvre et la façon dont vous pouvez facilement, vous-même, créer des proxy Spring en JavaConfig (et pas forcément pour un besoin d’initialisation Lazy). Toutefois, elle devient lourde à appliquer si vous avez de nombreux beans Lazy à “proxyfier”.

Spring a prévu le coup depuis longtemps et fournissait une solution à ce problème dès sa version 1.2 : un mécanisme d’ “autoProxying” permettant de remplacer à la volée tous les beans marqués Lazy par un proxy d’initialisation retardée grâce en particulier à la classe LazyInitTargetSourceCreator.

Malheureusement, son utilisation sur des beans déclarés en JavaConfig (via @Bean donc), est longtemps resté bugguée : SPR-10508. Mais Juergen Hoeller a pris le problème en main et l’a corrigé deux jours avant la sortie de Spring 4.1 !

En Spring 4.1, il vous suffit donc de déclarer ces beans supplémentaires dans votre configuration JavaConfig pour que tous vos beans Lazy soient automatiquement remplacés par un proxy :

@Bean
		public BeanNameAutoProxyCreator lazyInitAutoProxyCreator() {
			BeanNameAutoProxyCreator autoProxyCreator = new BeanNameAutoProxyCreator();
			TargetSourceCreator[] targetSourceCreators = {lazyInitTargetSourceCreator()};
			autoProxyCreator.setCustomTargetSourceCreators(targetSourceCreators);
			return autoProxyCreator; 
		}

		@Bean
		public LazyInitTargetSourceCreator lazyInitTargetSourceCreator() { 
			return new LazyInitTargetSourceCreator();
		}

(Il reste toutefois possible de le faire fonctionner en Spring 4.0.x mais sous certaines conditions. Je vous laisse étudier les commentaires du ticket JIRA pour les détails.)

Author image
Fort de plus de 20 ans d'expérience autour de l'écosystème JavaEE, Fabien accompagne ses clients dans la conception et le cadrage de leurs projets.