簡體   English   中英

Spring Boot @ConditionalOnMissingBean 和泛型類型

[英]Spring Boot @ConditionalOnMissingBean and generic types

有沒有辦法讓這兩個bean實例化:

@Bean
@ConditionalOnMissingBean
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingBean
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

@ConditionalOnMissingBean只考慮 bean 類,錯誤地使bookContainercomputerContainer為同一類型,因此只有其中一個被注冊。

我可以為每個指定顯式限定符,但由於這是 Spring Boot Starter 的一部分,因此使用起來會很煩人,因為用戶將被迫以某種方式知道要覆蓋的確切名稱,而不僅僅是類型。

潛在的方法:

由於可以通過完整的泛型類型向 Spring 請求 bean,因此我可能能夠實現一個條件工廠,該工廠將嘗試獲取一個完整類型的實例並在它不存在時生成一個。 我現在正在調查是否/如何做到這一點。 令人驚訝的是,在實現自定義條件(與@Conditional一起使用)時,它無法訪問 bean 類型......

雖然其他所有現代注入框架都在完整類型上運行,但 Spring 仍然以某種方式使用原始類和字符串名稱(!),這讓我大吃一驚......有什么解決方法嗎?

更新: Spring 現在原生支持此功能。 請參閱已接受的答案。

這是一個完全有效的解決方案。 我放棄了這種方法,但我將其發布以防有人發現它有用。

我做了一個自定義注釋:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(MissingGenericBeanCondition.class)
public @interface ConditionalOnMissingGenericBean {

    Class<?> containerType() default Void.class;
    Class<?>[] typeParameters() default {};
}

以及與之配套的自定義條件:

public class MissingGenericBeanCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (!metadata.isAnnotated(ConditionalOnMissingGenericBean.class.getName()) || context.getBeanFactory() == null) {
            return false;
        }
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnMissingGenericBean.class.getName());
        Class<?> containerType = (Class<?>) attributes.get("containerType");
        Class<?>[] typeParameters = (Class<?>[]) attributes.get("typeParameters");

        ResolvableType resolvableType;
        if (Void.class.equals(containerType)) {
            if (!(metadata instanceof MethodMetadata) || !metadata.isAnnotated(Bean.class.getName())) {
                throw error();
            }
            //When resolving beans within the starter
            if (metadata instanceof StandardMethodMetadata) {
                resolvableType = ResolvableType.forType(((StandardMethodMetadata) metadata).getIntrospectedMethod().getGenericReturnType());
            } else {
                //When resolving beans in an application using the starter
                MethodMetadata methodMeta = (MethodMetadata) metadata;
                try {
                    // This might not be a safe thing to do. See the notes below.
                    Class<?> declaringClass = ClassUtils.forName(methodMeta.getDeclaringClassName(), context.getClassLoader());
                    Type returnType = Arrays.stream(declaringClass.getDeclaredMethods())
                            .filter(m -> m.isAnnotationPresent(Bean.class))
                            .filter(m -> m.getName().equals(methodMeta.getMethodName()))
                            .findFirst().map(Method::getGenericReturnType)
                            .orElseThrow(MissingGenericBeanCondition::error);
                    resolvableType = ResolvableType.forType(returnType);
                } catch (ClassNotFoundException e) {
                    throw error();
                }
            }
        } else {
            resolvableType = ResolvableType.forClassWithGenerics(containerType, typeParameters);
        }

        String[] names = context.getBeanFactory().getBeanNamesForType(resolvableType);
        return names.length == 0;
    }

    private static IllegalStateException error() {
        return new IllegalStateException(ConditionalOnMissingGenericBean.class.getSimpleName()
                + " is missing the explicit generic type and the implicit type can not be determined");
    }
}

這些允許我執行以下操作:

@Bean
@ConditionalOnMissingGenericBean
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingGenericBean
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

現在將加載這兩個 bean,因為它們屬於不同的泛型類型。 如果啟動器的用戶將注冊另一個Container<Book>Container<Computer>類型的 bean,則不會完全按照需要加載默認 bean。

在實現時,也可以這樣使用注釋:

@Bean
//Load this bean only if Container<Car> isn't present
@ConditionalOnMissingGenericBean(containerType=Container.class, typeParameters=Car.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

注意:在條件中加載配置類可能不安全,但在這種特定情況下它也可能是......我不知道。 由於我完全采用了不同的方法(每個 bean 的接口不同),因此沒有進一步調查。 如果您有這方面的任何信息,請發表評論。

如果在注釋中提供了顯式類型,例如

@Bean
@ConditionalOnMissingGenericBean(containerType=Container.class, typeParameters=Computer.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

這種擔憂消失了,但是如果類型參數也是通用的,例如Container<List<Computer>> ,這種方法將不起作用。

使這當然安全的另一種方法是接受注釋中的聲明類:

@ConditionalOnMissingGenericBean(declaringClass=CustomConfig.class)

然后用它代替

Class<?> declaringClass = ClassUtils.forName(methodMeta.getDeclaringClassName(), context.getClassLoader());

有什么解決方法嗎?

不幸的是,我認為不是。 問題是 Java 中的泛型不存在於編譯的字節碼中——它們僅用於編譯時的類型檢查,並且只存在於源代碼中。 因此,在運行時,當 Spring Boot 看到您的 bean 配置類時,它只會看到兩個都創建了 Container 的 bean 定義。 它看不出它們之間有任何區別,除非您自己通過為它們提供標識符來提供它。

從 Spring Boot v2.1.0 開始就有可能。

該版本在ConditionalOnMissingBean上引入了新字段parameterizedContainer

@Bean
@ConditionalOnMissingBean(value = Book.class, parameterizedContainer = Container.class)
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingBean(value = Computer.class, parameterizedContainer = Container.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM