Portée personnalisée au printemps

1. Vue d'ensemble

Prêt à l'emploi , Spring fournit deux portées bean standard ( «singleton» et «prototype» ) qui peuvent être utilisées dans n'importe quelle application Spring, ainsi que trois portées bean supplémentaires ( «request» , «session» et «globalSession» ) à utiliser uniquement dans les applications Web.

Les étendues de bean standard ne peuvent pas être remplacées, et il est généralement considéré comme une mauvaise pratique de remplacer les étendues Web. Cependant, vous pouvez avoir une application nécessitant des fonctionnalités différentes ou supplémentaires de celles trouvées dans les étendues fournies.

Par exemple, si vous développez un système mutualisé, vous souhaiterez peut-être fournir une instance distincte d'un bean particulier ou d'un ensemble de beans pour chaque client. Spring fournit un mécanisme pour créer des étendues personnalisées pour de tels scénarios.

Dans ce tutoriel rapide, nous montrerons comment créer, enregistrer et utiliser une étendue personnalisée dans une application Spring .

2. Création d'une classe d'étendue personnalisée

Afin de créer une étendue personnalisée, nous devons implémenter l' interface Scope . Ce faisant, nous devons également nous assurer que l'implémentation est thread-safe car les étendues peuvent être utilisées par plusieurs usines de beans en même temps.

2.1. Gestion des objets étendus et des rappels

L'une des premières choses à prendre en compte lors de l'implémentation d'une classe Scope personnalisée est de savoir comment vous allez stocker et gérer les objets étendus et les rappels de destruction. Cela pourrait être fait en utilisant une carte ou une classe dédiée, par exemple.

Pour cet article, nous allons le faire de manière thread-safe à l'aide de cartes synchronisées.

Commençons par définir notre classe de portée personnalisée:

public class TenantScope implements Scope { private Map scopedObjects = Collections.synchronizedMap(new HashMap()); private Map destructionCallbacks = Collections.synchronizedMap(new HashMap()); ... }

2.2. Récupération d'un objet depuis l'étendue

Pour récupérer un objet par son nom à partir de notre portée, implémentons la méthode getObject . Comme l'indique JavaDoc, si l'objet nommé n'existe pas dans la portée, cette méthode doit créer et renvoyer un nouvel objet .

Dans notre implémentation, nous vérifions si l'objet nommé est dans notre carte. Si c'est le cas, nous le renvoyons , et sinon, nous utilisons l' ObjectFactory pour créer un nouvel objet, l'ajouter à notre carte et le renvoyer:

@Override public Object get(String name, ObjectFactory objectFactory) { if(!scopedObjects.containsKey(name)) { scopedObjects.put(name, objectFactory.getObject()); } return scopedObjects.get(name); }

Parmi les cinq méthodes définies par l' interface Scope , seule la méthode get est requise pour avoir une implémentation complète du comportement décrit. Les quatre autres méthodes sont facultatives et peuvent lever une exception UnsupportedOperationException si elles n'ont pas besoin ou ne peuvent pas prendre en charge une fonctionnalité.

2.3. Enregistrement d'un rappel de destruction

Nous devons également implémenter la méthode registerDestructionCallback . Cette méthode fournit un rappel qui doit être exécuté lorsque l'objet nommé est détruit ou si la portée elle-même est détruite par l'application:

@Override public void registerDestructionCallback(String name, Runnable callback) { destructionCallbacks.put(name, callback); }

2.4. Suppression d'un objet de l'étendue

Ensuite, implémentons la méthode remove , qui supprime l'objet nommé de la portée et supprime également son rappel de destruction enregistré, retournant l'objet supprimé:

@Override public Object remove(String name) { destructionCallbacks.remove(name); return scopedObjects.remove(name); }

Notez qu'il est de la responsabilité de l'appelant d'exécuter réellement le rappel et de détruire l'objet supprimé .

2.5. Obtenir l'ID de conversation

Maintenant, implémentons la méthode getConversationId . Si votre portée prend en charge le concept d'identifiant de conversation, vous le renverrez ici. Sinon, la convention est de retourner null :

@Override public String getConversationId() { return "tenant"; }

2.6. Résolution d'objets contextuels

Enfin, implémentons la méthode ResolutionContextualObject . Si votre étendue prend en charge plusieurs objets contextuels, vous associez chacun à une valeur de clé et vous renverrez l'objet correspondant au paramètre de clé fourni . Sinon, la convention est de retourner null :

@Override public Object resolveContextualObject(String key) { return null; }

3. Enregistrement de l'étendue personnalisée

To make the Spring container aware of your new scope, you need to register it through the registerScope method on a ConfigurableBeanFactory instance. Let's take a look at this method's definition:

void registerScope(String scopeName, Scope scope);

The first parameter, scopeName, is used to identify/specify a scope by its unique name. The second parameter, scope, is an actual instance of the custom Scope implementation that you wish to register and use.

Let's create a custom BeanFactoryPostProcessor and register our custom scope using a ConfigurableListableBeanFactory:

public class TenantBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException { factory.registerScope("tenant", new TenantScope()); } }

Now, let's write a Spring configuration class that loads our BeanFactoryPostProcessor implementation:

@Configuration public class TenantScopeConfig { @Bean public static BeanFactoryPostProcessor beanFactoryPostProcessor() { return new TenantBeanFactoryPostProcessor(); } }

4. Using the Custom Scope

Now that we have registered our custom scope, we can apply it to any of our beans just as we would with any other bean that uses a scope other than singleton (the default scope) — by using the @Scope annotation and specifying our custom scope by name.

Let's create a simple TenantBean class — we'll declare tenant-scoped beans of this type in a moment:

public class TenantBean { private final String name; public TenantBean(String name) { this.name = name; } public void sayHello() { System.out.println( String.format("Hello from %s of type %s", this.name, this.getClass().getName())); } }

Note that we did not use the class-level @Component and @Scope annotations on this class.

Now, let's define some tenant-scoped beans in a configuration class:

@Configuration public class TenantBeansConfig { @Scope(scopeName = "tenant") @Bean public TenantBean foo() { return new TenantBean("foo"); } @Scope(scopeName = "tenant") @Bean public TenantBean bar() { return new TenantBean("bar"); } }

5. Testing the Custom Scope

Écrivons un test pour exercer notre configuration de portée personnalisée en chargeant un ApplicationContext , en enregistrant nos classes de configuration et en récupérant nos beans à portée client:

@Test public final void whenRegisterScopeAndBeans_thenContextContainsFooAndBar() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); try{ ctx.register(TenantScopeConfig.class); ctx.register(TenantBeansConfig.class); ctx.refresh(); TenantBean foo = (TenantBean) ctx.getBean("foo", TenantBean.class); foo.sayHello(); TenantBean bar = (TenantBean) ctx.getBean("bar", TenantBean.class); bar.sayHello(); Map foos = ctx.getBeansOfType(TenantBean.class); assertThat(foo, not(equalTo(bar))); assertThat(foos.size(), equalTo(2)); assertTrue(foos.containsValue(foo)); assertTrue(foos.containsValue(bar)); BeanDefinition fooDefinition = ctx.getBeanDefinition("foo"); BeanDefinition barDefinition = ctx.getBeanDefinition("bar"); assertThat(fooDefinition.getScope(), equalTo("tenant")); assertThat(barDefinition.getScope(), equalTo("tenant")); } finally { ctx.close(); } }

Et le résultat de notre test est:

Hello from foo of type org.baeldung.customscope.TenantBean Hello from bar of type org.baeldung.customscope.TenantBean

6. Conclusion

Dans ce tutoriel rapide, nous avons montré comment définir, enregistrer et utiliser une portée personnalisée dans Spring.

Vous pouvez en savoir plus sur les étendues personnalisées dans le Spring Framework Reference. Vous pouvez également jeter un œil aux implémentations de Spring de diverses classes Scope dans le référentiel Spring Framework sur GitHub.

Comme d'habitude, vous pouvez trouver les exemples de code utilisés dans cet article sur le projet GitHub.