Hola Mi nombre es Andrey Nevedomsky y soy el ingeniero jefe en SberTekh. Trabajo en un equipo que desarrolla uno de los servicios del sistema del FSE (Sistema Frontal Unificado). En nuestro trabajo, utilizamos activamente el Spring Framework, en particular su DI, y de vez en cuando nos enfrentamos con el hecho de que resolver dependencias en primavera no es lo suficientemente inteligente para nosotros. Este artículo es el resultado de mis intentos de hacerlo más inteligente y, en general, entender cómo funciona. Espero que puedas aprender algo nuevo sobre el dispositivo de la primavera.

Antes de leer el artículo, le recomiendo que lea
los informes de
Boris Yevgeny
EvgenyBorisov :
Spring Ripper, Parte 1 ;
Desgarrador de muelles, parte 2 . Todavía hay una
lista de reproducción de ellos .
Introduccion
Imaginemos que se nos pidió desarrollar un servicio para predecir el destino y los horóscopos. Hay varios componentes en nuestro servicio, pero los principales para nosotros serán dos:
- Globa, que implementará la interfaz FortuneTeller y predecirá el destino;

- Gypsy, que implementará la interfaz HoroscopeTeller y creará horóscopos.

También en nuestro servicio habrá varios puntos finales (controladores) para, de hecho, obtener adivinación y horóscopos. Y también controlaremos el acceso a nuestra aplicación por IP utilizando un aspecto que se aplicará a los métodos del controlador y se verá más o menos así:
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 que el acceso desde dicha IP está permitido, utilizaremos alguna implementación del predicado
ipIsAllowed
. En general, en el sitio de este aspecto, puede haber alguna otra, por ejemplo, autorización.
Entonces, desarrollamos la aplicación y todo funciona muy bien para nosotros. Pero hablemos de las pruebas ahora.
¿Cómo probarlo?
Hablemos sobre cómo podemos probar la aplicación de aspectos. Tenemos varias formas de hacer esto.
Puede escribir pruebas separadas para un aspecto y para los controladores, sin elevar el contexto de primavera (que solo creará un proxy con un aspecto para el controlador, puede leer más sobre esto en la
documentación oficial), pero en este caso
no probaremos exactamente qué aspectos se aplican correctamente a controladores y funcionan exactamente como esperamos ;
Puede escribir pruebas en las que plantearemos el contexto completo de nuestra aplicación, pero en este caso:
- ejecutar pruebas llevará mucho tiempo, porque todos los contenedores se levantarán;
- necesitaremos preparar datos de prueba válidos que puedan pasar a través de toda la cadena de llamadas entre contenedores sin lanzar NPE al mismo tiempo.
Pero queremos probar exactamente qué aspecto ha aplicado y está haciendo su trabajo. No queremos probar los servicios llamados por el controlador y, por lo tanto, no queremos quedar perplejos por los datos de prueba y sacrificar el tiempo de inicio. Por lo tanto, escribiremos pruebas en las que plantearemos solo una parte del contexto. Es decir en nuestro contexto habrá un bean de aspecto real y un bean de controlador real, y todo lo demás será mokami.
¿Cómo crear frijoles moka?
Hay varias formas de crear frijoles moka en la primavera. Para mayor claridad, como ejemplo, tomamos uno de los controladores de nuestro servicio:
PersonalizedHoroscopeTellController
, su código se ve así:
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(); } }
Configuración de Java con dependencias en cada prueba
Para cada prueba, podemos escribir Java Config en la que describimos tanto los beans de controlador y de aspecto como los beans con los moks de dependencia del controlador. Esta forma de describir los beans será imprescindible, porque le diremos explícitamente a spring cómo necesitamos crear los beans.
En este caso, la prueba para nuestro controlador se verá así:
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); } } }
Tal prueba parece bastante engorrosa. En este caso, tendremos que escribir Java Config para cada uno de los controladores. Aunque será diferente en contenido, tendrá el mismo significado: crear un bean controlador y moki para sus dependencias. Entonces, en esencia, será lo mismo para todos los controladores. Yo, como cualquier programador, soy una persona perezosa, así que inmediatamente rechacé esta opción.
@MockBean anotación sobre cada campo con dependencia
La
anotación @MockBean apareció en Spring Boot Test versión 1.4.0. Es similar a
@Mock de Mockito (y de hecho incluso lo usa internamente), con la única diferencia de que cuando se usa
@MockBean
, el simulacro creado se colocará automáticamente en el contexto de la primavera. Este método de declarar mok será declarativo, ya que no tenemos que decirle a spring exactamente cómo crear estos mok.
En este caso, la prueba se verá así:
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 { } }
En esta opción, todavía hay Java Config, pero es mucho más compacto. Entre las deficiencias: tuve que declarar campos con dependencias de controlador (campos con anotación
@MockBean
), a pesar de que no se usan más en la prueba. Bueno, en caso de que use la versión Spring Boot inferior a 1.4.0 por alguna razón, entonces no podrá usar esta anotación.
Por lo tanto, se me ocurrió una idea para otra opción para burlarse. Me gustaría que funcionara de esta manera ...
Anotación @Automocked sobre componente dependiente
Me gustaría que tuviéramos la anotación
@Automocked
, que podría poner solo sobre el campo con el controlador, y luego moki se crearía automáticamente para este controlador y se colocaría en contexto.
La prueba en este caso podría verse así:
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 puede ver, esta opción es la más compacta de las presentadas, solo hay un bean controlador (más un predicado para un aspecto), la anotación
@Automocked
está
@Automocked
, y toda la
magia de crear beans y colocarlos en contexto se escribe una vez y se puede usar en todos pruebas
Como funciona
Veamos cómo funciona y qué necesitamos para esto.
TestExecutionListener
Existe una interfaz de este tipo en
primavera :
TestExecutionListener . Proporciona una API para incrustar en el proceso de ejecución de la prueba en sus diversas etapas, por ejemplo, al crear una instancia de una clase de prueba, antes o después de llamar a un método de prueba, etc. Tiene varias implementaciones listas para usar. Por ejemplo,
DirtiesContextTestExecutionListener , que limpia el contexto si coloca la anotación adecuada;
DependencyInjectionTestExecutionListener : realiza la inyección de dependencia en pruebas, etc. Para aplicar su Listener personalizado a la prueba, debe colocar la anotación
@TestExecutionListeners
encima e indicar su implementación.
Ordenado
También hay una interfaz
ordenada en la primavera. Se utiliza para indicar que los objetos deben clasificarse de alguna manera. Por ejemplo, cuando tiene varias implementaciones de la misma interfaz y desea inyectarlas en una colección, en esta colección se ordenarán de acuerdo con Ordered. En el caso de TestExecutionListener, esta anotación indica en qué orden se deben aplicar.
Entonces, nuestro Listener implementará 2 interfaces:
TestExecutionListener y
Ordered . Lo llamamos
AutomockTestExecutionListener y se verá así:
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); } } }
¿Qué está pasando aquí? Primero, en el método
prepareTestInstance()
, encuentra todos los campos con la anotación
@Automocked
:
for (val field : testContext.getTestClass().getDeclaredFields()) { if (field.getAnnotation(Automocked.class) == null) { continue; }
Luego hace que estos campos se puedan escribir:
makeAccessible(field);
Luego, en el método
findConstructorToAutomock()
, encuentra el constructor apropiado:
Constructor<?> fallBackConstructor = clazz.getDeclaredConstructors()[0]; for (val constructor : clazz.getDeclaredConstructors()) { if (constructor.getParameterTypes().length > fallBackConstructor.getParameterTypes().length) { fallBackConstructor = constructor; } val autowired = getAnnotation(constructor, Autowired.class); if (autowired != null) { return constructor; } } return fallBackConstructor;
Un constructor adecuado en nuestro caso es el constructor con la anotación
@Autowired o el constructor con el mayor número de argumentos.
Luego, el constructor encontrado se pasa como argumento al método
createBeanWithMocks()
, que a su vez llama al método
createMocksForParameters()
, donde se crean y registran
createMocksForParameters()
para los argumentos del constructor en el 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()) ); } }
Es importante tener en cuenta que se usará una representación de cadena del tipo de argumento (junto con los genéricos) como el nombre del bin. Es decir, para un argumento de tipo
packages.Function<String, String>
representación de cadena será la cadena "packages.Function<java.lang.String, java.lang.String>"
. Esto es importante, volveremos a esto.
Después de crear simulacros para todos los argumentos y registrarlos en el contexto, volvemos a crear el bean de la clase dependiente (es decir, el controlador en nuestro caso):
if (!beanFactory.containsBean(beanName)) { val bean = beanFactory.createBean(clazz); beanFactory.registerSingleton(beanName, bean); }
También debe prestar atención al hecho de que utilizamos
Order 1900 . Esto se debe a que nuestro Listener debe ser llamado después de borrar el contexto de ohmios
DirtiesContextBeforeModesTestExecutionListener '(orden = 1500) y antes de la inyección de dependencia
DependencyInjectionTestExecutionListener ' (orden = 2000), porque nuestro Listener crea nuevos contenedores.
AutowireCandidateResolver
AutowireCandidateResolver se usa para determinar si
BeanDefinition coincide con la descripción de dependencia. Tiene varias implementaciones "listas para usar", entre ellas:
Al mismo tiempo, la implementación "fuera de la caja" es una muñeca rusa de herencia, es decir se expanden el uno al otro. Escribiremos un decorador, porque Es más flexible.
El resolutor funciona de la siguiente manera:
- Spring toma un descriptor de dependencia: DependencyDescriptor ;
- Luego toma todos los BeanDefinition 's de la clase apropiada;
- Itera sobre las BeanDefinitions recibidas, llamando al método
isAutowireCandidate()
del resolutor;
- Dependiendo de si la descripción del bean coincide o no con la descripción de la dependencia, el método devuelve verdadero o falso.
¿Por qué necesitabas tu resolutor?
Ahora veamos por qué necesitábamos nuestro resolutor en el ejemplo de nuestro 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 puede ver, tiene dos dependencias del mismo tipo:
función , pero con genéricos diferentes. En un caso,
String y
ZodiacSign , en el otro,
String y
String . Y el problema con esto es que
Mockito no puede crear moks teniendo en cuenta los genéricos . Es decir Si creamos mokas para estas dependencias y las ponemos en contexto, Spring no podrá inyectarlas en esta clase, ya que no contendrán información sobre genéricos. Y veremos la excepción de que en el contexto hay más de un bean de la clase
Function . Es precisamente este problema el que resolveremos con la ayuda de nuestro solucionador. Después de todo, como recordará, en nuestra implementación de Listener usamos un tipo con genéricos como nombre del contenedor, lo que significa que todo lo que tenemos que hacer es enseñarle al resorte a comparar el tipo de dependencia con el nombre del contenedor.
AutomockedBeanByNameAutowireCandidateResolver
Entonces, nuestro resolutor hará exactamente lo que escribí anteriormente, y la implementación del método
isAutowireCandidate()
se verá así:
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); }
Aquí obtiene la representación de cadena del tipo de dependencia de la descripción de la dependencia, obtiene el nombre del bean de BeanDefinition (que ya contiene la representación de cadena del tipo de bean), luego los compara y, si coinciden, devuelve verdadero. Si no coinciden, delega a la resolución interna.
Pruebe las opciones de humedecimiento del depósito
En total, en las pruebas podemos usar las siguientes opciones para la humectación del contenedor:
- Configuración de Java: será imprescindible, engorroso, con una plantilla repetitiva, pero, tal vez, lo más informativo posible;
@MockBean
: será declarativo, menos voluminoso que Java Config, pero seguirá habiendo una pequeña plantilla en forma de campos con dependencias que no se utilizan en la prueba en sí;
@Automocked
+ @Automocked
personalizado: código mínimo en pruebas y repetitivo, pero es posible que tenga un alcance bastante limitado y esto aún debe escribirse. Pero puede ser muy conveniente donde quieras asegurarte de que el resorte crea proxies correctamente.
Agregar decoradores
Nuestro equipo
ama la plantilla de diseño
Decorator por su flexibilidad. De hecho, los aspectos implementan este patrón particular. Pero en caso de que configure el contexto de Spring con anotaciones y use el escaneo de paquetes, se encontrará con un problema. Si tiene varias implementaciones de la misma interfaz en el contexto, cuando se
inicie la aplicación, se
producirá una
NoUniqueBeanDefinitionException , es decir Spring no podrá determinar cuál de los frijoles debe inyectarse. Este problema tiene varias soluciones, y luego las veremos, pero primero, descubramos cómo cambiará nuestra aplicación.
Ahora las interfaces
FortuneTeller y
HoroscopeTeller tienen una implementación, agregaremos 2 implementaciones más para cada una de las interfaces:

- Almacenamiento en caché ... - decorador de almacenamiento en caché;
- Registro ... es un decorador de registro.
Entonces, ¿cómo resuelve el problema de determinar el orden de los frijoles?
Configuración de Java con decorador de nivel superior
Puede usar Java Config nuevamente. En este caso, describiremos los beans como métodos de la clase config, y tendremos que especificar los argumentos necesarios para llamar al constructor del bean como los argumentos del método. De lo que se deduce que en caso de un cambio en el constructor del bin, tendremos que cambiar la configuración, lo que no es muy bueno. De las ventajas de esta opción:
- habrá baja conectividad entre decoradores, como la conexión entre ellos se describirá en la configuración, es decir no sabrán nada el uno del otro;
- Todos los cambios en el orden de los decoradores se localizarán en un solo lugar: la configuración.
En nuestro caso, Java Config se verá así:
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 puede ver, para cada una de las interfaces solo se declara un bean aquí, y los métodos contienen en los argumentos las dependencias de todos los objetos creados en su interior. En este caso, la lógica para crear beans es bastante obvia.
Calificador
Puede usar la anotación
@Qualifier . Esto será más declarativo que Java Config, pero en este caso deberá especificar explícitamente el nombre del bean del que depende el bean actual. Lo que implica la desventaja: mayor conectividad entre contenedores. Y dado que la conectividad aumenta, incluso en el caso de un cambio en el orden de los decoradores, los cambios se extenderán de manera uniforme sobre el código. Es decir, si se agrega un nuevo decorador, por ejemplo, en el medio de la cadena, los cambios afectarán al menos a 2 clases.
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', ;
- - – , , , .
Conclusiones
, , . – : . , , , . – , JRE. , , .
, – , , - . Gracias por leer!
Todas las fuentes están disponibles en: https://github.com/monosoul/spring-di-customization .