Customizing dependency resolver in Spring

    Hello! My name is Andrey Nevedomsky and I am the chief engineer at SberTech. I work in a team that develops one of the system services of the ESF (Unified Frontal System). In our work, we actively use the Spring Framework, in particular its DI, and from time to time we are confronted with the fact that resolving dependencies in the spring is not “smart” for us. This article is the result of my attempts to make it smarter and generally understand how it works. I hope, and you can learn from it something new about the device of the spring.



    Before reading the article, I strongly recommend that you read the reports of Evgeny Borisov EvgenyBorisov : Spring-Ripper, Part 1 ; Spring-ripper, part 2 . There is also a playlist of them .

    Introduction


    Let's imagine that we were asked to develop a service to predict fate and horoscopes. There are several components in our service, but the main ones for us will be two:

    • Globa, which will implement the FortuneTeller interface and predict fate;




    • Gypsy, which will implement the HoroscopeTeller interface and create horoscopes.




    Also in our service there will be several endpoints (controllers) for, in fact, receiving fortune telling and horoscopes. And we will control access to our application by IP using an aspect that will be applied to controller methods and looks like this:

    RestrictionAspect.java
    @Aspect@Component@Slf4j
    publicclassRestrictionAspect{
        privatefinal Predicate<String> ipIsAllowed;
        publicRestrictionAspect(@NonNull final Predicate<String> ipIsAllowed){
            this.ipIsAllowed = ipIsAllowed;
        }
        @Before("execution(public * com.github.monosoul.fortuneteller.web.*.*(..))")
        publicvoidcheckAccess(){
            val ip = getRequestSourceIp();
            log.debug("Source IP: {}", ip);
            if (!ipIsAllowed.test(ip)) {
                thrownew 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();
        }
    }


    To verify that access from such an IP is allowed, we will be using some predicate implementation ipIsAllowed. In general, in place of this aspect there may be some other, for example, authorizing.

    So, we have developed the application and everything works great for us. But let's talk about testing now.

    How to test it?


    Let's talk about how we test the correctness of the application of aspects. We have several ways to do this.

    You can write separate tests for an aspect and controllers, without raising the spring context (which will create a proxy with an aspect for the controller, you can read more about this in the official documentation ), but in this case we will not test exactly what aspects apply to controllers and work exactly as we expect ;

    You can write tests in which we will raise the full context of our application, but in this case:

    • the tests will run for a long time, because all the bins will rise;
    • we will need to prepare valid test data that can pass through the entire chain of calls between the bins without throwing out the NPE.

    But we want to test exactly what aspect applied and does its work. We do not want to test the services called by the controller, and therefore we do not want to bother with the test data and sacrifice the launch time. Therefore, we will write tests in which we will raise only part of the context. Those. in our context, there will be a real aspect bin and a real controller bin, and everything else will be mocks.

    How to create bina-moki?


    We can create moins in spring in several ways. For clarity, as an example, we take one of the controllers of our service - PersonalizedHoroscopeTellController, its code looks like this:

    PersonalizedHoroscopeTellController.java
    @Slf4j
    @RestController@RequestMapping(
            value = "/horoscope",
            produces = APPLICATION_JSON_UTF8_VALUE
    )
    publicclassPersonalizedHoroscopeTellController{
        privatefinal HoroscopeTeller horoscopeTeller;
        privatefinal Function<String, ZodiacSign> zodiacSignConverter;
        privatefinal Function<String, String> nameNormalizer;
        publicPersonalizedHoroscopeTellController(
                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 Config with dependencies in each test


    For each test, we can write Java Config, in which we describe both the controller and aspect beans, and the beans with the controller dependencies. This way of describing bins will be imperative, since we will explicitly tell the spring how we need to create bins.

    In this case, the test for our controller will look like this:

    javaconfig / PersonalizedHoroscopeTellControllerTest.java
    @SpringJUnitConfigpublicclassPersonalizedHoroscopeTellControllerTest{
        privatestaticfinalint LIMIT = 10;
        @Autowiredprivate PersonalizedHoroscopeTellController controller;
        @Autowiredprivate Predicate<String> ipIsAllowed;
        @TestvoiddoNothingWhenAllowed(){
            when(ipIsAllowed.test(anyString())).thenReturn(true);
            controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT));
        }
        @TestvoidthrowExceptionWhenNotAllowed(){
            when(ipIsAllowed.test(anyString())).thenReturn(false);
            assertThatThrownBy(() -> controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT)))
                    .isInstanceOf(AccessDeniedException.class);
        }
        @Configuration@Import(AspectConfiguration.class)
        @EnableAspectJAutoProxypublicstaticclassConfig{
            @Beanpublic PersonalizedHoroscopeTellController personalizedHoroscopeTellController(
                    final HoroscopeTeller horoscopeTeller,
                    final Function<String, ZodiacSign> zodiacSignConverter,
                    final Function<String, String> nameNormalizer
            ){
                returnnew PersonalizedHoroscopeTellController(horoscopeTeller, zodiacSignConverter, nameNormalizer);
            }
            @Beanpublic HoroscopeTeller horoscopeTeller(){
                return mock(HoroscopeTeller.class);
            }
            @Beanpublic Function<String, ZodiacSign> zodiacSignConverter(){
                return mock(Function.class);
            }
            @Beanpublic Function<String, String> nameNormalizer(){
                return mock(Function.class);
            }
        }
    }


    This test looks quite cumbersome. In this case, we have to write Java Config for each of the controllers. Although it will be different in content, it will have the same meaning: create a controller bin and moka for its dependencies. So in fact it will be the same for all controllers. I, like any programmer, a lazy person, so I refused this option right away.

    Annotation @MockBean over each field with dependency


    The @MockBean annotation appeared in Spring Boot Test version 1.4.0. It is similar to @Mock from Mockito (and in fact it even uses it inside), with the only difference that when used @MockBean, the created mock will automatically be placed in the context of the spring. This way of declaring mocks will be declarative, since we don’t have to tell the spring exactly how to create these mocks.

    In this case, the test will look like this:

    mockbean / PersonalizedHoroscopeTellControllerTest.java
    @SpringJUnitConfigpublicclassPersonalizedHoroscopeTellControllerTest{
        privatestaticfinalint LIMIT = 10;
        @MockBeanprivate HoroscopeTeller horoscopeTeller;
        @MockBeanprivate Function<String, ZodiacSign> zodiacSignConverter;
        @MockBeanprivate Function<String, String> nameNormalizer;
        @MockBeanprivate Predicate<String> ipIsAllowed;
        @Autowiredprivate PersonalizedHoroscopeTellController controller;
        @TestvoiddoNothingWhenAllowed(){
            when(ipIsAllowed.test(anyString())).thenReturn(true);
            controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT));
        }
        @TestvoidthrowExceptionWhenNotAllowed(){
            when(ipIsAllowed.test(anyString())).thenReturn(false);
            assertThatThrownBy(() -> controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT)))
                    .isInstanceOf(AccessDeniedException.class);
        }
        @Configuration@Import({PersonalizedHoroscopeTellController.class, RestrictionAspect.class, RequestContextHolderConfigurer.class})
        @EnableAspectJAutoProxypublicstaticclassConfig{
        }
    }


    In this embodiment, there is still Java Config, but it is much more compact. Among the shortcomings, I had to declare fields with controller dependencies (fields with annotation @MockBean), even though they are not used further in the test. Well, if you use Spring Boot version for some reason lower than 1.4.0, then you cannot use this annotation.

    Therefore, I had an idea for one more moistening option. I would like it to work like this ...

    Annotation @Automocked over the dependent component


    I would like us to have an annotation @Automockedthat I could put only above the field with the controller, and then mocks would be automatically created for this controller and placed in context.

    The test in this case could look like this:

    automocked / PersonalizedHoroscopeTellControllerTest.java
    @SpringJUnitConfig@ContextConfiguration(classes = AspectConfiguration.class)
    @TestExecutionListeners(listeners = AutomockTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
    publicclassPersonalizedHoroscopeTellControllerTest{
        privatestaticfinalint LIMIT = 10;
        @Automockedprivate PersonalizedHoroscopeTellController controller;
        @Autowiredprivate Predicate<String> ipIsAllowed;
        @TestvoiddoNothingWhenAllowed(){
            when(ipIsAllowed.test(anyString())).thenReturn(true);
            controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT));
        }
        @TestvoidthrowExceptionWhenNotAllowed(){
            when(ipIsAllowed.test(anyString())).thenReturn(false);
            assertThatThrownBy(() -> controller.tell(randomAlphabetic(LIMIT), randomAlphabetic(LIMIT)))
                    .isInstanceOf(AccessDeniedException.class);
        }
    }


    As you can see, this option is the smallest one presented here, there is only a controller bin (plus a predicate for the aspect) above which there is an annotation @Automocked, and all the magic of creating bins and placing them in context is written once and can be used in all tests.

    How it works?


    Let's see how it works and what we need for this.

    TestExecutionListener


    In the spring there is such an interface - TestExecutionListener . It provides an API for embedding into the process of test execution at its different stages, for example, when creating a test class instance, before or after calling the test method, etc. He has several out-of-box implementations. For example, DirtiesContextTestExecutionListener , which performs context clearing in case you have set the appropriate annotation; DependencyInjectionTestExecutionListener - performs injection dependencies in tests, etc. To apply your custom Listener to the test, you need to put an annotation above it @TestExecutionListenersand indicate your implementation.

    Ordered


    Also in the spring is the Ordered interface . It is used to indicate that objects should be ordered in some way. For example, when you have several implementations of the same interface and you want to inject them into a collection, then in this collection they will be ordered according to Ordered. In the case of TestExecutionListeners, this annotation indicates the order in which they should be applied.

    So, our Listener will implement 2 interfaces: TestExecutionListener and Ordered . We call it AutomockTestExecutionListener and it will look like this:

    AutomockTestExecutionListener.java
    @Slf4j
    publicclassAutomockTestExecutionListenerimplementsTestExecutionListener, Ordered{
        @OverridepublicintgetOrder(){
            return1900;
        }
        @OverridepublicvoidprepareTestInstance(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)
                );
            }
        }
        privatevoidsetByNameCandidateResolver(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> voidcreateMocksForParameters(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);
            }
        }
    }


    What's going on here? First, in the method prepareTestInstance()it finds all the fields with the annotation @Automocked:

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

    Then makes these fields writable:

    makeAccessible(field);

    Then it findConstructorToAutomock()finds a suitable constructor in the method :

    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;

    In our case, either a constructor with the @Autowired annotation or the constructor with the most arguments will be suitable .

    Then the found constructor is passed as an argument to the method createBeanWithMocks(), which in turn calls the method createMocksForParameters(), where the creation of mocks for the constructor arguments takes place and their registration in the context:

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

    It is important to note that the string representation of the type of the argument (along with generics) will be used as the name of the bean. That is, for the type argument, the packages.Function<String, String>string representation will be a string"packages.Function<java.lang.String, java.lang.String>" . This is important, we will come back to this.

    After creating mocks for all arguments and registering them in context, we return to creating the bean of the dependent class (that is, the controller in our case):

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

    You should also pay attention to the fact that we used Order 1900 . This is necessary because our Listener must be called after clearing the DirtiesContextBeforeModesTestExecutionListener context (order = 1500) and before the injection of DependencyInjectionTestExecutionListener (order = 2000) dependencies , because our Listener creates new bins.

    AutowireCandidateResolver


    AutowireCandidateResolver is used to determine if BeanDefinition matches the description of the dependency. He has several implementations out of the box, among them:


    At the same time, the implementation of "out of the box" is the nested doll of inheritance, they expand each other. We will write a decorator, because it's more flexible.

    Works resolver as follows:

    1. Spring takes a dependency descriptor - DependencyDescriptor ;
    2. Then it takes all the BeanDefinition 's of the appropriate class;
    3. Enumerates received BeanDefinitions by calling the isAutowireCandidate()resolver method ;
    4. Depending on whether the bean description fits the dependency description or not, the method returns true or false.

    Why did you need your resolver?


    Now let's see, why did we need our own resolver in the example of our controller?

    publicclassPersonalizedHoroscopeTellController{
        privatefinal HoroscopeTeller horoscopeTeller;
        privatefinal Function<String, ZodiacSign> zodiacSignConverter;
        privatefinal Function<String, String> nameNormalizer;
        publicPersonalizedHoroscopeTellController(
                final HoroscopeTeller horoscopeTeller,
                final Function<String, ZodiacSign> zodiacSignConverter,
                final Function<String, String> nameNormalizer
        ){
            this.horoscopeTeller = horoscopeTeller;
            this.zodiacSignConverter = zodiacSignConverter;
            this.nameNormalizer = nameNormalizer;
        }

    As you can see, it has two dependencies of the same type - Function , but with different generics. In one case, String and ZodiacSign , in the other, String and String . And the problem with this is that the Mockito does not know how to create mocks with regard to generics . Those. if we create mocks for these dependencies and put them in context, then Spring will not be able to inject them into this class, since they will not contain information about generics. And we will see an exception that there is more than one Function class bean in the context.. This is the problem we will solve with the help of our resolver. After all, as you remember, in our implementation of Listener, we used the type with generics as the name of a bin, which means all we need to do is teach the spring to compare the type of dependency with the name of the bin.

    AutomockedBeanByNameAutowireCandidateResolver


    So, our resolver will do exactly what I wrote above, and the implementation of the method isAutowireCandidate()will look like this:

    AutowireCandidateResolver.isAutowireCandidate ()
    @OverridepublicbooleanisAutowireCandidate(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) {
            returntrue;
        }
        return candidateResolver.isAutowireCandidate(beanDefinitionHolder, descriptor);
    }


    Here it gets a string representation of the dependency type from the dependency description, gets the name of the bean from BeanDefinition (which already contains the string representation of the type of the bin), then compares them, and if they match, it returns true. If not matched, it delegates to an internal resolver.

    Options for mocking bins in the tests


    So, in the tests, we can use the following options for mopping bins:

    • Java Config - it will be imperative, cumbersome, with the boilerplate, but, perhaps, as informative as possible;
    • @MockBean - it will be declarative, less cumbersome than Java Config, but there will still be a small boilerplate in the form of fields with dependencies that are not used in the test itself;
    • @Automocked+ custom resolver - minimum code in tests and boilerplate, but potentially quite narrow scope and it still needs to be written. But it can be very convenient where you want to make sure that the spring correctly creates a proxy.

    Add decorators


    We in our team really love the “Decorator” design pattern for its flexibility. Essentially, aspects implement this particular pattern. But if you configure the context with annotations and use package scan, you will encounter a problem. If you have several implementations of the same interface in the context, at the start of the application, a NoUniqueBeanDefinitionException will fall out , i.e. Spring can not figure out which of the bins where it should be injected. There are several solutions to this problem, and then we will look at them, but first let's see how our application changes.

    Now the FortuneTeller and HoroscopeTeller interfaces there is one implementation, we will add 2 more implementations for each of the interfaces:



    • Caching ... - caching decorator;
    • Logging ... is a logging decorator.

    So how to solve the problem of determining the order of bins?

    Java Config with top level decorator


    You can use Java Config again. In this case, we will describe the beans in the form of methods of the config class, and we will have to specify the arguments necessary to call the constructor of the bean as method arguments. From which it follows that in case of a change in the bean constructor, we will have to change the config, which is not very cool. Of the advantages of this option:

    • between the decorators will be low connectivity, because the connection between them will be described in the config, i.e. about each other they will not know anything;
    • all changes in the order of decorators will be localized in one place - the config.

    In our case, Java Config will look like this:

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


    As you can see, for each of the interfaces, only one bin is declared here, and the methods contain in the arguments the dependencies of all objects created inside. In this case, the logic for creating bins is fairly obvious.

    Qualifier


    You can use the @Qualifier annotation . This will be more declarative than Java Config, but in this case you will need to explicitly specify the name of the bean on which the current bean depends. This implies a disadvantage: the connectivity between the bins increases. And since connectivity increases, even in the case of a change in the order of decorators, the changes will be spread evenly across the code. That is, in the case of adding a new decorator, for example, in the middle of the chain, the changes will affect at least 2 classes.

    LoggingFortuneTeller.java
    @Primary@ComponentpublicfinalclassLoggingFortuneTellerimplementsFortuneTeller{
        privatefinal FortuneTeller internal;
        privatefinal Logger logger;
        publicLoggingFortuneTeller(
                @Qualifier("cachingFortuneTeller")
                @NonNull final FortuneTeller internal
        ) {
            this.internal = internal;
            this.logger = getLogger(internal.getClass());
        }


    By the example of a logging decorator for a fortune teller, it can be seen that, since it is top-level (it should be injected into all classes using FortuneTeller , for example, into controllers), the abstract @Primary stands above it . And above the argument of the constructor internal there is an annotation @Qualifierindicating the name of the bean on which it depends - cachingFortuneTeller . In our case, the caching decorator should be injected into it.

    Custom qualifier


    Starting from version 2.5, the spring provides an opportunity to declare your own qualifiers, which we can use. It can look like this.

    First we will declare an enum with the types of decorators:

    publicenum DecoratorType {
        LOGGING,
        CACHING,
        NOT_DECORATOR
    }

    Then we will announce our annotation, which will be qualifier:

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

    Note: for your annotation to work, you must either annotate it @Qualifieror declare a customAutowireConfigurer bean , which can be passed to your annotation class.

    Well, if the custom Qualifier is used, the decorators themselves will look like this:

    CachingFortuneTeller.java
    @Decorator(CACHING)
    @ComponentpublicfinalclassCachingFortuneTellerimplementsFortuneTeller{
        privatefinal FortuneTeller internal;
        privatefinal Map<FortuneRequest, FortuneResponse> cache;
        publicCachingFortuneTeller(
                @Decorator(NOT_DECORATOR)final FortuneTeller internal,
                final Map<FortuneRequest, FortuneResponse> cache
        ) {
            this.internal = internal;
            this.cache = cache;
        }


    The caching decorator is the middle one in our chain, so there is an annotation above it @Decoratorindicating that it is caching, and in its constructor the same annotation indicating that it should not be injected by the decorator , that is, the default implementation of FortuneTeller , our case is Globa .

    This option looks like something better, something worse compared to Qualifiers. Worse, because now the annotation must be placed not only in the constructor, but also above the class itself. Better, because the connectivity between the bins is still a bit lower - the bins now do not know the name of the decorator who should inject into them, they only know the type of this decorator.

    DecoratorAutowireCandidateResolver


    The last option is to write your resolver! After all, we are here just for this! :) I would like us to have some way to explicitly declare the order of decorators, as in Java Config, without declaring all dependencies of these decorators. For example, with the help of some custom bin in the config, which would contain the order of decorators. It might look like this:

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


    It looks like a good idea - we get the advantages of Java Config in the form of more explicit ads and localization, while getting rid of its disadvantage - cumbersome. Let's see what we need for this!

    To begin, we need some way to declare order. It can be, for example, an interface with one method that would return an ordered list of classes. It can look like this:

    @FunctionalInterfacepublicinterfaceOrderConfig<T> {
        List<Class<? extends T>> getClasses();
    }

    BeanDefinitionRegistryPostProcessor


    We will also need a BeanDefinitionRegistryPostProcessor , which extends BeanFactoryPostProcessor, is called before it, and, according to the documentation, can be used to register new BeanDefinitions. Not that you can't use BeanFactoryPostProcessor for this, it just seems more correct.

    He will do the following:

    • will iterate over all BeanDefinitions;
    • will remove the BeanDefinitions of the classes declared inside OrderConfig . This is necessary because these classes can have spring stereotype annotations and BeanDefinitions for scanning packages could be created for them;
    • will create for classes declared inside OrderConfig 's, new BeanDefinitions that will contain an annotation pointing to the parent bin (decorator) of the class.

    BeanFactoryPostProcessor


    Do not we dispense side and BeanFactoryPostProcessor , which is used to manipulate BeanDefinition'ami before the start initialization beans. Perhaps this class is best known as the “victim of the Ripper”.



    All he does for us is to register our implementation of AutowireCandidateResolver in a factory:

    DecoratorAutowireCandidateResolverConfigurer.java
    @ComponentclassDecoratorAutowireCandidateResolverConfigurerimplementsBeanFactoryPostProcessor{
        @OverridepublicvoidpostProcessBeanFactory(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


    Well, the resolver itself will look like this:
    DecoratorAutowireCandidateResolver.java
    @RequiredArgsConstructorpublicfinalclassDecoratorAutowireCandidateResolverimplementsAutowireCandidateResolver{
        privatefinal AutowireCandidateResolver resolver;
        @OverridepublicbooleanisAutowireCandidate(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);
        }


    It gets the type of dependency (dependencyType) and the type of the dependent class (dependentType) from descriptor:

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

    It then gets from bdHolder BeanDefinition:

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

    Compares the type of dependency and the type of the dependent class. Thus, we verify that we are dealing with a decorator:

    dependencyType.isAssignableFrom(dependentType)

    If they do not match, then delegate further check to the internal resolver, since we are not dealing with a decorator.

    Gets an annotation from BeanDefinition with an indication of the class of the parent decorator:

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

    And if there is a summary, it compares the class specified in it with the dependent class:

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

    If they match, we find a suitable bin (decorator); if not, we return false.

    Other options for implementation


    • Perhaps it would be better to implement a custom configuration bin through extending the ConfigurationClassBeanDefinitionReader 's functionality ;
    • Instead of a custom annotation indicating the class of the decorator's parent bin could be the other way round, in the parent BeanDefintions you can put Qualifiers in the designers. But in this case, I would have to explicitly describe all the arguments of the constructor and what should be injected into them, but I did not want to do that.

    Options for determining the order of injection


    So, to determine the order of injection in the spring, you can use the following options:

    • Java Config - it will be imperative, slightly cumbersome, you will have to change when the dependencies of the bins change, but it provides low connectivity and local changes;
    • @Qualifier - declarative option, but with high connectivity and because of this, changes that are not localized in one place;
    • Custom qualifier - a little less coherence than with ordinary Qualifiers, but more code;
    • Свой резолвер и кастомный конфиг-бин – императивно, низкая связность, изменения локализованы в одном месте, но придется написать и поддерживать.

    Выводы


    As you can see, customization of dependency resolvings can help you make the spring much more flexible, sharpen it specifically for your needs. There can be at least two views on such a way of solving a problem: conservative and liberal. The conservative is that adding your own logic to the spring can make your code less obvious to newbies and possibly less followed. On the other hand, following this logic, you can generally not write your classes and use only primitives from the JRE. Or do not use the spring at all, because it may be incomprehensible to beginners how it works.

    In any case, to use it or not to use it is up to you, but I hope that you managed to learn something new from this article. Thank you for reading!

    All sources are available at the link:https://github.com/monosoul/spring-di-customization .

    Also popular now: