HackToTech

Hack To Technology

SpringBootでAutowireする際に複数のBeanがマッチした際の挙動をみたのでメモ

雰囲気で挙動を理解していたのをちゃんと見たのでメモ

spring.pleiades.io

@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
        @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
        ...
        // Step 3b: direct bean matches, possibly direct beans of type Collection / Map
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor); // ここで型に対して一致するbeanの一覧を取得する
        ...

        // Step 4: determine single candidate
        if (matchingBeans.size() > 1) {
            autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); // 複数のbeanが当てはまる場合はどれを使うかを決定する
            if (autowiredBeanName == null) {
                if (isRequired(descriptor) || !indicatesArrayCollectionOrMap(type)) {
                    // Raise exception if no clear match found for required injection point
                    return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
                }
                else {
                    // In case of an optional Collection/Map, silently ignore a non-unique case:
                    // possibly it was meant to be an empty collection of multiple regular beans
                    // (before 4.3 in particular when we didn't even look for collection beans).
                    return null;
                }
            }
            instanceCandidate = matchingBeans.get(autowiredBeanName);
        }
        ...
}
protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
    Class<?> requiredType = descriptor.getDependencyType();
    String primaryCandidate = determinePrimaryCandidate(candidates, requiredType); // @Primaryがないかを優先して探す
    if (primaryCandidate != null) {
        return primaryCandidate; // 見つかったら返す
    }
    String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType); // @Priorityで指定した中で最も優先度の高い(値の低い)ものを探す
    if (priorityCandidate != null) {
        return priorityCandidate; // 見つかったら返す
    }
    // Fallback: pick directly registered dependency or qualified bean name match
    for (Map.Entry<String, Object> entry : candidates.entrySet()) {
        String candidateName = entry.getKey();
        Object beanInstance = entry.getValue();
        if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
                matchesBeanName(candidateName, descriptor.getDependencyName())) {
            return candidateName; // Autowireする際の変数名が一致するものが見つかったら返す
        }
    }
    return null;
}

@Priority でInjectするbeanを操作しようとした際に、↓は一見 @Priority が評価されそうに見えるが、
実際は評価されず exampleAexampleB どちらを使えば良いかわからずに終了する

import jakarta.annotation.Priority
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered


interface MarkerExample
data class ExampleImpl1(val value: String): MarkerExample
data class ExampleImpl2(val value: String): MarkerExample

@Configuration
class ExampleConfig {
    @Bean
    @Priority(Ordered.LOWEST_PRECEDENCE)
    fun exampleA(): MarkerExample {
        return ExampleImpl1(
            value = "ExampleA",
        )
    }

    @Bean
    @Priority(Ordered.HIGHEST_PRECEDENCE)
    fun exampleB(): MarkerExample {
        return ExampleImpl2(
            value = "ExampleB",
        )
    }

    @Bean
    fun exampleC(example: MarkerExample): String {
        return "ExampleC"
    }
}

逆に↓は動く(Bで解決される)

import jakarta.annotation.Priority
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered


interface MarkerExample
@Priority(Ordered.LOWEST_PRECEDENCE)
data class ExampleImpl1(val value: String): MarkerExample
@Priority(Ordered.HIGHEST_PRECEDENCE)
data class ExampleImpl2(val value: String): MarkerExample

@Configuration
class ExampleConfig {
    @Bean
    fun exampleA(): MarkerExample {
        return ExampleImpl1(
            value = "ExampleA",
        )
    }

    @Bean
    fun exampleB(): MarkerExample {
        return ExampleImpl2(
            value = "ExampleB",
        )
    }

    @Bean
    fun exampleC(example: MarkerExample): String {
        return "ExampleC"
    }
}

こういったパターンはほぼ @Primary があれば足りるし使うことはないが、
思っていた挙動と違ったので意外だった