계기
개발을 하다 보면 Bean이 아닌 클래스인데 Bean을 참조해야 하는 경우가 있다.
그런 경우 이런 방식으로 ApplicationContextProvider를 만들어 주면 된다. Spring Bean이 아닌 클래스에서 Bean 가져오기
Spring Boot가 실행될 때는 ApplicationContext가 null
인 상황이 없을 줄 알았다.
하지만 개발 중 그런 상황을 마주쳤다.
특정 Bean 클래스의 @PostConstruct
메서드에서 수행하는 작업에 위와 같이 Bean이 아닌 클래스에서 Bean을 참조하는 작업이 포함되었을 경우이다.
정확히 말해 실제 ApplicationContext가 null
은 아니고
ApplicationContextAware를 구현한 ApplicationContextProvider 클래스의 setApplicationContext 메서드가 @PostConstruct
보다 늦게 실행되어서 그 시점에는 ApplicationContextProvider.applicationContext가 null 인 상황이다.
데모 프로젝트를 만들어서 테스트해 보니
@PostConstruct
가 끝나야 setApplicationContext가 호출된다. 즉 Bean들의 초기화가 끝나야 setApplicationContext가 호출되기 때문에@PostConstruct
에서 ApplicationContextProvider를 사용하면 무조건null
이라는 얘기다. Thread를 사용했을 때의 변수(variable 아님)는 논외이다.
그래서 ApplicationContextProvider.setApplicationContext 메서드가 실행된 이후에 내가 만든 Annotation을 붙인 함수를 실행하는 Annotation을 만들기로 했다.
원하는 기능
@PostConstruct
와 같이 method에 붙일 수 있는 Annotation- 내가 원하는 시점에 실행 시킬 수 있어야함
- 어느 클래스의 어느 메서드에 붙이던지 상관 없이 감지하고 실행해야함
구현
Annotation 구현
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostApplicationContextSet {
}
CustomInitAnnotationProcessor
BeanPostProcessor
를 구현해서 postProcessBeforeInitialization
메서드를 작성하면 된다.
자세한 건 모르지만 대략 모든 Bean이 초기화 될 때마다 호출되는 것 같다.
bean에서 클래스를 얻고, class에서 method 목록을 얻는다.
그럼 method 목록을 순회하며 method.isAnnotationPresent
함수를 이용하여 해당 메서드에 내가 원하는 Annotation이 붙었는지 확인한다.
붙었다면 해당하는 bean과 method를 내부 변수에 저장하고 있다가 사용할 곳에서 가져가서 사용하면 된다.
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Getter
@Component
public class CustomInitAnnotationProcessorForPostApplicationContextSet implements
BeanPostProcessor {
private final List<InvokeInfo> invokeInfoList = new ArrayList<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class<?> clazz = bean.getClass();
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(PostApplicationContextSet.class)) {
InvokeInfo invokeInfo = new InvokeInfo();
invokeInfo.setBean(bean);
invokeInfo.setMethod(method);
invokeInfoList.add(invokeInfo);
}
}
return bean;
}
}
InvokeInfo
이 클래스는 Bean과 Method를 같이 담기 위해서 내가 만든 Data 용도 클래스이다.
import java.lang.reflect.Method;
import lombok.Data;
@Data
public class InvokeInfo {
private Object bean;
private Method method;
}
ApplicationContextProvider
글 초반에 링크된 ApplicationContextProvider 글을 참고.
만들어져 있는 ApplicationContextProvider를 수정한다.
CustomInitAnnotationProcessor에 저장된 invokeInfoList를 가져와서 순회를 돌며서 해당 method를 invoke 해주는 메서드를 작성한다.
private void executeAfterContextSet() {
List<InvokeInfo> invokeInfoList = customInitAnnotationProcessorForPostApplicationContextSet.getInvokeInfoList();
for (InvokeInfo invokeInfo : invokeInfoList) {
if (invokeInfo.getMethod().isAnnotationPresent(PostApplicationContextSet.class)) {
try {
invokeInfo.getMethod().invoke(invokeInfo.getBean());
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}
setApplicationContext 이후에 실행한다.
@Override
public void setApplicationContext(...) ... {
...
executeAfterContextSet();
}
ApplicationContextProvider 부분의 이해를 돕기 위해 전체적인 코드를 첨부한다. (필요 없는 부분 생략)
...
@Component
@RequiredArgsConstructor
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private final CustomInitAnnotationProcessorForPostApplicationContextSet customInitAnnotationProcessorForPostApplicationContextSet;
...
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext)
throws BeansException {
ApplicationContextProvider.applicationContext = applicationContext;
executeAfterContextSet();
}
private void executeAfterContextSet() {
List<InvokeInfo> invokeInfoList = customInitAnnotationProcessorForPostApplicationContextSet.getInvokeInfoList();
for (InvokeInfo invokeInfo : invokeInfoList) {
if (invokeInfo.getMethod().isAnnotationPresent(PostApplicationContextSet.class)) {
try {
invokeInfo.getMethod().invoke(invokeInfo.getBean());
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}
}
향후 개선할 점
- 현재는 Annotation이 붙는 함수가 public이어야 하는데 private이어도 동작하게 개선