Hai Nama saya Andrey Nevedomsky dan saya adalah chief engineer di SberTekh. Saya bekerja dalam tim yang mengembangkan salah satu layanan sistem ESF (Unified Frontal System). Dalam pekerjaan kami, kami secara aktif menggunakan Kerangka Pegas, khususnya DI-nya, dan dari waktu ke waktu kami dihadapkan pada kenyataan bahwa menyelesaikan dependensi pada pegas tidak cukup cerdas bagi kami. Artikel ini adalah hasil dari upaya saya untuk membuatnya lebih pintar dan secara umum memahami cara kerjanya. Saya harap Anda dapat mempelajari sesuatu yang baru dari itu tentang perangkat pegas.

Sebelum membaca artikel, saya sangat menyarankan Anda membaca laporan
Boris Yevgeny
EvgenyBorisov :
Spring Ripper, Bagian 1 ;
Spring ripper, bagian 2 . Masih ada
daftar putar mereka .
Pendahuluan
Mari kita bayangkan bahwa kita diminta mengembangkan layanan untuk memprediksi takdir dan horoskop. Ada beberapa komponen dalam layanan kami, tetapi yang utama bagi kami adalah dua:
- Globa, yang akan mengimplementasikan antarmuka FortuneTeller dan memprediksi nasib;

- Gypsy, yang akan mengimplementasikan antarmuka HoroscopeTeller dan membuat horoskop.

Juga dalam layanan kami akan ada beberapa titik akhir (pengontrol) untuk, pada kenyataannya, memperoleh ramalan nasib dan horoskop. Dan kami juga akan mengontrol akses ke aplikasi kami dengan IP menggunakan aspek yang akan diterapkan pada metode pengontrol dan akan terlihat seperti ini:
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(); } }
Untuk memverifikasi bahwa akses dari IP semacam itu diizinkan, kami akan menggunakan beberapa penerapan predikat
ipIsAllowed
. Secara umum, di situs aspek ini, mungkin ada beberapa yang lain, misalnya, otorisasi.
Jadi, kami mengembangkan aplikasi dan semuanya bekerja dengan baik untuk kami. Tapi mari kita bicara tentang pengujian sekarang.
Bagaimana cara mengujinya?
Mari kita bicara tentang bagaimana kita dapat menguji penerapan berbagai aspek. Kami memiliki beberapa cara untuk melakukan ini.
Anda dapat menulis tes terpisah untuk suatu aspek dan untuk pengontrol, tanpa meningkatkan konteks pegas (yang hanya akan membuat proksi dengan aspek untuk pengontrol, Anda dapat membaca lebih lanjut tentang ini di
dokumentasi resmi), tetapi dalam kasus ini kami
tidak akan
menguji dengan tepat aspek apa yang diterapkan dengan benar pengendali dan bekerja persis seperti yang kita harapkan ;
Anda dapat menulis tes di mana kami akan meningkatkan konteks penuh aplikasi kami, tetapi dalam kasus ini:
- menjalankan tes akan memakan waktu lama, karena semua tempat sampah akan naik;
- kita perlu menyiapkan data uji yang valid yang dapat melewati seluruh rantai panggilan antar nampan tanpa membuang NPE secara bersamaan.
Tetapi kami ingin menguji dengan tepat aspek apa yang telah diterapkan dan melakukan tugasnya. Kami tidak ingin menguji layanan yang dipanggil oleh pengontrol, dan oleh karena itu tidak ingin bingung dengan data uji dan mengorbankan waktu startup. Oleh karena itu, kami akan menulis tes di mana kami hanya akan meningkatkan bagian dari konteks. Yaitu dalam konteks kita akan ada kacang aspek nyata dan kacang pengontrol nyata, dan segalanya akan menjadi mokami.
Bagaimana cara membuat kacang moka?
Ada beberapa cara untuk membuat kacang moka di musim semi. Untuk kejelasan, sebagai contoh, kami mengambil salah satu pengontrol layanan kami -
PersonalizedHoroscopeTellController
, kode-nya terlihat seperti ini:
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(); } }
Konfigurasi Java dengan dependensi dalam setiap tes
Untuk setiap tes, kita dapat menulis Java Config di mana kita menggambarkan kedua pengontrol dan aspek kacang dan kacang dengan pengontrol ketergantungan mok. Cara menggambarkan kacang ini akan sangat penting, karena kami akan secara eksplisit memberi tahu musim semi bagaimana kita perlu membuat kacang.
Dalam hal ini, tes untuk pengontrol kami akan terlihat seperti ini:
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); } } }
Tes semacam itu terlihat agak rumit. Dalam hal ini, kita harus menulis Java Config untuk masing-masing pengontrol. Meskipun akan berbeda dalam konten, itu akan memiliki arti yang sama: membuat kacang pengontrol dan moki untuk dependensinya. Jadi pada dasarnya itu akan sama untuk semua pengontrol. Saya, seperti programmer mana pun, adalah orang yang malas, jadi saya langsung menolak opsi ini.
@MockBean anotasi atas setiap bidang dengan ketergantungan
Anotasi @MockBean muncul di Spring Boot Test versi 1.4.0. Ini mirip dengan
@Mock dari Mockito (dan bahkan ia menggunakannya secara internal), dengan satu-satunya perbedaan adalah ketika menggunakan
@MockBean
, mock yang dibuat akan secara otomatis ditempatkan dalam konteks pegas. Metode menyatakan mok ini akan bersifat deklaratif, karena kita tidak harus memberi tahu pegas bagaimana cara membuat mok ini.
Dalam hal ini, tes akan terlihat seperti ini:
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 { } }
Dalam opsi ini, masih ada Java Config, tetapi jauh lebih ringkas. Di antara kekurangannya - saya harus mendeklarasikan field dengan dependensi controller (bidang dengan penjelasan
@MockBean
), meskipun mereka tidak digunakan dalam pengujian lebih lanjut. Nah, jika Anda menggunakan versi Spring Boot lebih rendah dari 1.4.0 karena beberapa alasan, maka Anda tidak akan dapat menggunakan anotasi ini.
Karena itu, saya datang dengan ide untuk opsi lain untuk bercanda. Saya ingin ini bekerja dengan cara ini ...
@Automocked anotasi atas komponen dependen
Saya ingin agar kita memiliki anotasi
@Automocked
, yang hanya dapat saya letakkan di atas bidang dengan pengontrol, dan kemudian moki akan secara otomatis dibuat untuk pengontrol ini dan ditempatkan dalam konteks.
Tes dalam hal ini dapat terlihat seperti ini:
automocked / PersonalisedoroscopeTellControllerTest.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); } }
Seperti yang Anda lihat, opsi ini adalah yang paling ringkas dari yang disajikan, hanya ada kacang pengontrol (plus predikat untuk suatu aspek), penjelasan
@Automocked
, dan semua
keajaiban membuat kacang dan menempatkannya dalam konteks ditulis sekali dan dapat digunakan di semua tes.
Bagaimana cara kerjanya?
Mari kita lihat cara kerjanya dan apa yang kita butuhkan untuk ini.
TestExecutionListener
Ada antarmuka seperti itu di
musim semi -
TestExecutionListener . Ini menyediakan API untuk menanamkan dalam proses pelaksanaan pengujian di berbagai tahap, misalnya, ketika membuat turunan dari kelas tes, sebelum atau setelah memanggil metode pengujian, dll. Dia memiliki beberapa implementasi di luar kotak. Misalnya,
DirtiesContextTestExecutionListener , yang membersihkan konteks jika Anda memasukkan anotasi yang sesuai;
DependencyInjectionTestExecutionListener - melakukan injeksi dependensi dalam tes, dll. Untuk menerapkan pendengar khusus Anda ke tes, Anda harus meletakkan anotasi
@TestExecutionListeners
di atasnya dan menunjukkan implementasi Anda.
Dipesan
Ada juga antarmuka yang
Dipesan di musim semi. Ini digunakan untuk menunjukkan bahwa objek harus diurutkan dalam beberapa cara. Misalnya, ketika Anda memiliki beberapa implementasi dari antarmuka yang sama dan Anda ingin menyuntikkannya ke dalam koleksi, maka dalam koleksi ini mereka akan dipesan sesuai dengan yang dipesan. Dalam kasus TestExecutionListener, anotasi ini menunjukkan urutan penerapannya.
Jadi, Pendengar kami akan mengimplementasikan 2 antarmuka:
TestExecutionListener dan
Dipesan . Kami menyebutnya
AutomockTestExecutionListener dan akan terlihat seperti ini:
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); } } }
Apa yang sedang terjadi di sini? Pertama, dalam metode
prepareTestInstance()
, ia menemukan semua bidang dengan anotasi
@Automocked
:
for (val field : testContext.getTestClass().getDeclaredFields()) { if (field.getAnnotation(Automocked.class) == null) { continue; }
Kemudian buat bidang-bidang ini dapat ditulis:
makeAccessible(field);
Kemudian, dalam metode
findConstructorToAutomock()
,
findConstructorToAutomock()
menemukan konstruktor yang sesuai:
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;
Konstruktor yang sesuai dalam kasus kami adalah konstruktor dengan anotasi
@Autowired atau konstruktor dengan jumlah argumen terbesar.
Kemudian, konstruktor yang ditemukan dilewatkan sebagai argumen ke metode
createBeanWithMocks()
, yang pada gilirannya memanggil metode
createMocksForParameters()
, di mana tiruan untuk argumen konstruktor dibuat dan terdaftar dalam konteks:
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()) ); } }
Penting untuk dicatat bahwa representasi string dari tipe argumen (bersama dengan generik) akan digunakan sebagai nama bin. Artinya, untuk argumen tipe
packages.Function<String, String>
representasi string akan menjadi string "packages.Function<java.lang.String, java.lang.String>"
. Ini penting, kami akan kembali ke sini.
Setelah membuat ejekan untuk semua argumen dan mendaftarkannya dalam konteks, kami kembali membuat kacang kelas dependen (mis., Controller dalam kasus kami):
if (!beanFactory.containsBean(beanName)) { val bean = beanFactory.createBean(clazz); beanFactory.registerSingleton(beanName, bean); }
Anda juga harus memperhatikan fakta bahwa kami menggunakan
Pesanan 1900 . Ini karena Pendengar kita harus dipanggil setelah menghapus konteks ohm
DirtiesContextBeforeModesTestExecutionListener '(urutan = 1500) dan sebelum injeksi ketergantungan
DependencyInjectionTestExecutionListener ' (urutan = 2000), karena Pendengar kita membuat nampan baru.
AutowireCandidateResolver
AutowireCandidateResolver digunakan untuk menentukan apakah
BeanDefinition cocok dengan deskripsi ketergantungan. Dia memiliki beberapa implementasi "out of the box", di antaranya:
Pada saat yang sama, implementasi "out of the box" adalah boneka Rusia dari warisan, yaitu mereka saling memperluas. Kami akan menulis dekorator, karena lebih fleksibel.
Penyelesai berfungsi sebagai berikut:
- Spring mengambil deskriptor dependensi - DependencyDescriptor ;
- Kemudian dibutuhkan semua BeanDefinition dari kelas yang sesuai;
- Iterate atas BeanDefinitions yang diterima, memanggil metode
isAutowireCandidate()
dari resolver;
- Bergantung pada apakah deskripsi kacang cocok dengan deskripsi dependensi atau tidak, metode mengembalikan benar atau salah.
Mengapa Anda membutuhkan resolver Anda?
Sekarang mari kita lihat mengapa kita membutuhkan resolver kita pada contoh controller kita.
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; }
Seperti yang Anda lihat, ia memiliki dua dependensi dari jenis yang sama -
Fungsi , tetapi dengan obat generik yang berbeda. Dalam satu kasus,
String dan
ZodiacSign , yang lain,
String dan
String . Dan masalah dengan ini adalah bahwa
Mockito tidak dapat membuat moks dengan mempertimbangkan obat generik . Yaitu jika kita membuat mokas untuk dependensi ini dan meletakkannya dalam konteks, maka Spring tidak akan dapat menyuntikkan mereka ke dalam kelas ini, karena mereka tidak akan mengandung informasi tentang obat generik. Dan kita akan melihat pengecualian bahwa dalam konteksnya ada lebih dari satu kacang dari kelas
Function . Hanya masalah ini yang akan kami pecahkan dengan bantuan resolver kami. Bagaimanapun, seperti yang Anda ingat, dalam implementasi Listener kami menggunakan jenis dengan generik sebagai nama nampan, yang berarti bahwa semua yang perlu kita lakukan adalah mengajarkan pegas untuk membandingkan jenis ketergantungan dengan nama nampan.
AutomockedBeanByNameAutowireCandidateResolver
Jadi, resolver kami akan melakukan persis seperti yang saya tulis di atas, dan implementasi metode
isAutowireCandidate()
akan terlihat seperti ini:
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); }
Di sini ia mendapatkan representasi string dari tipe dependensi dari deskripsi dependensi, mendapatkan nama bean dari BeanDefinition (yang sudah berisi representasi string dari tipe bean), kemudian membandingkannya dan, jika cocok, mengembalikan true. Jika tidak cocok, itu akan didelegasikan ke resolver internal.
Opsi pembasahan bin uji
Secara total, dalam pengujian kami dapat menggunakan opsi berikut untuk mengompol bin:
- Java Config - ini akan menjadi keharusan, rumit, dengan pelat ketel, tetapi, mungkin, seinformatif mungkin;
@MockBean
- akan menjadi deklaratif, kurang besar daripada Java Config, tetapi masih akan ada boilerplate kecil dalam bentuk bidang dengan dependensi yang tidak digunakan dalam tes itu sendiri;
@Automocked
+ penyelesai kustom - kode minimum dalam pengujian dan boilerplate, tetapi cakupannya mungkin cukup sempit dan ini masih perlu ditulis. Tapi itu bisa sangat nyaman di mana Anda ingin memastikan bahwa pegas dengan benar membuat proksi.
Tambahkan dekorator
Tim kami
menyukai template desain
Dekorator karena fleksibilitasnya. Bahkan, aspek menerapkan pola khusus ini. Tetapi jika Anda mengkonfigurasi konteks pegas dengan anotasi dan menggunakan pemindaian paket, Anda akan mengalami masalah. Jika Anda memiliki beberapa implementasi antarmuka yang sama dalam konteksnya, maka ketika aplikasi
dimulai ,
NoUniqueBeanDefinitionException akan rontok , mis. musim semi tidak akan bisa mengetahui kacang mana yang harus disuntikkan. Masalah ini memiliki beberapa solusi, dan kemudian kita akan melihatnya, tetapi pertama-tama, mari kita cari tahu bagaimana aplikasi kita akan berubah.
Sekarang antarmuka
FortuneTeller dan
HoroscopeTeller memiliki satu implementasi, kami akan menambahkan 2 implementasi lagi untuk masing-masing antarmuka:

- Caching ... - dekorator caching;
- Logging ... adalah dekorator logging.
Jadi, bagaimana Anda memecahkan masalah menentukan urutan kacang?
Java Config dengan Dekorator Tingkat Atas
Anda dapat menggunakan Java Config lagi. Dalam hal ini, kita akan menggambarkan kacang sebagai metode kelas config, dan kita harus menentukan argumen yang diperlukan untuk memanggil konstruktor kacang sebagai argumen ke metode. Dari sinilah bahwa jika terjadi perubahan konstruktor pada bin, kita harus mengubah konfigurasi, yang tidak terlalu keren. Keuntungan dari opsi ini:
- akan ada konektivitas rendah antara dekorator, karena koneksi di antara mereka akan dijelaskan dalam konfigurasi, mis. mereka tidak akan tahu apa-apa tentang satu sama lain;
- semua perubahan dalam urutan dekorator akan dilokalkan di satu tempat - konfigurasi.
Dalam kasus kami, Java Config akan terlihat seperti ini:
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 ) ); } }
Seperti yang Anda lihat, untuk masing-masing antarmuka hanya satu kacang dideklarasikan di sini, dan metode berisi argumen tentang dependensi semua objek yang dibuat di dalamnya. Dalam hal ini, logika untuk membuat kacang cukup jelas.
Kualifikasi
Anda dapat menggunakan anotasi
@Qualifier . Ini akan lebih deklaratif daripada Java Config, tetapi dalam hal ini Anda harus secara eksplisit menentukan nama kacang di mana kacang saat ini tergantung. Apa kerugiannya menyiratkan: peningkatan konektivitas antara tempat sampah. Dan karena konektivitas meningkat, bahkan dalam kasus perubahan urutan dekorator, perubahan akan dioleskan secara merata pada kode. Artinya, dalam hal menambahkan dekorator baru, misalnya, di tengah rantai, perubahan akan mempengaruhi setidaknya 2 kelas.
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', ;
- - โ , , , .
Kesimpulan
, , . โ : . , , , . โ , JRE. , , .
, โ , , - . Terima kasih sudah membaca!
:
https://github.com/monosoul/spring-di-customization .