
記事を読む前に、 Boris Yevgeny EvgenyBorisovのレポート: Spring Ripper、Part 1を読むことを強くお勧めします。 スプリングリッパー、パート2 まだそれらのプレイリストがあります 。
はじめに
運命と星占いを予測するサービスの開発を依頼されたと想像してみましょう。 サービスにはいくつかのコンポーネントがありますが、主なものは2つです。
- FortuneTellerインターフェイスを実装し、運命を予測するGloba。
- Gypsy。HoroscopeTellerインターフェイスを実装し、ホロスコープを作成します。
また、このサービスには、実際に運命と星占いの予測を受信するためのいくつかのエンドポイント(コントローラー)があります。 また、コントローラーメソッドに適用されるアスペクトを使用して、IPによるアプリケーションへのアクセスを制御し、次のようになります。
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(); } }
そのようなIPからのアクセスが許可されていることを確認するには、
ipIsAllowed
述語の実装を使用します。 一般に、この側面のサイトでは、たとえば認証を実行するなど、他の何かがあるかもしれません。
それで、私たちはアプリケーションを開発しました、そして、すべてが私たちにとって素晴らしい作品です。 しかし、今からテストについて話しましょう。
それをテストするには?
アスペクトのアプリケーションをテストする方法について話しましょう。 これを行うにはいくつかの方法があります。
スプリングコンテキストを上げることなく、アスペクトとコントローラーに別々のテストを書くことができます(コントローラーのアスペクトを使用してプロキシを作成します。これについては公式ドキュメントで詳細を読むことができます )。 コントローラと期待どおりに動作します。
アプリケーションの完全なコンテキストを上げるテストを作成できますが、この場合は次のようになります。
- テストは十分に長く実行されます。 すべてのビンが上昇します。
- NPEを同時にスローすることなく、ビン間の呼び出しのチェーン全体を通過できる有効なテストデータを準備する必要があります。
しかし、アスペクトが何を適用し、その仕事をしているかを正確にテストしたいと思います。 コントローラによって呼び出されるサービスをテストしたくないので、テストデータに困惑して起動時間を犠牲にしたくありません。 したがって、コンテキストの一部のみを発生させるテストを作成します。 つまり このコンテキストでは、実際のアスペクトBeanと実際のコントローラーBeanがあり、他のすべてはモカミになります。
モカ豆の作り方
春にモカ豆を作る方法はいくつかあります。 わかりやすくするために、例として、サービスのコントローラーの1つである
PersonalizedHoroscopeTellController
を取り上げます。そのコードは次のようになります。
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
各テスト用にJava Configを作成できます。ここでは、コントローラーBeanとアスペクトBean、およびコントローラー依存関係moksを持つBeanの両方を記述します。 Beanの作成方法が明示的にSpringに通知されるため、Beanを記述するこの方法は必須です。
この場合、コントローラーのテストは次のようになります。
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); } } }
このようなテストはかなり面倒に見えます。 この場合、コントローラーごとにJava Configを作成する必要があります。 内容は異なりますが、意味は同じです。コントローラーBeanと依存関係用のmokiを作成します。 したがって、本質的には、すべてのコントローラーで同じになります。 私は、プログラマと同じように怠け者なので、このオプションをすぐに拒否しました。
依存関係のある各フィールドに対する@MockBeanアノテーション
@MockBeanアノテーションは、Spring Boot Testバージョン1.4.0で登場しました。 これはMockitoの@Mockと似ています (実際、内部で使用することさえあります)が、
@MockBean
を使用
@MockBean
と、作成されたモックが自動的にスプリングコンテキストに配置される点が異なります。 mokを宣言するこの方法は、これらのmokの作成方法を正確にspringに伝える必要がないため、宣言的です。
この場合、テストは次のようになります。
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 { } }
このオプションにはまだJava Configがありますが、はるかにコンパクトです。 欠点の中でも-コントローラー依存関係を持つフィールド(
@MockBean
アノテーションを持つフィールド)を宣言する必要がありましたが、それらはテストでさらに使用されていません。 何らかの理由でバージョン1.4.0より前のSpring Bootを使用している場合は、このアノテーションを使用できません。
そのため、私は、もう1つのオプションのモーキングのアイデアを思いつきました。 私はそれがこのように働くことを望みます...
依存コンポーネント上の@Automockedアノテーション
@Automocked
アノテーションを
@Automocked
したいのですが、これはコントローラーのあるフィールドの上にのみ配置でき、このコントローラーに対して
@Automocked
が自動的に作成され、コンテキストに配置されます。
この場合のテストは次のようになります。
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); } }
ご覧のとおり、このオプションは提示されたものの中で最もコンパクトであり、コントローラーBean(およびアスペクトの述語)のみがあり、
@Automocked
アノテーションが
@Automocked
にあり、 Beanを作成してコンテキストに配置するすべての魔法が一度書かれ、すべてで使用できますテスト。
どのように機能しますか?
それがどのように機能し、これに必要なものを見てみましょう。
TestExecutionListener
spring - TestExecutionListenerにはそのようなインターフェースがあります。 たとえば、テストクラスのインスタンスの作成時、テストメソッドの呼び出し前後など、テスト実行プロセスにさまざまな段階で埋め込むためのAPIを提供します。 彼には、すぐに使える実装がいくつかあります。 たとえば、 DirtiesContextTestExecutionListenerは、適切な注釈を付けるとコンテキストをクリーンアップします。 DependencyInjectionTestExecutionListener-テストなどで依存性注入を実行します。 カスタムリスナーをテストに適用するには、その上に
@TestExecutionListeners
アノテーションを配置し、実装を示す必要があります。
注文済み
また、春にはOrderedインターフェイスがあります。 オブジェクトを何らかの方法でソートする必要があることを示すために使用されます。 たとえば、同じインターフェイスの複数の実装があり、それらをコレクションに注入する場合、このコレクションではOrderedに従って順序付けられます。 TestExecutionListenerの場合、この注釈は、適用する順序を示します。
そのため、リスナーはTestExecutionListenerとOrderedの 2つのインターフェースを実装します。 AutomockTestExecutionListenerと呼び、 次のようになります。
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); } } }
ここで何が起こっていますか? まず、
prepareTestInstance()
メソッドで、
@Automocked
アノテーションが付いたすべてのフィールドを検索します。
for (val field : testContext.getTestClass().getDeclaredFields()) { if (field.getAnnotation(Automocked.class) == null) { continue; }
次に、これらのフィールドを書き込み可能にします。
makeAccessible(field);
次に、
findConstructorToAutomock()
メソッドで、適切なコンストラクターを検索します。
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;
この場合に適しているのは、 @ Autowiredアノテーションが付いたコンストラクター、または引数の数が最も多いコンストラクターです。
次に、見つかったコンストラクターが引数として
createBeanWithMocks()
メソッドに渡され、
createBeanWithMocks()
メソッドが
createBeanWithMocks()
メソッドを呼び出します。このメソッドでは、コンストラクター引数のモックが作成され、コンテキストに登録されます。
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()) ); } }
引数の型の文字列表現が(ジェネリックと一緒に)ビンの名前として使用されることに注意することが重要です。 つまり、タイプ
packages.Function<String, String>
引数の場合
packages.Function<String, String>
ストリング表現はストリング
"packages.Function<java.lang.String, java.lang.String>"
ます。 これは重要です。これに戻ります。
すべての引数のモックを作成してコンテキストに登録した後、依存クラスのBean(つまり、この場合はコントローラー)の作成に戻ります。
if (!beanFactory.containsBean(beanName)) { val bean = beanFactory.createBean(clazz); beanFactory.registerSingleton(beanName, bean); }
また、 Order 1900を使用したことに注意してください。 これは、リスナーが新しいビンを作成するため、 DirtiesContextBeforeModesTestExecutionListener 'ohm context(order = 1500)をクリアした後、 DependencyInjectionTestExecutionListener ' th依存性注入(order = 2000)の前にリスナーを呼び出す必要があるためです。
AutowireCandidateResolver
AutowireCandidateResolverは 、 BeanDefinitionが依存関係の説明と一致するかどうかを判断するために使用されます。 彼には、いくつかの実装が「すぐに使える」うちにあります。
- QualifierAnnotationAutowireCandidateResolver-依存関係がQualifier'aに基づいて適切かどうかを判断します。
- GenericTypeAwareAutowireCandidateResolver-ジェネリックに基づいて依存関係が適切かどうかを判断します。
- その他。
同時に、「箱から出して」実装は、継承からのロシアの人形です。 それらは互いに拡張します。 デコレータを作成します。なぜなら より柔軟です。
リゾルバーは次のように機能します。
- Springは依存関係記述子-DependencyDescriptorを取ります。
- 次に、適切なクラスのすべてのBeanDefinitionを取得します。
- レゾルバの
isAutowireCandidate()
メソッドを呼び出して、受信したBeanDefinitionsを反復処理します。
- Beanの説明が依存関係の説明に適合するかどうかに応じて、メソッドはtrueまたはfalseを返します。
リゾルバーが必要な理由は何ですか?
次に、コントローラーの例にリゾルバーが必要な理由を考えてみましょう。
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; }
ご覧のとおり、同じタイプの2つの依存関係-Functionがありますが、ジェネリックは異なります。 1つのケースでは、 StringおよびZodiacSign 、もう1つのケースでは、 StringおよびStringです。 そして、これに関する問題は、 Mockitoがジェネリックを考慮してモックを作成できないことです。 つまり これらの依存関係のmokaを作成してコンテキストに配置すると、Springはジェネリックに関する情報を含まないため、このクラスにそれらを挿入できません。 また、コンテキストにはFunctionクラスのBeanが複数あるという例外があります。 レゾルバの助けを借りて解決するのはまさにこの問題です。 結局、覚えているように、リスナーの実装では、ビンの名前としてジェネリックを持つ型を使用しました。つまり、必要なのは、依存関係のタイプとビンの名前を比較するようにスプリングに教えるだけです。
AutomockedBeanByNameAutowireCandidateResolver
したがって、リゾルバーは上記で説明したとおりの処理を行い、
isAutowireCandidate()
メソッドの実装は次のようになります。
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); }
ここでは、依存関係の説明から依存関係タイプの文字列表現を取得し、BeanDefinition(Beanタイプの文字列表現を既に含む)からBean名を取得し、それらを比較し、一致する場合はtrueを返します。 それらが一致しない場合、内部リゾルバに委任します。
ビン湿潤オプションのテスト
合計で、テストでは、ビンの湿潤に次のオプションを使用できます。
- Java Config-定型文を使用する必要があり、扱いにくくなりますが、おそらく、可能な限り有益です。
-
@MockBean
宣言型であり、Java Configよりも煩雑ではありませんが、テスト自体では使用されない依存関係を持つフィールドの形式で小さな定型文が残ります。
-
@Automocked
+カスタムリゾルバー-テストおよびボイラープレートの最小限のコードですが、潜在的にかなり狭いスコープであり、これはまだ記述する必要があります。 しかし、スプリングがプロキシを正しく作成することを確認したい場合には非常に便利です。
デコレータを追加する
私たちのチームは、その柔軟性のためにデコレータのデザインテンプレートを気に入っています。 実際、アスペクトはこの特定のパターンを実装しています。 ただし、注釈付きのSpringコンテキストを構成し、パッケージスキャンを使用する場合、問題が発生します。 コンテキスト内に同じインターフェースの実装が複数ある場合、アプリケーションの起動時にNoUniqueBeanDefinitionExceptionが発生します。 春は、どの豆を注入するべきかを判断できません。 この問題にはいくつかの解決策があり、それらを見ていきますが、最初にアプリケーションがどのように変化するかを考えましょう。
FortuneTellerおよびHoroscopeTellerインターフェースには1つの実装がありますが、各インターフェースにさらに2つの実装を追加します。

- キャッシュ...-キャッシュデコレータ。
- Logging ...はロギングデコレータです。
では、Beanの順序を決定する問題をどのように解決しますか?
トップレベルデコレータを使用したJava Config
Java Configを再び使用できます。 この場合、Beanをconfigクラスのメソッドとして記述し、Beanのコンストラクターをメソッドの引数として呼び出すために必要な引数を指定する必要があります。 ビンのコンストラクターが変更された場合、設定を変更する必要がありますが、これはあまりクールではありません。 このオプションの利点:
- デコレータ間の接続性は低くなります。 それらの間の接続は、設定で説明されます。 彼らはお互いについて何も知らないでしょう。
- デコレータの順序の変更はすべて、1つの場所、つまり構成にローカライズされます。
この場合、Java Configは次のようになります。
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 ) ); } }
ご覧のとおり、各インターフェイスに対して1つのBeanのみがここで宣言され、メソッドには、内部に作成されたすべてのオブジェクトの依存関係が引数に含まれています。 この場合、Beanを作成するためのロジックはかなり明白です。
予選
@Qualifierアノテーションを使用できます。 これはJava Configよりも宣言的ですが、この場合、現在のBeanが依存するBeanの名前を明示的に指定する必要があります。 欠点が意味するもの:ビン間の接続性の向上。 また、接続性が向上するため、デコレータの順序が変更された場合でも、変更はコード全体に均等に塗りつぶされます。 つまり、たとえばチェーンの途中で新しいデコレータが追加された場合、変更は少なくとも2つのクラスに影響します。
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.
- , ConfigurationClassBeanDefinitionReader ';
- , BeanDefintion' Qualifier'. , .
, :
- Java Config – , , , ;
-
@Qualifier
– , - ;
- Custom qualifier – , Qualifier', ;
- - – , , , .
結論
, , . – : . , , , . – , JRE. , , .
, – , , - . !
: https://github.com/monosoul/spring-di-customization .