Ambito personalizzato in primavera

1. Panoramica

Per impostazione predefinita , Spring fornisce due ambiti di bean standard ( "singleton" e "prototype" ) che possono essere utilizzati in qualsiasi applicazione Spring, più tre ambiti di bean aggiuntivi ( "request" , "session" e "globalSession" ) da utilizzare solo nelle applicazioni web-aware.

Gli ambiti bean standard non possono essere sovrascritti ed è generalmente considerata una cattiva pratica sovrascrivere gli ambiti web-aware. Tuttavia, potresti avere un'applicazione che richiede funzionalità diverse o aggiuntive rispetto a quelle trovate negli ambiti forniti.

Ad esempio, se si sta sviluppando un sistema multi-tenant, potrebbe essere necessario fornire un'istanza separata di un particolare bean o set di bean per ogni tenant. Spring fornisce un meccanismo per la creazione di ambiti personalizzati per scenari come questo.

In questo breve tutorial, dimostreremo come creare, registrare e utilizzare un ambito personalizzato in un'applicazione Spring .

2. Creazione di una classe di ambito personalizzata

Per creare un ambito personalizzato, dobbiamo implementare l' interfaccia Scope . In tal modo, dobbiamo anche assicurarci che l'implementazione sia thread-safe perché gli ambiti possono essere utilizzati da più bean factory contemporaneamente.

2.1. Gestione degli oggetti con ambito e dei callback

Una delle prime cose da considerare quando si implementa una classe Scope personalizzata è come archiviare e gestire gli oggetti con ambito e i callback di distruzione. Questo potrebbe essere fatto usando una mappa o una classe dedicata, per esempio.

Per questo articolo, lo faremo in modo thread-safe utilizzando mappe sincronizzate.

Cominciamo a definire la nostra classe di ambito personalizzata:

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

2.2. Recupero di un oggetto dall'ambito

Per recuperare un oggetto in base al nome dal nostro ambito, implementiamo il metodo getObject . Come afferma JavaDoc, se l'oggetto denominato non esiste nell'ambito, questo metodo deve creare e restituire un nuovo oggetto .

Nella nostra implementazione, controlliamo se l'oggetto denominato è nella nostra mappa. Se lo è, lo restituiamo e, in caso contrario, utilizziamo ObjectFactory per creare un nuovo oggetto, aggiungerlo alla nostra mappa e restituirlo:

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

Dei cinque metodi definiti dall'interfaccia Scope , solo il metodo get è richiesto per avere un'implementazione completa del comportamento descritto. Gli altri quattro metodi sono facoltativi e possono generare UnsupportedOperationException se non è necessario o non supportano una funzionalità.

2.3. Registrazione di una richiamata di distruzione

Dobbiamo anche implementare il metodo registerDestructionCallback . Questo metodo fornisce un callback che deve essere eseguito quando l'oggetto denominato viene distrutto o se l'ambito stesso viene distrutto dall'applicazione:

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

2.4. Rimozione di un oggetto dall'ambito

Successivamente, implementiamo il metodo remove , che rimuove l'oggetto denominato dall'ambito e rimuove anche il callback di distruzione registrato, restituendo l'oggetto rimosso:

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

Notare che è responsabilità del chiamante eseguire effettivamente il callback e distruggere l'oggetto rimosso .

2.5. Ottenere l'ID conversazione

Ora implementiamo il metodo getConversationId . Se il tuo ambito supporta il concetto di ID conversazione, lo restituirai qui. Altrimenti, la convenzione è di restituire null :

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

2.6. Risoluzione di oggetti contestuali

Infine, implementiamo il metodo resolxtualObject . Se l'ambito supporta più oggetti contestuali, assoceresti ciascuno a un valore chiave e restituiresti l'oggetto corrispondente al parametro chiave fornito . Altrimenti, la convenzione è di restituire null :

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

3. Registrazione dell'ambito personalizzato

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

Scriviamo un test per esercitare la nostra configurazione dell'ambito personalizzato caricando un ApplicationContext , registrando le nostre classi di configurazione e recuperando i nostri bean con ambito tenant:

@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(); } }

E l'output del nostro test è:

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

6. Conclusione

In questo breve tutorial, abbiamo mostrato come definire, registrare e utilizzare un ambito personalizzato in Spring.

È possibile leggere ulteriori informazioni sugli ambiti personalizzati in Spring Framework Reference. Puoi anche dare un'occhiata alle implementazioni di Spring di varie classi Scope nel repository Spring Framework su GitHub.

Come al solito, puoi trovare gli esempi di codice usati in questo articolo sul progetto GitHub.