Portée personnalisée au printemps

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, plus 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 portées fournies.

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

Dans ce rapide tutoriel, nous allons démontrerhow to create, register, and use a custom scope in a Spring application.

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

Afin de créer une étendue personnalisée,we must implement the Scope interface. Ce faisant, nous devons égalementensure that the implementation is thread-safe car les scopes peuvent être utilisés par plusieurs usines de haricots 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 classeScope personnalisée est la façon dont 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 d'étendue 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 dans notre étendue, implémentons la méthodegetObject. Comme l'indique JavaDoc,if the named object does not exist in the scope, this method must create and return a new object.

Dans notre implémentation, nous vérifions si l'objet nommé est dans notre carte. Si c'est le cas, nous le retournons, et sinon, nous utilisons lesObjectFactory 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'interfaceScope,only the get method is required to have a full implementation du comportement décrit. Les quatre autres méthodes sont facultatives et peuvent générer desUnsupportedOperationException 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éthoderegisterDestructionCallback. Cette méthode fournit un rappel à exécuter lorsque l'objet nommé est détruit ou si l'étendue 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éthoderemove, qui supprime l'objet nommé de la portée et supprime également son rappel de destruction enregistré, en renvoyant l'objet supprimé:

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

Notez queit is the caller’s responsibility to actually execute the callback and destroy the removed object.

2.5. Obtention de l'ID de conversation

Maintenant, implémentons la méthodegetConversationId. Si votre portée prend en charge le concept d'un identifiant de conversation, vous le renverriez ici. Sinon, la convention est de renvoyernull:

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

2.6. Résolution d'objets contextuels

Enfin, implémentons la méthoderesolveContextualObject. Si votre étendue prend en charge plusieurs objets contextuels, vous associeriez chacun à une valeur de clé et vous renverriez l'objet correspondant au paramètrekey fourni. Sinon, la convention est de retournernull:

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

3. Enregistrement de l'étendue personnalisée

Pour rendre le conteneur Spring conscient de votre nouvelle portée, vous devezregister it through the registerScope method on a ConfigurableBeanFactory instance. Jetons un œil à la définition de cette méthode:

void registerScope(String scopeName, Scope scope);

Le premier paramètre,scopeName, est utilisé pour identifier / spécifier une portée par son nom unique. Le deuxième paramètre,scope, est une instance réelle de l'implémentation personnalisée deScope que vous souhaitez enregistrer et utiliser.

Créons unBeanFactoryPostProcessor personnalisé et enregistrons notre étendue personnalisée en utilisant unConfigurableListableBeanFactory:

public class TenantBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

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

Maintenant, écrivons une classe de configuration Spring qui charge notre implémentationBeanFactoryPostProcessor:

@Configuration
public class TenantScopeConfig {

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

4. Utilisation de l'étendue personnalisée

Maintenant que nous avons enregistré notre portée personnalisée, nous pouvons l'appliquer à n'importe lequel de nos beans comme nous le ferions avec n'importe quel autre bean qui utilise une portée autre quesingleton (la portée par défaut) - en utilisant le@Scope annotation et spécifiant notre portée personnalisée par nom.

Créons une simple classeTenantBean - nous déclarerons dans un instant des beans de ce type à la portée du client:

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

Notez que nous n'avons pas utilisé les annotations de niveau classe@Component et@Scope sur cette classe.

À présent, définissons des beans à l'échelle du client dans une classe de configuration:

@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. Test de l'étendue personnalisée

Écrivons un test pour tester notre configuration de portée personnalisée en chargeant unApplicationContext, en enregistrant nos classesConfiguration 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.example.customscope.TenantBean
Hello from bar of type org.example.customscope.TenantBean

6. Conclusion

Dans ce rapide tutoriel, 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 lesSpring Framework Reference. Vous pouvez également jeter un œil aux implémentations de Spring de diverses classesScope dans lesSpring Framework repository on GitHub.

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