📜 ⬆️ ⬇️

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 him 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:
')


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


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

So, we 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 it 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:


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


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 essentially it will be the same for all controllers. I, like any programmer, a lazy person, so I immediately refused this option.

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 using @MockBean , the created mock will be automatically placed in the context. 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
 @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 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 the @MockBean annotation), even though they are not used further in the test. Well, if you use Spring Boot version lower than 1.4.0 for some reason, 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 the @Automocked annotation, which I could only put over 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) 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); } } 


As you can see, this option is the most compact of those presented, there is only a controller bin (plus a predicate for the aspect), which is @Automocked annotation, 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 a test at different stages in its execution, for example, when creating a test class instance, before or after calling a 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 @TestExecutionListeners over it and specify 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 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); } } } 


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

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

Then makes these fields writable:

 makeAccessible(field); 

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

 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 the 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 createBeanWithMocks() method, which in turn calls the createMocksForParameters() method, where the creation of mocks for the constructor arguments and their registration in the context occurs:

 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 bin. That is, for the argument of the packages.Function<String, String> string representation will be the 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 should be called after clearing the DirtiesContextBeforeModesTestExecutionListener context (order = 1500) and before the injection of DependencyInjectionTestExecutionListener dependencies (order = 2000), because our Listener creates new bins.

AutowireCandidateResolver


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


At the same time, the implementation “out of the box” is a nested doll of inheritance, i.e. 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() 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 we even needed a resolver using our controller as an example.

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

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 bean of the Function class 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 bean, which means that all we need to do is teach the spring to compare the type of dependency with the name of the bean.

AutomockedBeanByNameAutowireCandidateResolver


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

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


Here he 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:


Add decorators


We in our team very much love the “Decorator” design pattern for its flexibility. Essentially, aspects implement this particular pattern. But if you configure the context with annotations for spring 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 beans 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 have one implementation, we will add 2 more implementations for each of the interfaces:




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 invoke the constructor of the bean as method arguments. From which it follows that in case of a change in the constructor of a bin, we will have to change the config, which is not very cool. Of the advantages of this option:


In our case, Java Config will look like this:

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


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, if you add a new decorator, for example, in the middle of the chain, the changes will affect at least 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, .

:


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.




, :


findings


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

, – , , - . !

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

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


All Articles