diff --git a/hyeonsik/spring-boot-project/build.gradle b/hyeonsik/spring-boot-project/build.gradle index 301fbd4..5a581b7 100644 --- a/hyeonsik/spring-boot-project/build.gradle +++ b/hyeonsik/spring-boot-project/build.gradle @@ -13,7 +13,7 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' + implementation('org.springframework.boot:spring-boot-starter-web') testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/ConditionalMyOnClass.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/ConditionalMyOnClass.java new file mode 100644 index 0000000..84bd9dc --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/ConditionalMyOnClass.java @@ -0,0 +1,15 @@ +package tobySpringBoot.config; + +import org.springframework.context.annotation.Conditional; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Conditional(MyOnClassCondition.class) +public @interface ConditionalMyOnClass { + String value(); +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/EnableMyConfigurationProperties.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/EnableMyConfigurationProperties.java new file mode 100644 index 0000000..fedb529 --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/EnableMyConfigurationProperties.java @@ -0,0 +1,15 @@ +package tobySpringBoot.config; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Import(MyConfigurationPropertiesImportSelector.class) +public @interface EnableMyConfigurationProperties { + Class value(); +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyConfigurationProperties.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyConfigurationProperties.java new file mode 100644 index 0000000..b04a32e --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyConfigurationProperties.java @@ -0,0 +1,15 @@ +package tobySpringBoot.config; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) // marking하는 역할이므로 +@Component +public @interface MyConfigurationProperties { + String prefix(); +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyConfigurationPropertiesImportSelector.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyConfigurationPropertiesImportSelector.java new file mode 100644 index 0000000..5e5cc0c --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyConfigurationPropertiesImportSelector.java @@ -0,0 +1,14 @@ +package tobySpringBoot.config; + +import org.springframework.context.annotation.DeferredImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.MultiValueMap; + +public class MyConfigurationPropertiesImportSelector implements DeferredImportSelector { + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + MultiValueMap attr = importingClassMetadata.getAllAnnotationAttributes(EnableMyConfigurationProperties.class.getName()); + Class propertyClass = (Class) attr.getFirst("value"); + return new String[] {propertyClass.getName() }; + } +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyOnClassCondition.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyOnClassCondition.java new file mode 100644 index 0000000..41053bd --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/MyOnClassCondition.java @@ -0,0 +1,17 @@ +package tobySpringBoot.config; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.ClassUtils; + +import java.util.Map; + +public class MyOnClassCondition implements Condition { + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + Map annotationAttributes = metadata.getAnnotationAttributes(ConditionalMyOnClass.class.getName()); + String value = (String) annotationAttributes.get("value"); + return ClassUtils.isPresent(value, context.getClassLoader()); + } +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/JettyWebServerConfig.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/JettyWebServerConfig.java new file mode 100644 index 0000000..4ab8f92 --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/JettyWebServerConfig.java @@ -0,0 +1,24 @@ +package tobySpringBoot.config.autoconfig; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.ClassUtils; +import tobySpringBoot.config.ConditionalMyOnClass; +import tobySpringBoot.config.MyAutoConfiguration; + +@MyAutoConfiguration +@ConditionalMyOnClass("org.eclipse.jetty.server.Server") +public class JettyWebServerConfig { + @Bean("jettyWebServerFactory") // bean name은 기본적으로 method 명을 따라간다. 동일 이름이 있으면 충돌이 날 수 있으니 이름을 구분!! + @ConditionalOnMissingBean + public ServletWebServerFactory servletWebServerFactory() { + return new JettyServletWebServerFactory(); + } + +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/PropertyPlaceholderConfig.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/PropertyPlaceholderConfig.java new file mode 100644 index 0000000..0ce61f2 --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/PropertyPlaceholderConfig.java @@ -0,0 +1,14 @@ +package tobySpringBoot.config.autoconfig; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import tobySpringBoot.config.MyAutoConfiguration; +import tobySpringBoot.config.MyConfigurationProperties; + +@MyAutoConfiguration +public class PropertyPlaceholderConfig { + @Bean + PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/PropertyPostProcessorConfig.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/PropertyPostProcessorConfig.java new file mode 100644 index 0000000..bbc8139 --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/PropertyPostProcessorConfig.java @@ -0,0 +1,32 @@ +package tobySpringBoot.config.autoconfig; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import tobySpringBoot.config.MyAutoConfiguration; +import tobySpringBoot.config.MyConfigurationProperties; + +import java.util.Map; + +@MyAutoConfiguration +public class PropertyPostProcessorConfig { + @Bean + BeanPostProcessor propertyPostProcessor(Environment env) { + return new BeanPostProcessor() { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + // 우리가 만든 marker annotation만 이를 적용할 것이다. (MyConfigurationProperties) + MyConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), MyConfigurationProperties.class); + if (annotation == null) return bean; + + Map attrs = AnnotationUtils.getAnnotationAttributes(annotation); // 애노테이션에 있는 모든 value를 찾는다. + String prefix = (String) attrs.get("prefix"); // 그 중 prefix 로 시작하는 녀석을 확인한다. + + return Binder.get(env).bindOrCreate(prefix, bean.getClass()); + } + }; + } +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/ServerProperties.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/ServerProperties.java new file mode 100644 index 0000000..5d350c3 --- /dev/null +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/ServerProperties.java @@ -0,0 +1,28 @@ +package tobySpringBoot.config.autoconfig; + +import org.springframework.beans.factory.annotation.Value; +import tobySpringBoot.config.MyConfigurationProperties; + +// 그저 데이터를 가지고 있는 데이터 홀더 클래스임 -> 그러니 그냥 게터세터로 +@MyConfigurationProperties(prefix = "server") +public class ServerProperties { + private String contextPath; + + private int port; + + public String getContextPath() { + return contextPath; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } +} diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/TomcatWebServerConfig.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/TomcatWebServerConfig.java index b13035d..a13c14b 100644 --- a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/TomcatWebServerConfig.java +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/config/autoconfig/TomcatWebServerConfig.java @@ -1,16 +1,24 @@ package tobySpringBoot.config.autoconfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.*; +import tobySpringBoot.config.ConditionalMyOnClass; +import tobySpringBoot.config.EnableMyConfigurationProperties; import tobySpringBoot.config.MyAutoConfiguration; @MyAutoConfiguration +@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat") +@EnableMyConfigurationProperties(ServerProperties.class) public class TomcatWebServerConfig { - @Bean - public ServletWebServerFactory servletWebServerFactory() { - return new TomcatServletWebServerFactory(); + @Bean("tomcatWebServerFactory") + @ConditionalOnMissingBean // 이미 동일한 타입의 빈이 등록되어있는가? 그렇지 않다면 빈으로 등록해줘라! 라는 condition + public ServletWebServerFactory servletWebServerFactory(ServerProperties properties) { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + factory.setContextPath(properties.getContextPath()); + factory.setPort(properties.getPort()); + return factory; } } diff --git a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/learn/LearnApplication.java b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/learn/LearnApplication.java index deddf8a..3003188 100644 --- a/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/learn/LearnApplication.java +++ b/hyeonsik/spring-boot-project/src/main/java/tobySpringBoot/learn/LearnApplication.java @@ -1,10 +1,14 @@ package tobySpringBoot.learn; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; import tobySpringBoot.config.MySpringBootApplication; @MySpringBootApplication public class LearnApplication { - public static void main(String[] args) { - MySpringApplication.run(LearnApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(LearnApplication.class, args); + } } diff --git a/hyeonsik/spring-boot-project/src/main/resources/META-INF/spring/tobySpringBoot.config.MyAutoConfiguration.imports b/hyeonsik/spring-boot-project/src/main/resources/META-INF/spring/tobySpringBoot.config.MyAutoConfiguration.imports index ea4c6a5..b14fc02 100644 --- a/hyeonsik/spring-boot-project/src/main/resources/META-INF/spring/tobySpringBoot.config.MyAutoConfiguration.imports +++ b/hyeonsik/spring-boot-project/src/main/resources/META-INF/spring/tobySpringBoot.config.MyAutoConfiguration.imports @@ -1,2 +1,5 @@ +tobySpringBoot.config.autoconfig.PropertyPlaceholderConfig +tobySpringBoot.config.autoconfig.PropertyPostProcessorConfig tobySpringBoot.config.autoconfig.TomcatWebServerConfig +tobySpringBoot.config.autoconfig.JettyWebServerConfig tobySpringBoot.config.autoconfig.DispatcherServletConfig diff --git a/hyeonsik/spring-boot-project/src/main/resources/application.properties b/hyeonsik/spring-boot-project/src/main/resources/application.properties index 8b13789..fb34891 100644 --- a/hyeonsik/spring-boot-project/src/main/resources/application.properties +++ b/hyeonsik/spring-boot-project/src/main/resources/application.properties @@ -1 +1,2 @@ - +server.contextPath=/app +server.port=9090 diff --git a/hyeonsik/spring-boot-project/src/test/java/tobySpringBoot/study/ConditionalTest.java b/hyeonsik/spring-boot-project/src/test/java/tobySpringBoot/study/ConditionalTest.java new file mode 100644 index 0000000..6306909 --- /dev/null +++ b/hyeonsik/spring-boot-project/src/test/java/tobySpringBoot/study/ConditionalTest.java @@ -0,0 +1,73 @@ +package tobySpringBoot.study; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.*; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import java.beans.BeanProperty; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +//import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class ConditionalTest { + @Test + void conditional() { +// // true +// ApplicationContextRunner contextRunner = new ApplicationContextRunner(); +// contextRunner.withUserConfiguration(Config1.class) +// .run(context -> { +// assertThat(context).hasSingleBean(MyBean.class); +// assertThat(context).hasSingleBean(Config1.class); +// }); +// +// // false +// new ApplicationContextRunner().withUserConfiguration(Config1.class) +// .run(context -> { +// assertThat(context).doesNotHaveBean(MyBean.class); +// assertThat(context).doesNotHaveBean(Config1.class); +// }); + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Conditional(BooleanCondition.class) + @interface BooleanConditional { + boolean value(); + } + + @Configuration + @BooleanConditional(true) + static class Config1 { // <- static으로 선언하면 inner class 처럼 취급되지 않는다. 대신 상위 클래스가 마치 package 역할을 한다. + @Bean + MyBean myBean() { + return new MyBean(); + } + } + + @Configuration + @BooleanConditional(false) + static class Config2 { + @Bean + MyBean myBean() { + return new MyBean(); + } + } + + static class MyBean { + } + + static class BooleanCondition implements Condition { + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + Map annotationAttributes = metadata.getAnnotationAttributes(BooleanConditional.class.getName()); + return (Boolean) annotationAttributes.get("value"); + } + } + +}