مرحبا اسمي Andrey Nevedomsky وأنا كبير المهندسين في SberTekh. أعمل في فريق يقوم بتطوير إحدى خدمات النظام في نظام ESF (نظام أمامي موحد). في عملنا ، نستخدم بنشاط Spring Framework ، وخاصةً DI ، ومن وقت لآخر نواجه حقيقة أن حل التبعيات في الربيع ليس ذكيًا بما فيه الكفاية بالنسبة لنا. هذه المقالة هي نتيجة لمحاولاتي جعلها أكثر ذكاءً وفهم كيف تعمل بشكل عام. آمل أن تتمكن من تعلم شيء جديد منه حول جهاز الربيع.

قبل قراءة المقال ، أوصي بشدة أن تقرأ تقارير
بوريس يفغيني
إفغيني بوريسوف :
Spring Ripper، Part 1 ؛
ربيع الخارق ، الجزء 2 . لا يزال هناك
قائمة تشغيل منهم .
مقدمة
دعنا نتخيل أنه طُلب منا تطوير خدمة للتنبؤ بالمصير والأبراج. تحتوي خدمتنا على العديد من المكونات ، ولكن أهمها بالنسبة لنا هو مكونان:
- Globa ، التي ستنفذ واجهة FortuneTeller وتتنبأ بالمصير ؛

- الغجر ، والتي ستنفذ واجهة HoroscopeTeller وإنشاء الأبراج.

أيضا في خدمتنا سيكون هناك العديد من نقاط النهاية (وحدات التحكم) ، في الواقع ، للحصول على الكهانة والأبراج. وسنتحكم أيضًا في الوصول إلى تطبيقنا عن طريق بروتوكول الإنترنت باستخدام جانب سيتم تطبيقه على طرق التحكم وسيبدو كما يلي:
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 في نفس الوقت.
لكننا نريد أن نختبر بالضبط ما هو الجانب المطبق ويقوم بعمله. لا نريد اختبار الخدمات التي يطلق عليها جهاز التحكم ، وبالتالي لا نريد أن نكون في حيرة من بيانات الاختبار والتضحية بوقت بدء التشغيل. لذلك ، سوف نكتب الاختبارات التي سنطرح فيها جزءًا فقط من السياق. أي في سياقنا سيكون هناك فاصوليا حقيقية وجهاز تحكم حقيقي ، وكل شيء آخر سيكون mokami.
كيفية إنشاء حبوب موكا؟
هناك عدة طرق لإنشاء حبوب موكا في الربيع. للتوضيح ، على سبيل المثال ، نأخذ إحدى وحدات التحكم في خدمتنا -
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 مع التبعيات في كل اختبار
بالنسبة لكل اختبار ، يمكننا كتابة Java Config الذي نصف فيه كلاً من وحدة التحكم والفاصوليا الجانبية والفاصوليا باستخدام moks التبعية للتحكم. ستكون هذه الطريقة لوصف الفاصوليا ضرورية ، لأننا سنخبر الربيع بوضوح كيف نحتاج إلى إنشاء الفاصوليا.
في هذه الحالة ، سيبدو اختبار جهاز التحكم الخاص بنا كما يلي:
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 لكل وحدة من وحدات التحكم. على الرغم من اختلافه في المحتوى ، إلا أنه سيكون له نفس المعنى: إنشاء وحدة تحكم و moki تبعياتها. لذلك في جوهرها سيكون هو نفسه بالنسبة لجميع وحدات التحكم. أنا ، مثل أي مبرمج ، شخص كسول ، لذلك رفضت على الفور هذا الخيار.
التعليقات التوضيحيةMockBean على كل حقل مع التبعية
ظهر
التعليق التوضيحيMockBean في إصدار Spring Boot Test 1.4.0. إنه مشابه لـ
Mock من Mockito (بل إنه في الواقع يستخدمه داخليًا) ، مع الاختلاف الوحيد أنه عند استخدام
@MockBean
، سيتم وضع
@MockBean
تلقائيًا في سياق الربيع. ستكون هذه الطريقة لإعلان mok تعريفيًا ، نظرًا لأننا لا نضطر إلى إخبار الربيع بالتحديد عن كيفية إنشاء mok.
في هذه الحالة ، سيبدو الاختبار كما يلي:
موكبين / مشخصنةمنظار الأبراج TellControllerTest.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 ، لكنه مضغوط بدرجة أكبر. من بين أوجه القصور - اضطررت إلى إعلان الحقول ذات التبعيات وحدة التحكم (الحقول مع تعليق توضيحي
@MockBean
) ، على الرغم من عدم استخدامها في الاختبار بشكل أكبر. حسنًا ، إذا كنت تستخدم إصدار Spring Boot أقل من 1.4.0 لسبب ما ، فلن تتمكن من استخدام هذا التعليق التوضيحي.
لذلك ، توصلت إلى فكرة عن خيار آخر للتطهير. أود أن تعمل بهذه الطريقة ...
@ التعليقات التوضيحية التلقائية على المكون التابع
أود أن يكون لدينا تعليق توضيحي
@Automocked
، والذي يمكنني وضعه فقط فوق الحقل مع وحدة التحكم ، ومن ثم سيتم إنشاء moki تلقائيًا لوحدة التحكم هذه ووضعها في السياق.
قد يبدو الاختبار في هذه الحالة كما يلي:
أوتوكوكيد / شخصية هوروسكوبيلكونتروليرترست.جافا @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); } }
كما ترون ، فإن هذا الخيار هو الأكثر
@Automocked
المقدمة ، لا يوجد سوى وحدة تحكم وحدة التحكم (بالإضافة إلى تقييم الجانب الخاص بالجانب) ،
@Automocked
التعليق التوضيحي
@Automocked
، وكل
سحر إنشاء الحبوب ووضعها في السياق يتم كتابته مرة واحدة ويمكن استخدامه في الكل الاختبارات.
كيف يعمل؟
دعونا نرى كيف يعمل وماذا نحتاج لهذا.
TestExecutionListener
هناك مثل هذه الواجهة في
الربيع -
TestExecutionListener . يوفر واجهة برمجة التطبيقات للتضمين في عملية تنفيذ الاختبار في مراحلها المختلفة ، على سبيل المثال ، عند إنشاء مثيل لفئة اختبار ، قبل أو بعد استدعاء طريقة اختبار ، إلخ. لديه العديد من التطبيقات من خارج منطقة الجزاء. على سبيل المثال ،
DirtiesContextTestExecutionListener ، الذي ينظف السياق إذا وضعت التعليق التوضيحي المناسب ؛
DependencyInjectionTestExecutionListener - يؤدي حقن التبعية في الاختبارات ، إلخ. لتطبيق المستمع المخصص الخاص بك على الاختبار ، تحتاج إلى وضع التعليق التوضيحي
@TestExecutionListeners
فوقه والإشارة إلى التطبيق الخاص بك.
أمرت
هناك أيضا واجهة
أمرت في الربيع. يتم استخدامه للإشارة إلى أنه يجب فرز الكائنات بطريقة ما. على سبيل المثال ، عندما يكون لديك عدة تطبيقات لنفس الواجهة وترغب في ضخها في مجموعة ، فسيتم طلبها في هذه المجموعة وفقًا للطلب. في حالة TestExecutionListener ، يشير هذا التعليق التوضيحي إلى أي ترتيب يجب تطبيقه.
لذلك ، سيقوم المستمع لدينا بتنفيذ واجهات 2:
TestExecutionListener و
Ordered . نحن نسميها
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()
، والذي بدوره يستدعي الأسلوب
createMocksForParameters()
، حيث يتم إنشاء mocks
createMocksForParameters()
وتسجيله في السياق:
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<java.lang.String, java.lang.String>"
. هذا مهم ، سنعود إلى هذا.
بعد إنشاء يسخر لجميع الحجج وتسجيلها في السياق ، نعود إلى إنشاء فاصوليا للفئة التابعة (على سبيل المثال ، وحدة التحكم في حالتنا):
if (!beanFactory.containsBean(beanName)) { val bean = beanFactory.createBean(clazz); beanFactory.registerSingleton(beanName, bean); }
يجب عليك أيضًا الانتباه إلى حقيقة أننا استخدمنا
الأمر 1900 . يعد ذلك ضروريًا لأنه يجب استدعاء المستمع الخاص بنا بعد مسح سياق
DirtiesContextBeforeModesTestExecutionListener 'أوم (ترتيب = 1500) وقبل
DependencyInjectionTestExecutionListener '
s حقن التبعية (ترتيب = 2000) ، لأن المستمع لدينا بإنشاء صناديق جديدة.
AutowireCandidateResolver
يستخدم
AutowireCandidateResolver لتحديد ما إذا كان
BeanDefinition يطابق وصف التبعية. لديه العديد من التطبيقات "خارج الصندوق" ، من بينها:
في الوقت نفسه ، فإن تطبيق "خارج الصندوق" هو دمية روسية من الميراث ، أي أنها تتوسع بعضها البعض. سنكتب ديكور ، لأنه هو أكثر مرونة.
يعمل محلل على النحو التالي:
- يأخذ الربيع واصف التبعية - DependencyDescriptor ؛
- ثم يأخذ كل BeanDefinition من الفئة المناسبة؛
- يتكرر عبر BeanDefinitions ، استدعاء أسلوب
isAutowireCandidate()
من محلل ؛
- اعتمادًا على ما إذا كان وصف الحاوية يتطابق مع وصف التبعية أم لا ، تُرجع الطريقة صح أو خطأ.
لماذا تحتاج محلل الخاص بك؟
الآن دعونا نرى لماذا نحن بحاجة إلى محلل لدينا على سبيل المثال من وحدة تحكم لدينا.
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; }
كما ترون ، فقد اثنين من التبعيات من نفس النوع -
وظيفة ، ولكن مع الأدوية الجنيسة مختلفة. في حالة واحدة ،
String و
ZodiacSign ، في الحالة الأخرى ،
String و
String . والمشكلة في ذلك هي أن
موكيتو غير قادر على إنشاء موكس مع مراعاة الأدوية الجنيسة . أي إذا أنشأنا mokas لهذه التبعيات ووضعناها في سياقها ، فلن يتمكن Spring من حقنها في هذه الفئة ، لأنها لن تحتوي على معلومات حول الأدوية العامة. وسنرى استثناء أنه في السياق يوجد أكثر من حبة واحدة من فئة
الدالة . إنها بالضبط هذه المشكلة التي سنحلها بمساعدة محللنا. بعد كل شيء ، كما تتذكر ، في تطبيقنا لـ المستمع ، استخدمنا نوعًا من الوراثة كاسم للحاوية ، مما يعني أن كل ما نحتاج إليه هو تعليم الربيع لمقارنة نوع التبعية مع اسم الحاوية.
AutomockedBeanByNameAutowireCandidateResolver
لذلك ، سوف يقوم
isAutowireCandidate()
بعمل ما كتبته أعلاه تمامًا ،
isAutowireCandidate()
تنفيذ طريقة
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 (الذي يحتوي بالفعل على تمثيل سلسلة لنوع الحبة) ، ثم يقوم بمقارنتها ، وإذا كان مطابقًا ، فسيتم إرجاعه. إذا لم تكن متطابقة ، فستفوض إلى محلل داخلي.
اختبار خيارات ترطيب بن
في المجموع ، في الاختبارات ، يمكننا استخدام الخيارات التالية لترطيب الصناديق:
- تكوين Java - سيكون أمرًا ضروريًا ، ضخمًا ، يحتوي على قالب نحاسي ، ولكن ربما يكون مفيدًا قدر الإمكان ؛
@MockBean
- سيكون إعلانيًا ، وأقل تعقيدًا من Java Config ، ولكن سيظل هناك صفيحة صغيرة في شكل حقول ذات تبعيات غير مستخدمة في الاختبار نفسه ؛
@Automocked
+ محلل مخصص - الحد الأدنى من التعليمات البرمجية في الاختبارات و boilerplate ، ولكن يحتمل أن يكون نطاق ضيق للغاية وهذا لا يزال يحتاج إلى كتابتها. ولكن يمكن أن تكون مريحة للغاية حيث تريد التأكد من أن الربيع بشكل صحيح يخلق وكلاء.
إضافة الديكور
فريقنا
يحب قالب تصميم
Decorator لمرونته. في الواقع ، فإن الجوانب تنفذ هذا النمط بالذات. ولكن في حالة تكوين سياق الربيع مع التعليقات التوضيحية واستخدام تفحص الحزمة ، فستواجه مشكلة. إذا كان لديك العديد من تطبيقات نفس الواجهة في السياق ، فعند
بدء تشغيل التطبيق ،
فسوف تسقط NoUniqueBeanDefinitionException ، أي لن يكون الربيع قادرًا على معرفة أي من الحبوب يجب حقنه. تحتوي هذه المشكلة على العديد من الحلول ، وبعد ذلك سننظر فيها ، ولكن أولاً ، دعونا نتعرف على كيفية تغير تطبيقنا.
الآن لدى كل من واجهات
FortuneTeller و
HoroscopeTeller تطبيق واحد ، سنضيف
تطبيقين إضافيين لكل من الواجهات:

- التخزين المؤقت ... - ديكور التخزين المؤقت.
- تسجيل ... هو الديكور تسجيل.
إذا كيف يمكنك حل مشكلة تحديد ترتيب الفاصوليا؟
جافا التكوين مع ديكور المستوى الأعلى
يمكنك استخدام Java Config مرة أخرى. في هذه الحالة ، سنقوم بوصف الفاصوليا كطرق لفئة التكوين ، وسيتعين علينا تحديد الوسائط اللازمة لاستدعاء مُنشئ الفاصوليا كوسيطات لهذه الطريقة. من الذي يتبع ذلك في حالة حدوث تغيير في مُنشئ سلة المهملات ، سيتعين علينا تغيير التكوين ، وهو أمر غير رائع جدًا. من مزايا هذا الخيار:
- سيكون هناك اتصال منخفضة بين الديكور ، كما سيتم وصف العلاقة بينهما في التهيئة ، أي لن يعرفوا شيئًا عن بعضهم البعض ؛
- سيتم ترجمة جميع التغييرات في ترتيب الديكور في مكان واحد - التكوين.
في حالتنا ، سيبدو 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 ) ); } }
كما ترون ، يتم الإعلان عن حبة واحدة فقط لكل من الواجهات ، وتتضمن الطرق في الوسيطات تبعيات كل الكائنات التي تم إنشاؤها داخلها. في هذه الحالة ، يكون منطق إنشاء الفاصوليا واضحًا إلى حد ما.
التصفيات
يمكنك استخدام التعليقات التوضيحية
Qualifier . سيكون هذا أكثر تعريفًا من Java Config ، ولكن في هذه الحالة ، ستحتاج إلى تحديد اسم الحبة التي تعتمد عليها الحبة الحالية بشكل صريح. ما يتضمنه العيب: زيادة الاتصال بين الصناديق. ونظرًا لزيادة الاتصال ، حتى في حالة حدوث تغيير في ترتيب الديكور ، سيتم تلطيخ التغييرات بالتساوي على الكود. أي إذا تم إضافة ديكور جديد ، على سبيل المثال ، في منتصف السلسلة ، ستؤثر التغييرات على فئتين على الأقل.
LoggingFortuneTeller.java @Primary @Component public final class LoggingFortuneTeller implements FortuneTeller { private final FortuneTeller internal; private final Logger logger; public LoggingFortuneTeller( @Qualifier("cachingFortuneTeller") @NonNull final FortuneTeller internal ) { this.internal = internal; this.logger = getLogger(internal.getClass()); }
, , ( ,
FortuneTeller , ),
@Primary .
internal @Qualifier
, —
cachingFortuneTeller . .
Custom qualifier
2.5 Qualifier', . .
enum :
public enum DecoratorType { LOGGING, CACHING, NOT_DECORATOR }
, qualifier':
@Qualifier @Retention(RUNTIME) public @interface Decorator { DecoratorType value() default NOT_DECORATOR; }
: ,
@Qualifier
,
CustomAutowireConfigurer , .
Qualifier' :
CachingFortuneTeller.java @Decorator(CACHING) @Component public final class CachingFortuneTeller implements FortuneTeller { private final FortuneTeller internal; private final Map<FortuneRequest, FortuneResponse> cache; public CachingFortuneTeller( @Decorator(NOT_DECORATOR) final FortuneTeller internal, final Map<FortuneRequest, FortuneResponse> cache ) { this.internal = internal; this.cache = cache; }
– ,
@Decorator
, , – ,
,
FortuneTeller ', –
Globa .
Qualifier' - , - . , , . , - – , , .
DecoratorAutowireCandidateResolver
– ! ! :) , - , Java Config', . , - , . :
DomainConfig.java @Configuration public class DomainConfig { @Bean public OrderConfig<FortuneTeller> fortuneTellerOrderConfig() { return () -> asList( LoggingFortuneTeller.class, CachingFortuneTeller.class, Globa.class ); } @Bean public OrderConfig<HoroscopeTeller> horoscopeTellerOrderConfig() { return () -> asList( LoggingHoroscopeTeller.class, CachingHoroscopeTeller.class, Gypsy.class ); } }
– Java Config' , – . , !
- . , , , . :
@FunctionalInterface public interface OrderConfig<T> { List<Class<? extends T>> getClasses(); }
BeanDefinitionRegistryPostProcessor
BeanDefinitionRegistryPostProcessor , BeanFactoryPostProcessor, , , , BeanDefinition'. , BeanFactoryPostProcessor, .
:
- BeanDefinition';
- BeanDefinition' , OrderConfig '. , .. BeanDefinition' ;
- , OrderConfig ', BeanDefinition', , () .
BeanFactoryPostProcessor
BeanFactoryPostProcessor , BeanDefinition' , . , « Spring-».

, , – AutowireCandidateResolver':
DecoratorAutowireCandidateResolverConfigurer.java @Component class DecoratorAutowireCandidateResolverConfigurer implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(final ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { Assert.state(configurableListableBeanFactory instanceof DefaultListableBeanFactory, "BeanFactory needs to be a DefaultListableBeanFactory"); val beanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory; beanFactory.setAutowireCandidateResolver( new DecoratorAutowireCandidateResolver(beanFactory.getAutowireCandidateResolver()) ); } }
DecoratorAutowireCandidateResolver
:
DecoratorAutowireCandidateResolver.java @RequiredArgsConstructor public final class DecoratorAutowireCandidateResolver implements AutowireCandidateResolver { private final AutowireCandidateResolver resolver; @Override public boolean isAutowireCandidate(final BeanDefinitionHolder bdHolder, final DependencyDescriptor descriptor) { val dependentType = descriptor.getMember().getDeclaringClass(); val dependencyType = descriptor.getDependencyType(); val candidateBeanDefinition = (AbstractBeanDefinition) bdHolder.getBeanDefinition(); if (dependencyType.isAssignableFrom(dependentType)) { val candidateQualifier = candidateBeanDefinition.getQualifier(OrderQualifier.class.getTypeName()); if (candidateQualifier != null) { return dependentType.getTypeName().equals(candidateQualifier.getAttribute("value")); } } return resolver.isAutowireCandidate(bdHolder, descriptor); }
descriptor' (dependencyType) (dependentType):
val dependentType = descriptor.getMember().getDeclaringClass(); val dependencyType = descriptor.getDependencyType();
bdHolder' BeanDefinition:
val candidateBeanDefinition = (AbstractBeanDefinition) bdHolder.getBeanDefinition();
. , :
dependencyType.isAssignableFrom(dependentType)
, , .. .
BeanDefinition' :
val candidateQualifier = candidateBeanDefinition.getQualifier(OrderQualifier.class.getTypeName());
, :
if (candidateQualifier != null) { return dependentType.getTypeName().equals(candidateQualifier.getAttribute("value")); }
– (), – false.
, :
- Java Config – , , , ;
@Qualifier
– , - ;
- Custom qualifier – , Qualifier', ;
- - – , , , .
الاستنتاجات
, , . – : . , , , . – , JRE. , , .
, – , , - . شكرا للقراءة!
:
https://github.com/monosoul/spring-di-customization .