Personnalisation de la résolution des dépendances au printemps

Salut Je m'appelle Andrey Nevedomsky et je suis l'ingénieur en chef de SberTekh. Je travaille au sein d'une équipe qui développe l'un des services système de l'ESF (Unified Frontal System). Dans notre travail, nous utilisons activement Spring Framework, en particulier son DI, et de temps en temps nous sommes confrontés au fait que la résolution des dépendances au printemps n'est pas assez intelligente pour nous. Cet article est le résultat de mes tentatives pour le rendre plus intelligent et comprendre généralement comment cela fonctionne. J'espère que vous pourrez en apprendre quelque chose de nouveau sur l'appareil du printemps.



Avant de lire l'article, je vous recommande fortement de lire les rapports de Boris Yevgeny EvgenyBorisov : Spring Ripper, Part 1 ; Ripper Ă  ressort, partie 2 . Il y a toujours une liste de lecture d'entre eux .

Présentation


Imaginons qu'on nous ait demandé de développer un service de prédiction du destin et des horoscopes. Il existe plusieurs composants dans notre service, mais les principaux pour nous seront deux:

  • Globa, qui implĂ©mentera l'interface FortuneTeller et prĂ©dira le sort;




  • Gypsy, qui implĂ©mentera l'interface HoroscopeTeller et crĂ©era des horoscopes.




Dans notre service, il y aura également plusieurs points de terminaison (contrôleurs) pour, en fait, obtenir la diseuse de bonne aventure et les horoscopes. Et nous contrôlerons également l'accès à notre application par IP en utilisant un aspect qui sera appliqué aux méthodes du contrôleur et ressemblera à ceci:

RestrictionAspect.java
@Aspect @Component @Slf4j public class RestrictionAspect {    private final Predicate<String> ipIsAllowed;    public RestrictionAspect(@NonNull final Predicate<String> ipIsAllowed) {        this.ipIsAllowed = ipIsAllowed;    }    @Before("execution(public * com.github.monosoul.fortuneteller.web.*.*(..))")    public void checkAccess() {        val ip = getRequestSourceIp();       log.debug("Source IP: {}", ip);        if (!ipIsAllowed.test(ip)) {            throw new AccessDeniedException(format("Access for IP [%s] is denied", ip));        }    }    private String getRequestSourceIp() {        val requestAttributes = currentRequestAttributes();        Assert.state(requestAttributes instanceof ServletRequestAttributes,                "RequestAttributes needs to be a ServletRequestAttributes");        val request = ((ServletRequestAttributes) requestAttributes).getRequest();        return request.getRemoteAddr();    } } 


Pour vérifier que l'accès à partir d'une telle IP est autorisé, nous utiliserons une implémentation du prédicat ipIsAllowed . En général, sur le site de cet aspect, il peut y en avoir d'autres, par exemple, une autorisation.

Nous avons donc développé l'application et tout fonctionne très bien pour nous. Mais parlons maintenant des tests.

Comment le tester?


Voyons comment tester l'application des aspects. Nous avons plusieurs façons de procéder.

Vous pouvez écrire des tests séparés pour un aspect et pour les contrôleurs, sans augmenter le contexte du ressort (ce qui créera simplement un proxy avec un aspect pour le contrôleur, vous pouvez en savoir plus à ce sujet dans la documentation officielle), mais dans ce cas, nous ne testerons pas exactement à quels aspects sont correctement appliqués. contrôleurs et fonctionnent exactement comme nous nous attendons ;

Vous pouvez écrire des tests dans lesquels nous aborderons le contexte complet de notre application, mais dans ce cas:

  • l'exĂ©cution des tests prendra beaucoup de temps, car tous les bacs se lèveront;
  • nous devrons prĂ©parer des donnĂ©es de test valides qui peuvent traverser toute la chaĂ®ne d'appels entre les bacs sans lancer NPE en mĂŞme temps.

Mais nous voulons tester exactement ce que l'aspect a appliqué et fait son travail. Nous ne voulons pas tester les services appelés par le contrôleur, et ne voulons donc pas être intrigués par les données de test et sacrifier le temps de démarrage. Par conséquent, nous écrirons des tests dans lesquels nous n'évoquerons qu'une partie du contexte. C'est-à-dire dans notre contexte, il y aura un vrai bean aspect et un vrai bean contrôleur, et tout le reste sera mokami.

Comment créer des haricots moka?


Il existe plusieurs façons de créer des haricots moka au printemps. Pour plus de clarté, par exemple, nous prenons l'un des contrôleurs de notre service - PersonalizedHoroscopeTellController , son code ressemble à ceci:

PersonalizedHoroscopeTellController.java
 @Slf4j @RestController @RequestMapping( value = "/horoscope", produces = APPLICATION_JSON_UTF8_VALUE ) public class PersonalizedHoroscopeTellController { private final HoroscopeTeller horoscopeTeller; private final Function<String, ZodiacSign> zodiacSignConverter; private final Function<String, String> nameNormalizer; public PersonalizedHoroscopeTellController( final HoroscopeTeller horoscopeTeller, final Function<String, ZodiacSign> zodiacSignConverter, final Function<String, String> nameNormalizer ) { this.horoscopeTeller = horoscopeTeller; this.zodiacSignConverter = zodiacSignConverter; this.nameNormalizer = nameNormalizer; } @GetMapping(value = "/tell/personal/{name}/{sign}") public PersonalizedHoroscope tell(@PathVariable final String name, @PathVariable final String sign) { log.info("Received name: {}; sign: {}", name, sign); return PersonalizedHoroscope.builder() .name( nameNormalizer.apply(name) ) .horoscope( horoscopeTeller.tell( zodiacSignConverter.apply(sign) ) ) .build(); } } 


Configuration Java avec dépendances dans chaque test


Pour chaque test, nous pouvons écrire Java Config dans lequel nous décrivons à la fois les beans contrôleur et aspect et les beans avec les moks de dépendance du contrôleur. Cette façon de décrire les beans sera impérative, car nous dirons explicitement au printemps comment nous devons créer les beans.

Dans ce cas, le test de notre contrĂ´leur ressemblera Ă  ceci:

javaconfig / PersonalizedHoroscopeTellControllerTest.java
 @SpringJUnitConfig public class PersonalizedHoroscopeTellControllerTest { private static final int LIMIT = 10; @Autowired private PersonalizedHoroscopeTellController controller; @Autowired private Predicate<String> ipIsAllowed; @Test void doNothingWhenAllowed() { when(ipIsAllowed.test(anyString())).thenReturn(true); controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT)); } @Test void throwExceptionWhenNotAllowed() { when(ipIsAllowed.test(anyString())).thenReturn(false); assertThatThrownBy(() -> controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT))) .isInstanceOf(AccessDeniedException.class); } @Configuration @Import(AspectConfiguration.class) @EnableAspectJAutoProxy public static class Config { @Bean public PersonalizedHoroscopeTellController personalizedHoroscopeTellController( final HoroscopeTeller horoscopeTeller, final Function<String, ZodiacSign> zodiacSignConverter, final Function<String, String> nameNormalizer ) { return new PersonalizedHoroscopeTellController(horoscopeTeller, zodiacSignConverter, nameNormalizer); } @Bean public HoroscopeTeller horoscopeTeller() { return mock(HoroscopeTeller.class); } @Bean public Function<String, ZodiacSign> zodiacSignConverter() { return mock(Function.class); } @Bean public Function<String, String> nameNormalizer() { return mock(Function.class); } } } 


Un tel test semble plutôt lourd. Dans ce cas, nous devrons écrire Java Config pour chacun des contrôleurs. Bien que son contenu soit différent, il aura la même signification: créer un bean contrôleur et un moki pour ses dépendances. Donc, en substance, ce sera le même pour tous les contrôleurs. Comme tout programmeur, je suis une personne paresseuse, j'ai donc immédiatement refusé cette option.

Annotation @MockBean sur chaque champ avec dépendance


L' annotation @MockBean est apparue dans Spring Boot Test version 1.4.0. Il est similaire à @Mock de Mockito (et en fait il l'utilise même en interne), à ​​la seule différence que lorsque vous utilisez @MockBean , le mock créé sera automatiquement placé dans le contexte du printemps. Cette méthode de déclaration de mok sera déclarative, car nous n'avons pas à dire à spring exactement comment créer ces mok.

Dans ce cas, le test ressemblera Ă  ceci:

mockbean / PersonalizedHoroscopeTellControllerTest.java
 @SpringJUnitConfig public class PersonalizedHoroscopeTellControllerTest { private static final int LIMIT = 10; @MockBean private HoroscopeTeller horoscopeTeller; @MockBean private Function<String, ZodiacSign> zodiacSignConverter; @MockBean private Function<String, String> nameNormalizer; @MockBean private Predicate<String> ipIsAllowed; @Autowired private PersonalizedHoroscopeTellController controller; @Test void doNothingWhenAllowed() { when(ipIsAllowed.test(anyString())).thenReturn(true); controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT)); } @Test void throwExceptionWhenNotAllowed() { when(ipIsAllowed.test(anyString())).thenReturn(false); assertThatThrownBy(() -> controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT))) .isInstanceOf(AccessDeniedException.class); } @Configuration @Import({PersonalizedHoroscopeTellController.class, RestrictionAspect.class, RequestContextHolderConfigurer.class}) @EnableAspectJAutoProxy public static class Config { } } 


Dans cette option, il y a toujours Java Config, mais il est beaucoup plus compact. Parmi les lacunes - j'ai dû déclarer des champs avec des dépendances de contrôleur (champs avec annotation @MockBean ), même s'ils ne sont pas utilisés plus loin dans le test. Eh bien, si vous utilisez la version Spring Boot inférieure à 1.4.0 pour une raison quelconque, vous ne pourrez pas utiliser cette annotation.

Par conséquent, j'ai eu une idée pour une autre option pour le moking. J'aimerais que ça fonctionne comme ça ...

@Annotation automatique sur le composant dépendant


J'aimerais que nous ayons l'annotation @Automocked , que je ne pourrais mettre qu'au-dessus du champ avec le contrôleur, puis moki serait automatiquement créé pour ce contrôleur et placé dans le contexte.

Le test dans ce cas pourrait ressembler Ă  ceci:

automocked / PersonalizedHoroscopeTellControllerTest.java
 @SpringJUnitConfig @ContextConfiguration(classes = AspectConfiguration.class) @TestExecutionListeners(listeners = AutomockTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) public class PersonalizedHoroscopeTellControllerTest { private static final int LIMIT = 10; @Automocked private PersonalizedHoroscopeTellController controller; @Autowired private Predicate<String> ipIsAllowed; @Test void doNothingWhenAllowed() { when(ipIsAllowed.test(anyString())).thenReturn(true); controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT)); } @Test void throwExceptionWhenNotAllowed() { when(ipIsAllowed.test(anyString())).thenReturn(false); assertThatThrownBy(() -> controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT))) .isInstanceOf(AccessDeniedException.class); } } 


Comme vous pouvez le voir, cette option est la plus compacte des options présentées, il n'y a qu'un bean contrôleur (plus un prédicat pour un aspect), l'annotation @Automocked est @Automocked - @Automocked , et toute la magie de créer des beans et de les placer dans le contexte est écrite une fois et peut être utilisée dans tous tests.

Comment ça marche?


Voyons comment cela fonctionne et ce dont nous avons besoin pour cela.

TestExecutionListener


Il existe une telle interface au printemps - TestExecutionListener . Il fournit une API pour l'incorporation dans le processus d'exécution de test à ses différentes étapes, par exemple, lors de la création d'une instance d'une classe de test, avant ou après l'appel d'une méthode de test, etc. Il a plusieurs implémentations hors de la boîte. Par exemple, DirtiesContextTestExecutionListener , qui nettoie le contexte si vous placez l'annotation appropriée; DependencyInjectionTestExecutionListener - effectue une injection de dépendance dans les tests, etc. Pour appliquer votre écouteur personnalisé au test, vous devez placer l'annotation @TestExecutionListeners au-dessus et indiquer votre implémentation.

Commandé


Il existe également une interface ordonnée au printemps. Il est utilisé pour indiquer que les objets doivent être triés d'une manière ou d'une autre. Par exemple, lorsque vous avez plusieurs implémentations de la même interface et que vous souhaitez les injecter dans une collection, alors dans cette collection, elles seront ordonnées selon Ordered. Dans le cas de TestExecutionListener, cette annotation indique dans quel ordre ils doivent être appliqués.

Ainsi, notre Listener implémentera 2 interfaces: TestExecutionListener et Ordered . Nous l'appelons AutomockTestExecutionListener et cela ressemblera à ceci:

AutomockTestExecutionListener.java
 @Slf4j public class AutomockTestExecutionListener implements TestExecutionListener, Ordered { @Override public int getOrder() { return 1900; } @Override public void prepareTestInstance(final TestContext testContext) { val beanFactory = ((DefaultListableBeanFactory) testContext.getApplicationContext().getAutowireCapableBeanFactory()); setByNameCandidateResolver(beanFactory); for (val field : testContext.getTestClass().getDeclaredFields()) { if (field.getAnnotation(Automocked.class) == null) { continue; } log.debug("Performing automocking for the field: {}", field.getName()); makeAccessible(field); setField( field, testContext.getTestInstance(), createBeanWithMocks(findConstructorToAutomock(field.getType()), beanFactory) ); } } private void setByNameCandidateResolver(final DefaultListableBeanFactory beanFactory) { if ((beanFactory.getAutowireCandidateResolver() instanceof AutomockedBeanByNameAutowireCandidateResolver)) { return; } beanFactory.setAutowireCandidateResolver( new AutomockedBeanByNameAutowireCandidateResolver(beanFactory.getAutowireCandidateResolver()) ); } private Constructor<?> findConstructorToAutomock(final Class<?> clazz) { log.debug("Looking for suitable constructor of {}", clazz.getCanonicalName()); Constructor<?> fallBackConstructor = clazz.getDeclaredConstructors()[0]; for (val constructor : clazz.getDeclaredConstructors()) { if (constructor.getParameterTypes().length > fallBackConstructor.getParameterTypes().length) { fallBackConstructor = constructor; } val autowired = getAnnotation(constructor, Autowired.class); if (autowired != null) { return constructor; } } return fallBackConstructor; } private <T> T createBeanWithMocks(final Constructor<T> constructor, final DefaultListableBeanFactory beanFactory) { createMocksForParameters(constructor, beanFactory); val clazz = constructor.getDeclaringClass(); val beanName = forClass(clazz).toString(); log.debug("Creating bean {}", beanName); if (!beanFactory.containsBean(beanName)) { val bean = beanFactory.createBean(clazz); beanFactory.registerSingleton(beanName, bean); } return beanFactory.getBean(beanName, clazz); } private <T> void createMocksForParameters(final Constructor<T> constructor, final DefaultListableBeanFactory beanFactory) { log.debug("{} is going to be used for auto mocking", constructor); val constructorArgsAmount = constructor.getParameterTypes().length; for (int i = 0; i < constructorArgsAmount; i++) { val parameterType = forConstructorParameter(constructor, i); val beanName = parameterType.toString(); if (!beanFactory.containsBean(beanName)) { beanFactory.registerSingleton( beanName, mock(parameterType.resolve(), withSettings().stubOnly()) ); } log.debug("Mocked {}", beanName); } } } 


Que se passe-t-il ici? Tout d'abord, dans la méthode prepareTestInstance() , il trouve tous les champs avec l'annotation @Automocked :

 for (val field : testContext.getTestClass().getDeclaredFields()) { if (field.getAnnotation(Automocked.class) == null) { continue; } 

Rend ensuite ces champs accessibles en écriture:

 makeAccessible(field); 

Ensuite, dans la méthode findConstructorToAutomock() , findConstructorToAutomock() trouve le constructeur approprié:

 Constructor<?> fallBackConstructor = clazz.getDeclaredConstructors()[0]; for (val constructor : clazz.getDeclaredConstructors()) { if (constructor.getParameterTypes().length > fallBackConstructor.getParameterTypes().length) { fallBackConstructor = constructor; } val autowired = getAnnotation(constructor, Autowired.class); if (autowired != null) { return constructor; } } return fallBackConstructor; 

Un constructeur approprié dans notre cas est soit le constructeur avec l'annotation @Autowired ou le constructeur avec le plus grand nombre d'arguments.

Ensuite, le constructeur trouvé est passé en tant qu'argument à la méthode createBeanWithMocks() , qui à son tour appelle la méthode createMocksForParameters() , où les mocks pour les arguments du constructeur sont créés et enregistrés dans le contexte:

 val constructorArgsAmount = constructor.getParameterTypes().length; for (int i = 0; i < constructorArgsAmount; i++) { val parameterType = forConstructorParameter(constructor, i); val beanName = parameterType.toString(); if (!beanFactory.containsBean(beanName)) { beanFactory.registerSingleton( beanName, mock(parameterType.resolve(), withSettings().stubOnly()) ); } } 

Il est important de noter qu'une représentation sous forme de chaîne du type de l'argument (avec les génériques) sera utilisée comme nom de la corbeille. Autrement dit, pour un argument de type packages.Function<String, String> représentation sous forme de chaîne sera la chaîne "packages.Function<java.lang.String, java.lang.String>" . C'est important, nous y reviendrons.

Après avoir créé des maquettes pour tous les arguments et les avoir enregistrés dans le contexte, nous revenons à la création du bean de la classe dépendante (c'est-à-dire le contrôleur dans notre cas):

 if (!beanFactory.containsBean(beanName)) { val bean = beanFactory.createBean(clazz); beanFactory.registerSingleton(beanName, bean); } 

Vous devez également faire attention au fait que nous avons utilisé la commande 1900 . Cela est nécessaire car notre écouteur doit être appelé après avoir effacé le contexte ohm de DirtiesContextBeforeModesTestExecutionListener (ordre = 1500) et avant l'injection de dépendance DependencyInjectionTestExecutionListener (ordre = 2000), car notre écouteur crée de nouveaux bacs.

AutowireCandidateResolver


AutowireCandidateResolver est utilisé pour déterminer si la définition de bean correspond à la description de la dépendance. Il a plusieurs implémentations "prêtes à l'emploi", parmi lesquelles:


Dans le même temps, l'implémentation «out of the box» est une poupée russe d'héritage, c'est à dire ils se dilatent. Nous allons écrire un décorateur, car c'est plus flexible.

Le résolveur fonctionne comme suit:

  1. Spring prend un descripteur de dépendance - DependencyDescriptor ;
  2. Ensuite, il prend tous les BeanDefinition de la classe appropriée;
  3. Itère sur les BeanDefinitions reçues, appelant la méthode isAutowireCandidate() du résolveur;
  4. Selon que la description du bean correspond ou non à la description de la dépendance, la méthode renvoie true ou false.

Pourquoi aviez-vous besoin de votre résolveur?


Voyons maintenant pourquoi nous avions besoin de notre résolveur sur l'exemple de notre contrôleur.

 public class PersonalizedHoroscopeTellController { private final HoroscopeTeller horoscopeTeller; private final Function<String, ZodiacSign> zodiacSignConverter; private final Function<String, String> nameNormalizer; public PersonalizedHoroscopeTellController( final HoroscopeTeller horoscopeTeller, final Function<String, ZodiacSign> zodiacSignConverter, final Function<String, String> nameNormalizer ) { this.horoscopeTeller = horoscopeTeller; this.zodiacSignConverter = zodiacSignConverter; this.nameNormalizer = nameNormalizer; } 

Comme vous pouvez le voir, il a deux dépendances du même type - Function , mais avec des génériques différents. Dans un cas, String et ZodiacSign , dans l'autre, String et String . Et le problème avec cela est que Mockito n'est pas en mesure de créer des moks en tenant compte des génériques . C'est-à-dire si nous créons des mokas pour ces dépendances et les mettons en contexte, alors Spring ne pourra pas les injecter dans cette classe, car ils ne contiendront pas d'informations sur les génériques. Et nous verrons l'exception que dans le contexte il y a plus d'un bean de la classe Function . C'est précisément ce problème que nous allons résoudre à l'aide de notre résolveur. Après tout, comme vous vous en souvenez, dans notre implémentation de Listener, nous avons utilisé un type avec des génériques comme nom du bac, ce qui signifie que tout ce que nous devons faire est d'apprendre au printemps à comparer le type de dépendance avec le nom du bac.

AutomockedBeanByNameAutowireCandidateResolver


Ainsi, notre résolveur fera exactement ce que j'ai écrit ci-dessus, et l'implémentation de la méthode isAutowireCandidate() ressemblera à ceci:

AutowireCandidateResolver.isAutowireCandidate ()
 @Override public boolean isAutowireCandidate(BeanDefinitionHolder beanDefinitionHolder, DependencyDescriptor descriptor) { val dependencyType = descriptor.getResolvableType().resolve(); val dependencyTypeName = descriptor.getResolvableType().toString(); val candidateBeanDefinition = (AbstractBeanDefinition) beanDefinitionHolder.getBeanDefinition(); val candidateTypeName = beanDefinitionHolder.getBeanName(); if (candidateTypeName.equals(dependencyTypeName) && candidateBeanDefinition.getBeanClass() != null) { return true; } return candidateResolver.isAutowireCandidate(beanDefinitionHolder, descriptor); } 


Ici, il obtient la représentation sous forme de chaîne du type de dépendance à partir de la description de la dépendance, obtient le nom du bean dans BeanDefinition (qui contient déjà la représentation sous forme de chaîne du type de bean), puis les compare et, s'ils correspondent, renvoie true. S'ils ne correspondent pas, il délègue au résolveur interne.

Options de mouillage du bac de test


Au total, dans les tests, nous pouvons utiliser les options suivantes pour le mouillage du bac:

  • Java Config - il sera impĂ©ratif, encombrant, avec un passe-partout, mais, peut-ĂŞtre, aussi informatif que possible;
  • @MockBean - sera dĂ©claratif, moins volumineux que Java Config, mais il y aura toujours un petit passe-partout sous forme de champs avec des dĂ©pendances qui ne sont pas utilisĂ©s dans le test lui-mĂŞme;
  • @Automocked + rĂ©solveur personnalisĂ© - code minimum dans les tests et passe-partout, mais portĂ©e potentiellement assez Ă©troite et cela doit encore ĂŞtre Ă©crit. Mais cela peut ĂŞtre très pratique lorsque vous voulez vous assurer que le ressort crĂ©e correctement des proxys.

Ajouter des décorateurs


Notre équipe aime le modèle de conception Decorator pour sa flexibilité. En fait, certains aspects mettent en œuvre ce modèle particulier. Mais si vous configurez le contexte Spring avec des annotations et utilisez l'analyse de packages, vous rencontrerez un problème. Si vous avez plusieurs implémentations de la même interface dans le contexte, alors lorsque l'application démarre , une NoUniqueBeanDefinitionException va tomber , c'est-à-dire le printemps ne pourra pas déterminer lequel des grains doit être injecté. Ce problème a plusieurs solutions, puis nous les examinerons, mais d'abord, voyons comment notre application va changer.

Maintenant les interfaces FortuneTeller et HoroscopeTeller ont une implémentation, nous ajouterons 2 implémentations supplémentaires pour chacune des interfaces:



  • Caching ... - cache dĂ©corateur;
  • Logging ... est un dĂ©corateur forestier.

Alors, comment résolvez-vous le problème de la détermination de l'ordre des haricots?

Configuration de Java avec le décorateur de niveau supérieur


Vous pouvez à nouveau utiliser Java Config. Dans ce cas, nous décrirons les beans comme des méthodes de la classe config, et nous devrons spécifier les arguments nécessaires pour appeler le constructeur du bean comme arguments de la méthode. D'où il s'ensuit qu'en cas de changement de constructeur du bin, il faudra changer la config, ce qui n'est pas très cool. Des avantages de cette option:

  • il y aura une faible connectivitĂ© entre les dĂ©corateurs, comme la connexion entre eux sera dĂ©crite dans la configuration, c'est-Ă -dire ils ne sauront rien les uns des autres;
  • tous les changements dans l'ordre des dĂ©corateurs seront localisĂ©s en un seul endroit - la config.

Dans notre cas, Java Config ressemblera Ă  ceci:

DomainConfig.java
 @Configuration public class DomainConfig { @Bean public FortuneTeller fortuneTeller( final Map<FortuneRequest, FortuneResponse> cache, final FortuneResponseRepository fortuneResponseRepository, final Function<FortuneRequest, PersonalData> personalDataExtractor, final PersonalDataRepository personalDataRepository ) { return new LoggingFortuneTeller( new CachingFortuneTeller( new Globa(fortuneResponseRepository, personalDataExtractor, personalDataRepository), cache ) ); } @Bean public HoroscopeTeller horoscopeTeller( final Map<ZodiacSign, Horoscope> cache, final HoroscopeRepository horoscopeRepository ) { return new LoggingHoroscopeTeller( new CachingHoroscopeTeller( new Gypsy(horoscopeRepository), cache ) ); } } 


Comme vous pouvez le voir, pour chacune des interfaces, un seul bean est déclaré ici, et les méthodes contiennent dans les arguments les dépendances de tous les objets créés à l'intérieur. Dans ce cas, la logique de création de beans est assez évidente.

Qualifier


Vous pouvez utiliser l'annotation @Qualifier . Ce sera plus déclaratif que Java Config, mais dans ce cas, vous devrez spécifier explicitement le nom du bean dont dépend le bean actuel. Ce que l'inconvénient implique: une connectivité accrue entre les bacs. Et puisque la connectivité augmente, même dans le cas d'un changement dans l'ordre des décorateurs, les changements seront étalés uniformément sur le code. Autrement dit, dans le cas de l'ajout d'un nouveau décorateur, par exemple, au milieu de la chaîne, les modifications affecteront au moins 2 classes.

LoggingFortuneTeller.java
 @Primary @Component public final class LoggingFortuneTeller implements FortuneTeller { private final FortuneTeller internal; private final Logger logger; public LoggingFortuneTeller( @Qualifier("cachingFortuneTeller") @NonNull final FortuneTeller internal ) { this.internal = internal; this.logger = getLogger(internal.getClass()); } 


, , ( , FortuneTeller , ), @Primary . internal @Qualifier , — cachingFortuneTeller . .

Custom qualifier


2.5 Qualifier', . .

enum :

 public enum DecoratorType { LOGGING, CACHING, NOT_DECORATOR } 

, qualifier':

 @Qualifier @Retention(RUNTIME) public @interface Decorator { DecoratorType value() default NOT_DECORATOR; } 

: , @Qualifier , CustomAutowireConfigurer , .

Qualifier' :

CachingFortuneTeller.java
 @Decorator(CACHING) @Component public final class CachingFortuneTeller implements FortuneTeller { private final FortuneTeller internal; private final Map<FortuneRequest, FortuneResponse> cache; public CachingFortuneTeller( @Decorator(NOT_DECORATOR) final FortuneTeller internal, final Map<FortuneRequest, FortuneResponse> cache ) { this.internal = internal; this.cache = cache; } 


– , @Decorator , , – , , FortuneTeller ', – Globa .

Qualifier' - , - . , , . , - – , , .

DecoratorAutowireCandidateResolver


– ! ! :) , - , Java Config', . , - , . :

DomainConfig.java
 @Configuration public class DomainConfig { @Bean public OrderConfig<FortuneTeller> fortuneTellerOrderConfig() { return () -> asList( LoggingFortuneTeller.class, CachingFortuneTeller.class, Globa.class ); } @Bean public OrderConfig<HoroscopeTeller> horoscopeTellerOrderConfig() { return () -> asList( LoggingHoroscopeTeller.class, CachingHoroscopeTeller.class, Gypsy.class ); } } 


– Java Config' , – . , !

- . , , , . :

 @FunctionalInterface public interface OrderConfig<T> { List<Class<? extends T>> getClasses(); } 

BeanDefinitionRegistryPostProcessor


BeanDefinitionRegistryPostProcessor , BeanFactoryPostProcessor, , , , BeanDefinition'. , BeanFactoryPostProcessor, .

:

  • BeanDefinition';
  • BeanDefinition' , OrderConfig '. , .. BeanDefinition' ;
  • , OrderConfig ', BeanDefinition', , () .

BeanFactoryPostProcessor


BeanFactoryPostProcessor , BeanDefinition' , . , « Spring-».



, , – AutowireCandidateResolver':

DecoratorAutowireCandidateResolverConfigurer.java
 @Component class DecoratorAutowireCandidateResolverConfigurer implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(final ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { Assert.state(configurableListableBeanFactory instanceof DefaultListableBeanFactory, "BeanFactory needs to be a DefaultListableBeanFactory"); val beanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory; beanFactory.setAutowireCandidateResolver( new DecoratorAutowireCandidateResolver(beanFactory.getAutowireCandidateResolver()) ); } } 



DecoratorAutowireCandidateResolver


:
DecoratorAutowireCandidateResolver.java
 @RequiredArgsConstructor public final class DecoratorAutowireCandidateResolver implements AutowireCandidateResolver { private final AutowireCandidateResolver resolver; @Override public boolean isAutowireCandidate(final BeanDefinitionHolder bdHolder, final DependencyDescriptor descriptor) { val dependentType = descriptor.getMember().getDeclaringClass(); val dependencyType = descriptor.getDependencyType(); val candidateBeanDefinition = (AbstractBeanDefinition) bdHolder.getBeanDefinition(); if (dependencyType.isAssignableFrom(dependentType)) { val candidateQualifier = candidateBeanDefinition.getQualifier(OrderQualifier.class.getTypeName()); if (candidateQualifier != null) { return dependentType.getTypeName().equals(candidateQualifier.getAttribute("value")); } } return resolver.isAutowireCandidate(bdHolder, descriptor); } 


descriptor' (dependencyType) (dependentType):

 val dependentType = descriptor.getMember().getDeclaringClass(); val dependencyType = descriptor.getDependencyType(); 

bdHolder' BeanDefinition:

 val candidateBeanDefinition = (AbstractBeanDefinition) bdHolder.getBeanDefinition(); 

. , :

 dependencyType.isAssignableFrom(dependentType) 

, , .. .

BeanDefinition' :

 val candidateQualifier = candidateBeanDefinition.getQualifier(OrderQualifier.class.getTypeName()); 

, :

 if (candidateQualifier != null) { return dependentType.getTypeName().equals(candidateQualifier.getAttribute("value")); } 

– (), – false.




, :

  • Java Config – , , , ;
  • @Qualifier – , - ;
  • Custom qualifier – , Qualifier', ;
  • - – , , , .

Conclusions


, , . – : . , , , . – , JRE. , , .

, – , , - . Merci d'avoir lu!

Toutes les sources sont disponibles sur: https://github.com/monosoul/spring-di-customization .

Source: https://habr.com/ru/post/fr435364/


All Articles