Customizando a resolução de dependência no Spring

Oi Meu nome é Andrey Nevedomsky e sou o engenheiro-chefe da SberTekh. Trabalho em equipe que desenvolve um dos serviços de sistema do ESF (Sistema Frontal Unificado). Em nosso trabalho, usamos ativamente o Spring Framework, em particular seu DI, e de tempos em tempos nos deparamos com o fato de que resolver dependências no Spring não é suficientemente inteligente para nós. Este artigo é o resultado de minhas tentativas de torná-lo mais inteligente e geralmente entender como ele funciona. Espero que você possa aprender algo novo sobre o dispositivo da primavera.



Antes de ler o artigo, recomendo fortemente que você leia os relatórios de Boris Yevgeny EvgenyBorisov : Spring Ripper, Parte 1 ; Estripador de mola, parte 2 . Ainda existe uma lista de reprodução deles .

1. Introdução


Vamos imaginar que nos pediram para desenvolver um serviço para prever o destino e os horóscopos. Existem vários componentes em nosso serviço, mas os principais para nós serão dois:

  • Globa, que implementará a interface do FortuneTeller e preverá o destino;




  • Gypsy, que implementará a interface HoroscopeTeller e criará horóscopos.




Também em nosso serviço, haverá vários pontos de extremidade (controladores) para, de fato, obter adivinhações e horóscopos. E também controlaremos o acesso ao nosso aplicativo por IP usando um aspecto que será aplicado aos métodos do controlador e terá a seguinte aparência:

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


Para verificar se o acesso desse IP é permitido, usaremos alguma implementação do predicado ipIsAllowed . Em geral, no site desse aspecto, pode haver outros, por exemplo, autorizando.

Por isso, desenvolvemos o aplicativo e tudo funciona muito bem para nós. Mas vamos falar sobre testes agora.

Como testá-lo?


Vamos falar sobre como podemos testar a aplicação de aspectos. Temos várias maneiras de fazer isso.

Você pode escrever testes separados para um aspecto e para os controladores, sem elevar o contexto da mola (que apenas criará um proxy com um aspecto para o controlador, você pode ler mais sobre isso na documentação oficial), mas, neste caso, não testaremos exatamente quais aspectos são aplicados corretamente controladores e funcionam exatamente como esperamos ;

Você pode escrever testes nos quais elevaremos o contexto completo de nossa aplicação, mas neste caso:

  • A execução de testes levará muito tempo, porque todas as caixas subirão;
  • precisaremos preparar dados de teste válidos que possam passar por toda a cadeia de chamadas entre compartimentos sem gerar NPE ao mesmo tempo.

Mas queremos testar exatamente o que o aspecto aplicou e está fazendo seu trabalho. Não queremos testar os serviços chamados pelo controlador e, portanto, não queremos ficar intrigados com os dados de teste e sacrificar o tempo de inicialização. Portanto, escreveremos testes nos quais levantaremos apenas parte do contexto. I.e. em nosso contexto, haverá um bean de aspecto real e um bean de controlador real, e todo o resto será mokami.

Como criar beans moka?


Existem várias maneiras de criar feijão moka na primavera. Para maior clareza, como exemplo, tomamos um dos controladores de nosso serviço - PersonalizedHoroscopeTellController , seu código se parece com o seguinte:

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


Configuração Java com dependências em cada teste


Para cada teste, podemos escrever Java Config no qual descrevemos os beans de controlador e de aspecto e os beans com os moks de dependência do controlador. Essa maneira de descrever os feijões será imperativa, porque diremos explicitamente à primavera como precisamos criar os feijões.

Nesse caso, o teste para o nosso controlador ficará assim:

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


Esse teste parece bastante complicado. Nesse caso, teremos que escrever Java Config para cada um dos controladores. Embora o conteúdo seja diferente, ele terá o mesmo significado: crie um bean e moki de controlador para suas dependências. Portanto, em essência, será o mesmo para todos os controladores. Eu, como qualquer programador, sou uma pessoa preguiçosa, então recusei imediatamente essa opção.

Anotação @MockBean em cada campo com dependência


A anotação @MockBean apareceu no Spring Boot Test versão 1.4.0. É semelhante ao @Mock do Mockito (e até o usa internamente), com a única diferença: ao usar o @MockBean , o mock criado será automaticamente colocado no contexto da primavera. Este método de declarar mok será declarativo, pois não precisamos dizer exatamente ao Spring como criar esses mok.

Nesse caso, o teste terá a seguinte aparência:

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


Nesta opção, ainda existe o Java Config, mas é muito mais compacto. Entre as deficiências - tive que declarar campos com dependências do controlador (campos com anotação @MockBean ), mesmo que não sejam mais usados ​​no teste. Bem, se você usar a versão Spring Boot menor que 1.4.0 por algum motivo, não poderá usar esta anotação.

Portanto, eu tive uma idéia para outra opção para zombar. Eu gostaria que funcionasse dessa maneira ...

@ Anotação automática no componente dependente


Gostaria que tivéssemos a anotação @Automocked , que eu poderia colocar apenas acima do campo com o controlador, e então o moki seria criado automaticamente para esse controlador e colocado no contexto.

O teste neste caso pode ser assim:

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


Como você pode ver, essa opção é a mais compacta das apresentadas, existe apenas um bean controlador (mais um predicado para um aspecto), a anotação @Automocked está @Automocked e toda a mágica de criar beans e colocá-los no contexto é escrita uma vez e pode ser usada em todos os testes.

Como isso funciona?


Vamos ver como funciona e o que precisamos para isso.

TestExecutionListener


Existe essa interface na primavera - TestExecutionListener . Ele fornece uma API para incorporação no processo de execução de teste em seus vários estágios, por exemplo, ao criar uma instância de uma classe de teste, antes ou depois de chamar um método de teste etc. Ele tem várias implementações prontas para uso. Por exemplo, DirtiesContextTestExecutionListener , que limpa o contexto se você colocar a anotação apropriada; DependencyInjectionTestExecutionListener - executa injeção de dependência em testes, etc. Para aplicar seu Ouvinte personalizado ao teste, você precisa colocar a anotação @TestExecutionListeners acima e indicar sua implementação.

Encomendado


Há também uma interface Ordenada na primavera. É usado para indicar que os objetos devem ser classificados de alguma maneira. Por exemplo, quando você tem várias implementações da mesma interface e deseja injetá-las em uma coleção, elas serão ordenadas de acordo com Ordenado. No caso de TestExecutionListener, esta anotação indica em qual ordem eles devem ser aplicados.

Portanto, nosso Ouvinte implementará 2 interfaces: TestExecutionListener e Ordered . Nós o chamamos de AutomockTestExecutionListener e ficará assim:

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


O que está acontecendo aqui? Primeiro, no método prepareTestInstance() , ele encontra todos os campos com a anotação @Automocked :

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

Em seguida, torna esses campos graváveis:

 makeAccessible(field); 

Em seguida, no método findConstructorToAutomock() , findConstructorToAutomock() encontra o construtor apropriado:

 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; 

Um construtor adequado no nosso caso é o construtor com a anotação @Autowired ou o construtor com o maior número de argumentos.

Em seguida, o construtor encontrado é passado como argumento para o método createBeanWithMocks() , que por sua vez chama o método createMocksForParameters() , onde as simulações dos argumentos do construtor são criadas e registradas no contexto:

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

É importante observar que uma representação em cadeia do tipo do argumento (junto com os genéricos) será usada como o nome do compartimento. Ou seja, para um argumento do tipo packages.Function<String, String> representação packages.Function<String, String> string será a string "packages.Function<java.lang.String, java.lang.String>" . Isso é importante, voltaremos a isso.

Após criar simulações para todos os argumentos e registrá-los no contexto, voltamos a criar o bean da classe dependente (ou seja, o controlador no nosso caso):

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

Você também deve prestar atenção ao fato de termos usado a Ordem 1900 . Isso é necessário porque nosso Ouvinte deve ser chamado após limpar o contexto ohm de DirtiesContextBeforeModesTestExecutionListener (order = 1500) e antes da injeção de dependência da DependencyInjectionTestExecutionListener (order = 2000), porque nosso Listener cria novos compartimentos.

AutowireCandidateResolver


AutowireCandidateResolver é usado para determinar se o BeanDefinition corresponde à descrição da dependência. Ele tem várias implementações "prontas para uso", entre elas:


Ao mesmo tempo, a implementação "pronta para uso" é uma boneca russa de herança, ou seja, eles se expandem. Vamos escrever um decorador, porque é mais flexível.

O resolvedor funciona da seguinte maneira:

  1. O Spring usa um descritor de dependência - DependencyDescriptor ;
  2. Em seguida, são necessários todos os BeanDefinition da classe apropriada;
  3. Repete as BeanDefinitions recebidas, chamando o método isAutowireCandidate() do resolvedor;
  4. Dependendo de a descrição do bean corresponder à descrição da dependência ou não, o método retornará verdadeiro ou falso.

Por que você precisou do seu resolvedor?


Agora vamos ver por que precisamos do nosso resolvedor no exemplo do nosso controlador.

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

Como você pode ver, ele possui duas dependências do mesmo tipo - Função , mas com genéricos diferentes. Em um caso, String e ZodiacSign , no outro, String e String . E o problema disso é que o Mockito não é capaz de criar moks levando em consideração os genéricos . I.e. se criarmos mokas para essas dependências e as colocarmos em contexto, o Spring não poderá injetá-las nessa classe, pois elas não conterão informações sobre genéricos. E veremos a exceção de que, no contexto, há mais de um bean da classe Function . É precisamente esse problema que resolveremos com a ajuda do nosso resolvedor. Afinal, como você se lembra, em nossa implementação do Listener, usamos um tipo com genéricos como o nome da lixeira, o que significa que tudo o que precisamos fazer é ensinar a primavera a comparar o tipo de dependência com o nome da lixeira.

AutomockedBeanByNameAutowireCandidateResolver


Portanto, nosso resolvedor fará exatamente o que escrevi acima, e a implementação do método isAutowireCandidate() será parecida com esta:

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


Aqui, ele obtém a representação em string do tipo de dependência na descrição da dependência, obtém o nome do bean em BeanDefinition (que já contém a representação em string do tipo de bean), depois os compara e, se corresponderem, retornará true. Se não corresponderem, ele delega para o resolvedor interno.

Opções de umedecimento da bandeja de teste


No total, em testes, podemos usar as seguintes opções para umectação de lixeira:

  • Configuração Java - será imperativo, complicado, com um padrão, mas, talvez, o mais informativo possível;
  • @MockBean - será declarativo, menos volumoso que o Java Config, mas ainda haverá um pequeno boilerplate na forma de campos com dependências que não são usadas no próprio teste;
  • @Automocked Resolvedor @Automocked + personalizado - código mínimo em testes e clichê, mas com escopo potencialmente bastante estreito e isso ainda precisa ser gravado. Mas pode ser muito conveniente onde você deseja garantir que a mola crie proxies corretamente.

Adicionar decoradores


Nossa equipe ama o modelo de design do Decorator por sua flexibilidade. De fato, aspectos implementam esse padrão específico. Mas, se você configurar o contexto da primavera com anotações e usar a varredura de pacotes, terá um problema. Se você tiver várias implementações da mesma interface no contexto, quando o aplicativo iniciar , uma NoUniqueBeanDefinitionException cairá , ou seja, a primavera não será capaz de descobrir qual dos grãos deve ser injetado. Esse problema tem várias soluções e, em seguida, vamos examiná-las, mas primeiro, vamos descobrir como nosso aplicativo será alterado.

Agora as interfaces FortuneTeller e HoroscopeTeller têm uma implementação, adicionaremos mais 2 implementações para cada uma das interfaces:



  • Armazenamento em cache ... - decorador de armazenamento em cache;
  • Logging ... é um decorador de logging.

Então, como você resolve o problema de determinar a ordem dos beans?

Configuração Java com decorador de nível superior


Você pode usar o Java Config novamente. Nesse caso, descreveremos os beans como métodos da classe config, e teremos que especificar os argumentos necessários para chamar o construtor do bean como argumentos para o método. Daí se segue que, no caso de uma alteração no construtor da lixeira, teremos que alterar a configuração, o que não é muito legal. Das vantagens desta opção:

  • haverá baixa conectividade entre decoradores, como a conexão entre eles será descrita na configuração, ou seja, eles não saberão nada um do outro;
  • todas as alterações na ordem dos decoradores serão localizadas em um só lugar - a configuração.

No nosso caso, o Java Config ficará assim:

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


Como você pode ver, para cada uma das interfaces, apenas um bean é declarado aqui, e os métodos contêm nos argumentos as dependências de todos os objetos criados dentro. Nesse caso, a lógica para criar beans é bastante óbvia.

Qualificador


Você pode usar a anotação @Qualifier . Isso será mais declarativo que o Java Config, mas nesse caso você precisará especificar explicitamente o nome do bean do qual o bean atual depende. Qual a desvantagem implica: maior conectividade entre os compartimentos. E como a conectividade aumenta, mesmo no caso de uma alteração na ordem dos decoradores, as alterações serão manchadas uniformemente no código. Ou seja, no caso de adicionar um novo decorador, por exemplo, no meio da cadeia, as alterações afetarão pelo menos duas classes.

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


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

Custom qualifier


2.5 Qualifier', . .

enum :

 public enum DecoratorType { LOGGING, CACHING, NOT_DECORATOR } 

, qualifier':

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

: , @Qualifier , CustomAutowireConfigurer , .

Qualifier' :

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


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

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

DecoratorAutowireCandidateResolver


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

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


– Java Config' , – . , !

- . , , , . :

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

BeanDefinitionRegistryPostProcessor


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

:

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

BeanFactoryPostProcessor


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



, , – AutowireCandidateResolver':

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



DecoratorAutowireCandidateResolver


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


descriptor' (dependencyType) (dependentType):

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

bdHolder' BeanDefinition:

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

. , :

 dependencyType.isAssignableFrom(dependentType) 

, , .. .

BeanDefinition' :

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

, :

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

– (), – false.




, :

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


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

, – , , - . Obrigado pela leitura!

Todas as fontes estão disponíveis em: https://github.com/monosoul/spring-di-customization .

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


All Articles