ããã«ã¡ã¯ ç§ã®ååã¯ã¢ã³ãã¬ã€ã»ãããã ã¹ããŒã§ãç§ã¯SberTechã®ããŒããšã³ãžãã¢ã§ãã ç§ã¯ãESFïŒUnified Frontal SystemïŒã®ã·ã¹ãã ãµãŒãã¹ã®1ã€ãéçºããããŒã ã§åããŠããŸãã ç§ãã¡ã®ä»äºã§ã¯ãSpring Frameworkãç¹ã«ãã®DIãç©æ¥µçã«äœ¿çšããŠããŸãããSpringã§äŸåé¢ä¿ã解決ããããšã¯ç§ãã¡ã«ãšã£ãŠååã«è³¢ããªããšããäºå®ã«æã
çŽé¢ããŠããŸãã ãã®èšäºã¯ããããããã¹ããŒãã«ãããããã©ã®ããã«æ©èœããããäžè¬çã«çè§£ããããšããç§ã®è©Šã¿ã®çµæã§ãã æ¥ã®è£
眮ã«ã€ããŠãããããäœãæ°ããããšãåŠã¹ãããšãé¡ã£ãŠããŸãã

èšäºãèªãåã«ã
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ãäŸåé¢ä¿ã®èª¬æãš
äžèŽãããã©ããã倿ããããã«äœ¿çšãããŸãã 圌ã«ã¯ãããã€ãã®å®è£
ããããã«äœ¿ããããã¡ã«ãããŸãã
åæã«ããç®±ããåºããŠãå®è£
ã¯ãç¶æ¿ããã®ãã·ã¢ã®äººåœ¢ã§ãã ãããã¯äºãã«æ¡åŒµããŸãã ãã³ã¬ãŒã¿ãäœæããŸãããªããªã ããæè»ã§ãã
ãªãŸã«ããŒã¯æ¬¡ã®ããã«æ©èœããŸãã
- 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.
, :
- Java Config â , , , ;
@Qualifier
â , - ;
- Custom qualifier â , Qualifier', ;
- - â , , , .
çµè«
, , . â : . , , , . â , JRE. , , .
, â , , - . !
:
https://github.com/monosoul/spring-di-customization .