본문 바로가기
Spring|Spring-boot

Properties Bean PostProcessor

by oncerun 2023. 2. 2.
반응형

Properties를 등록하기 위해 설정 정보를 담고 있는 클래스를 만든다. 

@Component
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;
    }
}

 

이는 ServletWebServerFactory Bean의 속성 정보로 사용될 프로퍼티 값이다. 

 

이를 주입받기 위해서 우리는 다음과 같이 Bean을 정의할 수 있다.

@Bean(name = "tomcatWebServerFactory")
@ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory(ServerProperties serverProperties) {
    TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
    tomcatServletWebServerFactory.setPort(serverProperties.getPort());
    return tomcatServletWebServerFactory;
}

 

 

그런데 ServerProperties Class에서는 프로퍼티 파일에서 값을 읽어오지 못했다.  이를 읽어오는 방법을 알아보자.

 

우선 Configuration Properties으로 사용되는 클래스임을 마킹하기 위해 다음과 같이 어노테이션을 정의합니다.

 

@Component
@MyConfigurationProperties
public class ServerProperties{..}

 

@MyConfigurationProperties라는 Marker 애노테이션은 빈 후처리기에서 프로퍼티를 바인딩해 주는 작업의 대상을 필터링하는 데 사용됩니다.

 

 

이제 BeanPostProcessor를 추가할 것 입니다.  이는 Bean을 최종적으로 생성한 이후 조작할 수 있는 확장 기능입니다.

@Bean
BeanPostProcessor postProcessor(Environment env) {
    return new BeanPostProcessor() {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            MyConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), MyConfigurationProperties.class);
            if (annotation == null) return bean;
            return Binder.get(env).bindOrCreate("", bean.getClass());
        }
    };
}

 

이제 프로퍼티에 prefix를 붙여보자 .

 

프로퍼티에 대한 namespace역할을 하여 다른 기술 프로퍼티와 중첩되지 않게 하는 방법이다.

 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface MyConfigurationProperties {
    String prefix();

}

@Bean
BeanPostProcessor postProcessor(Environment env) {
    return new BeanPostProcessor() {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            MyConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), MyConfigurationProperties.class);
            if (annotation == null) return bean;

            Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(annotation);
            String prefix = (String) attrs.get("prefix");
            return Binder.get(env).bindOrCreate(prefix, bean.getClass());
        }
    };
}

 

 

조금 더 욕심을 내서 @Import 구문 대신 스프링 부트가 사용하는 EnableConfigurationProperties처럼 동작하도록 만들어 보자.

 

@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
@EnableMyConfigurationProperties(ServerProperties.class)
public class TomcatWebServerConfig {...}

 

@EnableMyConfigurationProperties(ServerProperties.class) 

 

다음과 같이 기존 Import 구문이었던 것은 제외하고 애노테이션을 추가하고 사용할 프로퍼티 클래스를 받아보자.

 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyConfigurationPropertiesImportSelector.class)
public @interface EnableMyConfigurationProperties {
    Class<?> value();
}

 

클래스 타입의 value를 정의해 주고 @Import 구문에서 ImportSelector를 구현하는 클래스를 임포트 하자.

public class MyConfigurationPropertiesImportSelector implements DeferredImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        MultiValueMap<String, Object> attrs = importingClassMetadata.getAllAnnotationAttributes(EnableMyConfigurationProperties.class.getName());
        Class propertyClass = (Class) attrs.getFirst("value");

        return new String[] { propertyClass.getName()};
    }
}

 

다음과 같이 DefeerdImportSelector의 selectImports() 메서드를 구현하자. 

 

이렇게 설정하면 흐름이 다음과 같이 변한다.

 

 

1. 조건 검사.

@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")

 

2. 조건이 통과되면 ServletWebServerFactory, ServerProperties Bean을 생성을 시도한다.

@Bean(name = "tomcatWebServerFactory")
@ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory(ServerProperties serverProperties) {
    TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
    tomcatServletWebServerFactory.setPort(serverProperties.getPort());
    return tomcatServletWebServerFactory;
}

 

3. @EnableMyConfigurationProperties(ServerProperties.class)에 대해서 ImportSelector가 동작하고

EnableMyConfigurationProperties의 모든 속성값을 가져와서 value 값에 들어 있는 클래스를 가져온 이후 클래스 이름을 반환한다. 이때 반환 값은 ServerProperties이다.

 

public class MyConfigurationPropertiesImportSelector implements DeferredImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("sdasd");
        MultiValueMap<String, Object> attrs = importingClassMetadata.getAllAnnotationAttributes(EnableMyConfigurationProperties.class.getName());
        Class propertyClass = (Class) attrs.getFirst("value");

        return new String[] { propertyClass.getName()};
    }
}

 

4. ServerProperties.class를 @Import 하여서 빈이 생성된다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyConfigurationPropertiesImportSelector.class)
public @interface EnableMyConfigurationProperties {
    Class<?> value();
}

5. 빈이 생성된 이후 빈후처리 프로세서가 동작한다.

@MyConfigurationProperties(prefix = "server")
public class ServerProperties{}
@MyAutoConfiguration
public class PropertyPostProcessorConfig {

    @Bean
    BeanPostProcessor postProcessor(Environment env) {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                MyConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), MyConfigurationProperties.class);
                if (annotation == null) return bean;

                Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(annotation);
                String prefix = (String) attrs.get("prefix");
                return Binder.get(env).bindOrCreate(prefix, bean.getClass());
            }
        };
    }
}

@MyConfigurationProperties 어노테이션을 가지고 있는 Bean을 찾아서 프로퍼티 파일을 읽거나 환경변수 등등 값을 초기화한다.

 

6. 마지막으로 ServletWebServerFactory 빈을 생성하는데 생성된 ServerProperties를 주입받는다. 이후 프로퍼티 값을 설정한다.

@Bean(name = "tomcatWebServerFactory")
@ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory(ServerProperties serverProperties) {
    TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
    tomcatServletWebServerFactory.setPort(serverProperties.getPort());
    return tomcatServletWebServerFactory;
}

 

반응형

'Spring|Spring-boot' 카테고리의 다른 글

Spring Boot Actuator  (0) 2023.02.16
Spring Boot 좀 더 자세히.  (2) 2023.02.04
Environment Properties  (0) 2023.01.30
Spring boot AutoConfiguration and Conditional  (0) 2023.01.26
Spring Boot @AutoConfiguration  (0) 2023.01.24

댓글