Menyesuaikan penyelesaian ketergantungan di Musim Semi

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:

  1. Spring mengambil deskriptor dependensi - DependencyDescriptor ;
  2. Kemudian dibutuhkan semua BeanDefinition dari kelas yang sesuai;
  3. Iterate atas BeanDefinitions yang diterima, memanggil metode isAutowireCandidate() dari resolver;
  4. 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 .

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


All Articles