Project Winter
원래 애노테이션 기반으로 싱글톤 빈을 런타임시에 생성/관리 해주는 기능은 프로젝트 막바지에 개발하려 했다.
하지만 지난번 컨트롤러 기능 개발 중 컨트롤러 매핑 문제가 있었고, 문제 해결 방법으로 @Configuration
애노테이션이 붙은 설정 클래스에서 컨트롤러를 매핑해주기로 했다.
이 기능을 구현하기 위해 애노테이션 기반 빈 객체 생성
기능을 먼저 개발하였다.
Annotation
우선 애노테이션은 위와 같이 다섯개를 미리 만들어 뒀다.
@Component
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}
애플리케이션이 실행하면서 생성할 빈을 찾을 때, @Component
애노테이션이 붙은 클래스를 대상으로 검색을 하게끔 하였다.
따라서 @Controller
, @Configuration
등 빈으로 관리할 애노테이션들은 이 애노테이션을 모두 가지고 있어야 한다.
Configuration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Configuration {
}
설정 클래스들에 붙여줄 애노테이션이다. 후에 설명할 @Bean
애노테이션을 붙인 메서드를 만들어 빈으로 관리할 객체를 지정할수도 있다.
Controller
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Controller {
}
컨트롤러 역할을 할 클래스들에 붙여줄 애노테이션이다. 이 애노테이션을 기반으로 컨트롤러를 생성, 후에 설명할 @RequestMapping 애노테이션과 함께 핸들러 매핑을 하게 된다.
Bean
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
String name() default "";
}
메서드 레벨에 붙일 수 있으며 위에 설명한 @Configuration
클래스 안의 메서드에 붙으면 해당 메서드의 리턴값은 싱글톤 빈으로 우선 관리된다.
빈의 이름을 받을 수 있게 name
이라는 프로퍼티를 추가해줬다.
RequestMapping
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
}
위의 설명한 @Controller
클래스, 혹은 클래스 안의 메서드에 붙으며 핸들러 매핑을 할 때 사용된다.
이제 위 애노테이션들이 붙은 클래스를 동적으로 생성해주면 된다.
BeanInfo
public class BeanInfo {
private final String beanName;
private final Class<?> clazz;
public BeanInfo(String beanName, Class<?> clazz) {
this.beanName = beanName;
this.clazz = clazz;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BeanInfo beanInfo = (BeanInfo) o;
return Objects.equals(clazz, beanInfo.clazz);
}
@Override
public int hashCode() {
return Objects.hash(beanName, clazz);
}
}
생성한 Bean
의 키로 사용할 객체이다. 빈 이름과 클래스 리터럴을 이용해서 객체를 구성한다.
키의 비교를 위해 equals
를 오버라이딩 하여 클래스 리터럴이 같은지를 비교하게 구성했다.
BeanFactory
해당 객체에서 리플렉션을 이용해 특정 패키지 내의 애노테이션이 붙은 클래스들을 가져와 싱글톤으로 빈 관리를 해준다.
변수
private static String packagePrefix = "com.project.winter";
private static Reflections reflections;
private static final Map<BeanInfo, Object> beans = new HashMap<>();
private static final List<Object> configurations = new ArrayList<>();
이 객체에서 사용하는 변수들이다.
패키지는 일단 지금은 이 프로젝트를 이용하는 패키지가 확정적이므로 고정값을 두었지만, 추후에 서버가 실행되는 패키지의 하위로 설정되도록 기능을 추가할 예정이다.
초기화
public static void initialize() {
reflections = new Reflections(packagePrefix);
Set<Class<?>> preInstantiatedClazz = getClassTypeAnnotatedWith(Component.class);
createBeansByConfiguration();
createBeansByClass(preInstantiatedClazz);
}
private static Set<Class<?>> getClassTypeAnnotatedWith(Class<? extends Annotation> annotation) {
Set<Class<?>> types = new HashSet<>();
reflections.getTypesAnnotatedWith(annotation).forEach(type -> {
if (!type.isAnnotation() && !type.isInterface()) {
if (type.isAnnotationPresent(Configuration.class)) configurations.add(createInstance(type));
else types.add(type);
}
});
return types;
}
초기화 단계이다. 설정한 패키지 하위에서 @Component
가 붙은 클래스 리터럴을 가져온다.@Component
가 붙은 애노테이션, 인터페이스도 모두 가져와지기 때문에 클래스가 아니면 클래스 리터럴을 반환하지 않게끔 했다.
빈 생성 로직
private static Object createInstance(Class<?> clazz) {
Constructor<?> constructor = findConstructor(clazz);
try {
return constructor.newInstance();
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static Constructor<?> findConstructor(Class<?> clazz) {
Set<Constructor> allConstructors = ReflectionUtils.getAllConstructors(clazz);
Constructor<?> foundConstructor = null;
for (Constructor constructor : allConstructors) {
int parameterCount = constructor.getParameterCount();
if (parameterCount == 0) {
foundConstructor = constructor;
break;
}
}
return foundConstructor;
}
리플렉션을 통해 찾은 클래스 리터럴에서 생성자를 찾고 해당 생성자를 이용해 인스턴스 객체를 생성해주는 로직이다.
DI 와 관련된 기능은 추후에 업데이트 예정이므로 파라미터가 없는 생성자만 반환하게끔 하였다.
@Bean 을 이용한 빈 생성
private static void createBeansByConfiguration() {
configurations.forEach(BeanFactory::createBeanInConfigurationAnnotatedClass);
}
private static void createBeanInConfigurationAnnotatedClass(Object configuration) {
Class<?> subclass = configuration.getClass();
Map<Class<?>, Method> beanMethodNames = BeanFactoryUtils.getBeanAnnotatedMethodInConfiguration(subclass);
beanMethodNames.forEach((clazz, method) -> {
List<Object> parameters = new ArrayList<>();
Arrays.stream(method.getParameters()).forEach(parameter -> {
Class<?> parameterType = parameter.getType();
String parameterName = parameter.getName();
if (isBeanInitialized(parameterName, clazz)) parameters.add(getBean(parameterName, clazz));
else parameters.add(createInstance(parameterType));
});
try {
Object object = method.invoke(configuration, parameters.toArray());
Bean anno = method.getAnnotation(Bean.class);
String beanName = (anno.name().isEmpty()) ? method.getName() : anno.name();
putBean(beanName, clazz, object);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
});
}
스프링과 마찬가지로 @Bean
애노테이션으로 선언한 빈 정보들을 우선적으로 생성하게끔 하였다.
만약 파라미터가 있다면 클래스 생성된 빈들에서 클래스 리터럴이 일치하는 빈을 주입해준다. 만약 생성된 빈이 없다면 빈을 생성해준다.
@Component 를 이용한 빈 생성
private static void createBeansByClass(Set<Class<?>> preInstantiatedClazz) {
for (Class<?> clazz : preInstantiatedClazz) {
if (isBeanInitialized(clazz)) continue;
Object instance = createInstance(clazz);
putBean(clazz.getName(), clazz, instance);
}
}
리플렉션으로 가져온 @Component
애노테이션이 붙은 클래스 리터럴을 이용해 빈을 생성하는 로직이다.
해당 기능을 기반으로 컨트롤러를 생성해 요청을 수행하는 로직을 만들 차례이다.