ClassPathScanner.java

package sprout.scan;

import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;
import sprout.aop.annotation.Aspect;
import sprout.beans.BeanDefinition;
import sprout.beans.ConstructorBeanDefinition;
import sprout.beans.MethodBeanDefinition;
import sprout.beans.annotation.*;
import sprout.context.ApplicationContext;
import sprout.context.BeanDefinitionRegistry;
import sprout.context.BeanFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;

@Component
public class ClassPathScanner {

    public Collection<BeanDefinition> scan(ConfigurationBuilder configBuilder, Class<? extends Annotation>... componentAnnotations) {
        configBuilder.addScanners(Scanners.TypesAnnotated, Scanners.SubTypes);
        Reflections r = new Reflections(configBuilder);

        // @Component, @Service 등 어노테이션 기반의 빈 타입 탐색
        Set<Class<?>> componentCandidates = new HashSet<>();
        for (Class<? extends Annotation> anno : componentAnnotations) {
            componentCandidates.addAll(r.getTypesAnnotatedWith(anno));
        }

        Set<Class<?>> concreteComponentTypes = componentCandidates.stream()
                .filter(clazz -> !clazz.isInterface() && !clazz.isAnnotation() && !Modifier.isAbstract(clazz.getModifiers()))
                .collect(Collectors.toSet());

        // @Bean 메서드 기반의 빈 타입 탐색
        Set<Class<?>> beanMethodReturnTypes = new HashSet<>();
        Set<Class<?>> configClasses = r.getTypesAnnotatedWith(Configuration.class);
        for (Class<?> configClass : configClasses) {
            for (Method method : configClass.getDeclaredMethods()) {
                if (method.isAnnotationPresent(Bean.class)) {
                    beanMethodReturnTypes.add(method.getReturnType());
                }
            }
        }

        // DI 컨테이너가 알게 될 모든 빈의 타입을 통합
        Set<Class<?>> allKnownBeanTypes = new HashSet<>();
        allKnownBeanTypes.addAll(concreteComponentTypes);
        allKnownBeanTypes.addAll(beanMethodReturnTypes);

        List<BeanDefinition> definitions = new ArrayList<>();

        // @Component 등 일반 빈 처리
        for (Class<?> clazz : concreteComponentTypes) {
            try {
                Constructor<?> ctor = resolveUsableConstructor(clazz, allKnownBeanTypes);
                ConstructorBeanDefinition def = new ConstructorBeanDefinition(
                        generateBeanName(clazz),
                        clazz,
                        ctor,
                        ctor.getParameterTypes()
                );
                if (clazz.isAnnotationPresent(Configuration.class)) {
                    def.setConfigurationClassProxyNeeded(clazz.getAnnotation(Configuration.class).proxyBeanMethods());
                }
                if (clazz.isAnnotationPresent(Primary.class)) {
                    def.setPrimary(true);
                }

                definitions.add(def);
            } catch (NoSuchMethodException e) {
                throw new IllegalStateException("No usable constructor for class " + clazz.getName(), e);
            }
        }

        // @Bean 메서드 처리
        for (Class<?> configClass : configClasses) {
            for (Method method : configClass.getDeclaredMethods()) {
                if (method.isAnnotationPresent(Bean.class)) {
                    if (Modifier.isStatic(method.getModifiers()) || method.getReturnType() == void.class) continue;

                    MethodBeanDefinition def = new MethodBeanDefinition(
                            generateBeanName(method),
                            method.getReturnType(),
                            method,
                            generateBeanName(configClass),
                            method.getParameterTypes()
                    );

                    if (method.isAnnotationPresent(Primary.class)) {
                        def.setPrimary(true);
                    }

                    definitions.add(def);
                }
            }
        }

        // 디버깅 출력
        definitions.forEach(d -> System.out.println("→ BeanDefinition: Name=" + d.getName() + ", Type=" + d.getType().getSimpleName()));

        return definitions;
    }

    // 가장 많은 인자를 가지는 해결 가능한 생성자를 선호 (Spring의 기본 전략)
    private Constructor<?> resolveUsableConstructor(Class<?> clazz, Set<Class<?>> allKnownBeanTypes) throws NoSuchMethodException {
        return Arrays.stream(clazz.getDeclaredConstructors())
                .filter(constructor -> Arrays.stream(constructor.getParameterTypes())
                        .allMatch(param -> isResolvable(param, allKnownBeanTypes))) // 수정된 allKnownBeanTypes 사용
                .max(Comparator.comparingInt(Constructor::getParameterCount))
                .orElseThrow(() -> new NoSuchMethodException("No usable constructor for " + clazz.getName() + " with known types."));
    }

    private boolean isResolvable(Class<?> paramType, Set<Class<?>> allKnownBeanTypes) {
        if (List.class.isAssignableFrom(paramType)) {
            return true; // List 주입은 항상 가능하다고 가정
        }

        if (isKnownInfrastructureType(paramType)) {
            return true;
        }

        // allKnownBeanTypes에 파라미터 타입과 일치하거나, 해당 타입을 구현/상속하는 타입이 있는지 확인
        return allKnownBeanTypes.stream().anyMatch(knownType -> paramType.isAssignableFrom(knownType));
    }

    private boolean isKnownInfrastructureType(Class<?> paramType) {
        // ApplicationContext 외에 BeanFactory 등 다른 핵심 인터페이스도 추가할 수 있습니다.
        return ApplicationContext.class.isAssignableFrom(paramType) || BeanFactory.class.isAssignableFrom(paramType) || BeanDefinitionRegistry.class.isAssignableFrom(paramType);
    }

    private String generateBeanName(Class<?> clazz) {
        String className = clazz.getSimpleName();
        if (className.length() > 0) {
            return Character.toLowerCase(className.charAt(0)) + className.substring(1);
        }
        return className;
    }


    private String generateBeanName(Method method) {
        Bean beanAnno = method.getAnnotation(Bean.class);
        if (beanAnno != null && !beanAnno.value().isEmpty()) {
            return beanAnno.value();
        }
        return method.getName();
    }
}