@Component
@Controller@ServiceJunit에서 @Test와 비슷한 기능을 하는 애노테이션 만들어보는 예제다.@Retention(RetentionPolicy.RUNTIME) // 런타임에 필요
@Target(ElementType.METHOD) // 메소드에 적용 가능
@Test
@interface UnitTest {
}
public class HelloServiceTest {
@UnitTest
void simpleHelloService() {
// ...
}
}
ex) @RestController = @Controller + @ResponseBody
@Configuration + @ComponentScan으로 동작시켰던 애플리케이션도 합성 애노테이션으로 조합할 수 있다.@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
public @interface MySpringbootAnnotation {
}
@MySpringbootAnnotation
public class HellobootApplication {
//...
}
HelloControllerSimpleHelloServiceDataSourceJpaEntityManagerFactoryJdbcTransactionManagerApplicationContext/BeanFactoryEnvironmentBeanPostProcessorBeanFactoryPostProcessorDefaultAdvisorAutoProxyCreator@ComponentScan을 통해 자동으로 자바 코드와 어노테이션에서 구성 정보를 읽어서 빈을 등록한다.@Configuration 클래스들이 있고 스프링부트가 필요한 구성을 찾아 자동 적용한다는 것이 기본 동작 원리이다./*
main
- java
- tobyspring
- config (자동 구성 정보)
- TomcatWebServerConfig.java
- DispatcherServletConfig.java
- helloboot (컴포넌트 스캔 범위, 사용자 구성 정보)
- MySpringBootApplication.java
*/
@Configuration
public class TomcatWebServerConfig {
@Bean
public ServletWebServerFactory servrWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
@Configuration
public class DispatcherServletConfig {
@Bean
public DispatcherServlet dispatcherServlet() {
return DispatcherServlet();
}
}
@MySpringApplicationApplication 어노테이션에 다음 설정을 추가하면 된다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({ // 추가
DispatcherServletConfig.class,
TomcatWebServerConfig.class
})
public @interface MySpringbootApplication {
}
EnableMyAutoConfiguration 설정 클래스로 합쳐보자
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({
DispatcherServletConfig.class,
TomcatWebServerConfig.class
})
public @interface EnableMyAutoConfiguration {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@EnableMyAutoConfiguration
public @interface MySpringbootApplication {
}
EnableMyAutoConfiguration을 만들 때 인프라 빈을 수동으로 Import했다.ImportSelecter 인터페이스를 사용할 수 있다.public interface ImportSelecter {
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() { return null; }
}
ImportSelecter를 확장한 DefaultImportSelecter를 사용하면 Configuration이 붙은 클래스를 동적으로 등록할 수 있다.public class MyAutoConfigImportSelecter implements DefaultImportSelecter {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new Strint[] {
"tobyspring.config.autoconfig.DispatcherServletConfig",
"tobyspring.config.autoconfig.TomcatWebServerConfig"
};
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyAutoConfigImportSelecter.class)
public @interface EnableMyAutoConfiguration {
}
MyAutoConfiguration이라는 메타 어노테이션을 만든다.@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
public @interface MyAutoConfiguration {
}
MyAutoConfigImportSelecter를 아래와 같이 수정한다.public class MyAutoConfigImportSelecter implements DefaultImportSelecter {
private final ClassLoader classLoader;
public MyAutoConfigImportSelecter(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> autoConfigs = new ArrayList<>();
ImportCandidates.load(MyAutoConfiguration.class, classLoader)
.forEach(autoConfigs::add);
return autoConfigs.toArray(new String[]);
}
}
ImportCandidates.load()
META-INF/spring/ 경로 밑에 {full-qualified-annotation-name}.imports 파일에서 클래스 정보를 읽어온다.resources
- META-INF.spring
- tobyspring.config.MyAutoConfiguration.imports
tobyspring.config.MyAutoConfiguration.imports 파일 내부에 인프라 빈 정보를 작성한다.tobyspring.config.autoconfig.DispatcherServletConfig
tobyspring.config.autoconfig.TomcatWebServerConfig
ImportCandidates.load 메서드는 MyAutoConfiguration 네이밍이 된 파일에서 빈 정보를 찾는다.DispatcherServletConfig와 TomcatWebServerConfig에 적용되어 있던 @Configuration을 @MyAutoConfiguration으로 변경한다.
MyAutoConfiguration.imports 파일에서 읽어오는 정보라는 것을 명시하는 관례다.@MyAutoConfiguration
public class TomcatWebServerConfig {
@Bean
public ServletWebServerFactory servrWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
@MyAutoConfiguration
public class DispatcherServletConfig {
@Bean
public DispatcherServlet dispatcherServlet() {
return DispatcherServlet();
}
}
@MyAutoConfiguration 내부 @Configuration의 엘리먼트를 하나 변경한다.@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration(proxyBeanMethods = false) // 변경, 기본값은 true
public @interface MyAutoConfiguration {
}
@Configuration을 학습하기 위해 학습 테스트를 만들어보겠다
Bean1과 Bean2가 Common이라는 같은 빈을 의존하는 상황이라 가정@Configuration
static class MyConfig {
@Bean
Common common() {
return new Common();
}
@Bean
Bean1 bean1() {
return new Bean1(common());
}
@Bean
Bean1 bean2() {
return new Bean2(common());
}
}
Bean1과 Bean2는 다른 common 인스턴스를 의존할 것이다.@Test
void configuration() {
MyConfig myConfig = new MyConfig();
Bean1 bean1 = myConfig.bean1();
Bean2 bean2 = myConfig.bean2();
assertThat(bean1.common).isSamdAs(bean2.common) // 실패
}
MyConfig가 스프링 컨테이너의 구성 정보로 사용되면 동작 방식이 달라지게 된다.@Test
void configuration() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.register(MyConfig.class);
ac.refersh();
Bean1 bean1 = ac.getBean(Bean1.class);
Bean2 bean2 = ac.getBean(Bean2.class);
assertThat(bean1.common).isSamdAs(bean2.common) // 성공
}
proxyBeanMethods = true인 경우 (기본값)
Configuration이 붙은 설정 클래스 빈이 직접 등록되는 것이 아닌 프록시로 등록된다.@Configuration
static class MyConfigProxy extends MyConfig {
private Common common;
@Override
Common common() {
if (this.common == null) this.common = super.common();
return this.common;
}
// ...
}
proxyBeanMethods를 false로 변경하는 것이 가능해져 Configuration 빈이 프록시로 대체되지 않는다.
// @EnableScheduling가 import로 가져오는 설정 클래스
@Configuration(proxyBeanMethods = false)
@Rold(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
// ...
}