Anpassen der Abhängigkeitsauflösung im Frühjahr

Hallo! Mein Name ist Andrey Nevedomsky und ich bin der Chefingenieur in SberTekh. Ich arbeite in einem Team, das einen der Systemdienste des ESF (Unified Frontal System) entwickelt. In unserer Arbeit nutzen wir aktiv das Spring Framework, insbesondere dessen DI, und sehen uns von Zeit zu Zeit mit der Tatsache konfrontiert, dass das Auflösen von Abhängigkeiten im Frühjahr für uns nicht klug genug ist. Dieser Artikel ist das Ergebnis meiner Versuche, ihn intelligenter zu gestalten und allgemein zu verstehen, wie er funktioniert. Ich hoffe, Sie können daraus etwas Neues über das Gerät des Frühlings lernen.



Bevor Sie den Artikel lesen, empfehle ich Ihnen dringend, die Berichte von Boris Yevgeny EvgenyBorisov zu lesen: Spring the Ripper, Teil 1 ; Federtrenner, Teil 2 . Es gibt noch eine Wiedergabeliste von ihnen .

Einführung


Stellen wir uns vor, wir wurden gebeten, einen Dienst zur Vorhersage von Schicksal und Horoskopen zu entwickeln. Es gibt mehrere Komponenten in unserem Service, aber die wichtigsten für uns sind zwei:

  • Globa, das die FortuneTeller-Schnittstelle implementiert und das Schicksal vorhersagt;




  • Gypsy, der die HoroscopeTeller-Schnittstelle implementiert und Horoskope erstellt.




Auch in unserem Dienst wird es mehrere Endpunkte (Controller) geben, um tatsächlich Wahrsagerei und Horoskope zu erhalten. Außerdem werden wir den Zugriff auf unsere Anwendung über IP mithilfe eines Aspekts steuern, der auf Controller-Methoden angewendet wird und ungefähr so ​​aussieht:

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


Um zu überprüfen, ob der Zugriff von einer solchen IP aus zulässig ist, verwenden wir eine Implementierung des Prädikats ipIsAllowed . Im Allgemeinen kann es auf der Website dieses Aspekts eine andere, beispielsweise autorisierende Funktion geben.

Also haben wir die Anwendung entwickelt und alles funktioniert gut für uns. Aber lassen Sie uns jetzt über das Testen sprechen.

Wie teste ich es?


Lassen Sie uns darüber sprechen, wie wir die Anwendung von Aspekten testen können. Wir haben verschiedene Möglichkeiten, dies zu tun.

Sie können separate Tests für einen Aspekt und für Controller schreiben, ohne den Federkontext zu erhöhen (wodurch nur ein Proxy mit einem Aspekt für den Controller erstellt wird. Weitere Informationen hierzu finden Sie in der offiziellen Dokumentation ). In diesem Fall werden wir jedoch nicht genau testen, auf welche Aspekte korrekt angewendet wird Controller und arbeiten genau so, wie wir es erwarten ;

Sie können Tests schreiben, in denen wir den vollständigen Kontext unserer Anwendung ansprechen, aber in diesem Fall:

  • Das Ausführen von Tests wird lange dauern, weil alle Behälter werden steigen;
  • Wir müssen gültige Testdaten vorbereiten, die die gesamte Anrufkette zwischen Bins durchlaufen können, ohne gleichzeitig NPE zu werfen.

Aber wir wollen genau testen, was der Aspekt angewendet hat und seine Arbeit macht. Wir möchten die vom Controller aufgerufenen Dienste nicht testen und möchten daher nicht durch Testdaten verwirrt werden und die Startzeit opfern. Daher werden wir Tests schreiben, in denen wir nur einen Teil des Kontexts ansprechen. Das heißt, In unserem Kontext wird es eine echte Aspekt-Bean und eine echte Controller-Bean geben, und alles andere wird Mokami sein.

Wie erstelle ich Moka Bohnen?


Es gibt verschiedene Möglichkeiten, im Frühjahr Moka-Bohnen herzustellen. Zur Verdeutlichung nehmen wir als Beispiel einen der Controller unseres Dienstes - PersonalizedHoroscopeTellController . Der Code sieht folgendermaßen aus:

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


Java-Konfiguration mit Abhängigkeiten in jedem Test


Für jeden Test können wir Java Config schreiben, in dem wir sowohl die Controller- als auch die Aspect-Beans und die Beans mit den Controller-Abhängigkeits-Moks beschreiben. Diese Art der Beschreibung der Bohnen ist unerlässlich, da wir dem Frühling ausdrücklich mitteilen werden, wie wir die Bohnen herstellen müssen.

In diesem Fall sieht der Test für unseren Controller folgendermaßen aus:

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


Ein solcher Test sieht ziemlich umständlich aus. In diesem Fall müssen wir Java Config für jeden der Controller schreiben. Obwohl der Inhalt unterschiedlich sein wird, hat er dieselbe Bedeutung: Erstellen Sie eine Controller-Bean und ein Moki für ihre Abhängigkeiten. Im Wesentlichen ist es also für alle Controller gleich. Ich bin, wie jeder Programmierer, eine faule Person, deshalb habe ich diese Option sofort abgelehnt.

@ MockBean-Annotation über jedes Feld mit Abhängigkeit


Die Annotation @MockBean wurde in Spring Boot Test Version 1.4.0 angezeigt. Es ähnelt @Mock von Mockito (und verwendet es sogar intern), mit dem einzigen Unterschied, dass bei Verwendung von @MockBean das erstellte Mock automatisch in den Spring-Kontext gestellt wird. Diese Methode zum Deklarieren von Mok ist deklarativ, da wir Spring nicht genau sagen müssen, wie diese Mok erstellt werden sollen.

In diesem Fall sieht der Test folgendermaßen aus:

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 { } } 


In dieser Option gibt es noch Java Config, aber es ist viel kompakter. Unter den Mängeln - Ich musste Felder mit Controller-Abhängigkeiten deklarieren (Felder mit @MockBean Annotation), obwohl sie im Test nicht weiter verwendet werden. Wenn Sie aus irgendeinem Grund eine Spring Boot-Version unter 1.4.0 verwenden, können Sie diese Anmerkung nicht verwenden.

Daher kam mir eine Idee für eine andere Option zum Rauchen. Ich möchte, dass es so funktioniert ...

@Automocked Annotation über abhängige Komponente


Ich möchte, dass wir die Annotation @Automocked , die ich nur mit dem Controller über das Feld setzen kann, und dann wird moki automatisch für diesen Controller erstellt und in den Kontext gestellt.

Der Test in diesem Fall könnte folgendermaßen aussehen:

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


Wie Sie sehen können, ist diese Option die kompakteste der vorgestellten, es gibt nur eine Controller-Bean (plus ein Prädikat für einen Aspekt), @Automocked Annotation @Automocked befindet sich @Automocked , und die ganze Magie, Beans zu erstellen und in den Kontext zu stellen, wird einmal geschrieben und kann in allen verwendet werden Tests.

Wie funktioniert es


Mal sehen, wie es funktioniert und was wir dafür brauchen.

TestExecutionListener


Im Frühjahr gibt es eine solche Schnittstelle - TestExecutionListener . Es bietet eine API zum Einbetten in den Testausführungsprozess in seinen verschiedenen Phasen, z. B. beim Erstellen einer Instanz einer Testklasse, vor oder nach dem Aufrufen einer Testmethode usw. Er hat mehrere sofort einsatzbereite Implementierungen. Beispiel: DirtiesContextTestExecutionListener , der den Kontext bereinigt, wenn Sie die entsprechende Anmerkung einfügen . DependencyInjectionTestExecutionListener - Führt die Abhängigkeitsinjektion in Tests usw. durch. Um Ihren benutzerdefinierten Listener auf den Test anzuwenden, müssen Sie die Annotation @TestExecutionListeners darüber setzen und Ihre Implementierung angeben.

Bestellt


Im Frühjahr gibt es auch eine geordnete Schnittstelle. Es wird verwendet, um anzugeben, dass Objekte auf irgendeine Weise sortiert werden sollen. Wenn Sie beispielsweise mehrere Implementierungen derselben Schnittstelle haben und diese in eine Sammlung einfügen möchten, werden sie in dieser Sammlung gemäß der Reihenfolge sortiert. Im Fall von TestExecutionListener gibt diese Anmerkung an, in welcher Reihenfolge sie angewendet werden sollen.

Unser Listener implementiert also zwei Schnittstellen: TestExecutionListener und Ordered . Wir nennen es AutomockTestExecutionListener und es wird so aussehen:

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


Was ist hier los? Zunächst werden in der Methode prepareTestInstance() alle Felder mit der Annotation @Automocked :

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

Dann werden diese Felder beschreibbar:

 makeAccessible(field); 

Dann findet es in der findConstructorToAutomock() -Methode den entsprechenden Konstruktor:

 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; 

Ein geeigneter Konstruktor ist in unserem Fall entweder der Konstruktor mit der Annotation @Autowired oder der Konstruktor mit der größten Anzahl von Argumenten.

Anschließend wird der gefundene Konstruktor als Argument an die Methode createBeanWithMocks() , die wiederum die Methode createMocksForParameters() , wobei die Mocks für die Konstruktorargumente erstellt und im Kontext registriert werden:

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

Es ist wichtig zu beachten, dass eine Zeichenfolgendarstellung des Argumenttyps (zusammen mit Generika) als Name des Bin verwendet wird. Das heißt, für ein Argument vom Typ packages.Function<String, String> Zeichenfolgendarstellung die Zeichenfolge "packages.Function<java.lang.String, java.lang.String>" . Dies ist wichtig, wir werden darauf zurückkommen.

Nachdem wir Mocks für alle Argumente erstellt und im Kontext registriert haben, kehren wir zum Erstellen der Bean der abhängigen Klasse (d. H. Des Controllers in unserem Fall) zurück:

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

Sie sollten auch darauf achten, dass wir Order 1900 verwendet haben . Dies liegt daran, dass unser Listener nach dem Löschen des Ohm-Kontexts von DirtiesContextBeforeModesTestExecutionListener (order = 1500) und vor der Abhängigkeitsinjektion von DependencyInjectionTestExecutionListener (order = 2000) aufgerufen werden sollte, da unser Listener neue Bins erstellt.

AutowireCandidateResolver


Mit AutowireCandidateResolver wird ermittelt, ob die BeanDefinition mit der Abhängigkeitsbeschreibung übereinstimmt . Er hat mehrere Implementierungen "out of the box", darunter:


Gleichzeitig ist die Implementierung "out of the box" eine russische Puppe aus der Vererbung, dh sie erweitern sich gegenseitig. Wir werden einen Dekorateur schreiben, weil es ist flexibler.

Der Resolver funktioniert wie folgt:

  1. Spring verwendet einen Abhängigkeitsdeskriptor - DependencyDescriptor ;
  2. Dann werden alle BeanDefinitions der entsprechenden Klasse benötigt.
  3. Iteriert über die empfangenen BeanDefinitions und ruft die isAutowireCandidate() -Methode des Resolvers auf.
  4. Je nachdem, ob die Beschreibung der Bean mit der Beschreibung der Abhängigkeit übereinstimmt oder nicht, gibt die Methode true oder false zurück.

Warum brauchten Sie Ihren Resolver?


Lassen Sie uns nun am Beispiel unseres Controllers sehen, warum wir unseren Resolver brauchten.

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

Wie Sie sehen können, gibt es zwei Abhängigkeiten des gleichen Typs - Funktion , jedoch mit unterschiedlichen Generika. In einem Fall String und ZodiacSign , in dem anderen String und String . Das Problem dabei ist, dass Mockito unter Berücksichtigung von Generika keine Moks erstellen kann . Das heißt, Wenn wir Mokas für diese Abhängigkeiten erstellen und sie in einen Kontext setzen, kann Spring sie nicht in diese Klasse einfügen, da sie keine Informationen zu Generika enthalten. Und wir werden die Ausnahme sehen, dass es im Kontext mehr als eine Bean der Funktionsklasse gibt . Genau dieses Problem werden wir mit Hilfe unseres Resolvers lösen. Wie Sie sich erinnern, haben wir in unserer Implementierung von Listener einen Typ mit Generika als Namen des Fachs verwendet. Dies bedeutet, dass wir dem Frühling nur beibringen müssen, den Typ der Abhängigkeit mit dem Namen des Fachs zu vergleichen.

AutomockedBeanByNameAutowireCandidateResolver


Unser Resolver wird also genau das tun, was ich oben geschrieben habe, und die Implementierung der isAutowireCandidate() -Methode sieht folgendermaßen aus:

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


Hier erhält er die Zeichenfolgendarstellung des Abhängigkeitstyps aus der Abhängigkeitsbeschreibung, erhält den Bean-Namen von BeanDefinition (die bereits die Zeichenfolgendarstellung des Bean-Typs enthält), vergleicht sie und gibt sie zurück, wenn sie übereinstimmen. Wenn sie nicht übereinstimmen, wird sie an den internen Resolver delegiert.

Optionen zur Benetzung des Testbehälters


Insgesamt können wir in Tests die folgenden Optionen für die Behälterbenetzung verwenden:

  • Java Config - es wird zwingend, umständlich, mit einem Boilerplate, aber vielleicht so informativ wie möglich;
  • @MockBean - ist deklarativ, weniger sperrig als Java Config, aber es gibt immer noch eine kleine Boilerplate in Form von Feldern mit Abhängigkeiten, die im Test selbst nicht verwendet werden.
  • @Automocked + benutzerdefinierter Resolver - Mindestcode in Tests und Boilerplate, aber möglicherweise recht enger Umfang, und dieser muss noch geschrieben werden. Es kann jedoch sehr praktisch sein, wenn Sie sicherstellen möchten, dass die Feder die Proxys korrekt erstellt.

Dekorateure hinzufügen


Unser Team liebt die Decorator- Designvorlage wegen ihrer Flexibilität. Tatsächlich implementieren Aspekte dieses spezielle Muster. Wenn Sie jedoch den Spring-Kontext mit Anmerkungen konfigurieren und den Paketscan verwenden, tritt ein Problem auf. Wenn Sie mehrere Implementierungen derselben Schnittstelle im Kontext haben, fällt beim Start der Anwendung eine NoUniqueBeanDefinitionException aus , d. H. Der Frühling kann nicht herausfinden, welche der Bohnen injiziert werden sollen. Dieses Problem hat mehrere Lösungen, und dann werden wir sie betrachten, aber zuerst wollen wir herausfinden, wie sich unsere Anwendung ändern wird.

Jetzt haben die FortuneTeller- und HoroscopeTeller- Schnittstellen eine Implementierung. Wir werden für jede der Schnittstellen zwei weitere Implementierungen hinzufügen:



  • Caching ... - Caching Dekorateur;
  • Logging ... ist ein Logging-Dekorateur.

Wie lösen Sie das Problem der Bestimmung der Reihenfolge der Bohnen?

Java-Konfiguration mit Top Level Decorator


Sie können Java Config erneut verwenden. In diesem Fall beschreiben wir die Beans als Methoden der Konfigurationsklasse und müssen die Argumente angeben, die erforderlich sind, um den Konstruktor der Bean als Argumente für die Methode aufzurufen. Daraus folgt, dass wir im Falle einer Änderung im Konstruktor des Bin die Konfiguration ändern müssen, was nicht sehr cool ist. Von den Vorteilen dieser Option:

  • es wird eine geringe Konnektivität zwischen Dekorateuren geben, wie Die Verbindung zwischen ihnen wird in der Konfiguration beschrieben, d.h. sie werden nichts voneinander wissen;
  • Alle Änderungen in der Reihenfolge der Dekorateure werden an einem Ort lokalisiert - der Konfiguration.

In unserem Fall sieht Java Config folgendermaßen aus:

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


Wie Sie sehen können, wird hier für jede der Schnittstellen nur eine Bean deklariert, und die Methoden enthalten in den Argumenten die Abhängigkeiten aller darin erstellten Objekte. In diesem Fall ist die Logik zum Erstellen von Beans ziemlich offensichtlich.

Qualifikation


Sie können die Annotation @Qualifier verwenden . Dies ist deklarativer als Java Config. In diesem Fall müssen Sie jedoch den Namen der Bean explizit angeben, von der die aktuelle Bean abhängt. Was der Nachteil bedeutet: erhöhte Konnektivität zwischen Bins. Und da die Konnektivität auch bei einer Änderung der Reihenfolge der Dekorateure zunimmt, werden die Änderungen gleichmäßig über den Code verschmiert. Das heißt, wenn Sie beispielsweise in der Mitte der Kette einen neuen Dekorateur hinzufügen, wirken sich die Änderungen auf mindestens zwei Klassen aus.

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', ;
  • - – , , , .


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

, – , , - . Danke fürs Lesen!

: https://github.com/monosoul/spring-di-customization .

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


All Articles