quarkus依赖注入之四:选择注入bean的高级手段

quarkus,依赖,注入,选择,bean,高级,手段 · 浏览次数 : 137

小编点评

**第二步:用MyQualifier修饰HelloQualifierA** ```java @MyQualifier("\") public class HelloQualifierA { ... } ``` **第三步:在业务代码的注入点,用MyQualifier修饰HelloQualifier类型的成员变量** ```java @Inject HelloQualifier helloQualifier; ``` **改动完成后的代码:** ```java @MyQualifier("\") public class HelloQualifierA { ... } @Inject HelloQualifier helloQualifier; @Test public void testSelectHelloInstanceA() { Assertions.assertEquals(HelloPriorityC.class.getSimpleName(), helloQualifier.hello()); } ``` **单元测试结果:** ``` HelloPriorityC HelloInstanceA ```

正文

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 本文是《quarkus依赖注入》系列的第四篇,在应用中,一个接口有多个实现是很常见的,那么依赖注入时,如果类型是接口,如何准确选择实现呢?前文介绍了五种注解,用于通过配置项、profile等手段选择注入接口的实现类,面对复杂多变的业务场景,有时候仅靠这两种手段是不够的,最好是有更自由灵活的方式来选择bean,这就是本篇的内容,通过注解、编码等更多方式选择bean
  • 本篇涉及的选择bean的手段有以下四种:
  1. 修饰符匹配
  2. Named注解的属性匹配
  3. 根据优先级选择
  4. 写代码选择

关于修饰符匹配

  • 为了说明修饰符匹配,先来看一个注解Default,其源码如下
@Target({ TYPE, METHOD, PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
@Qualifier
public @interface Default {
    public static final class Literal extends AnnotationLiteral<Default> implements Default {
    	public static final Literal INSTANCE = new Literal();
			private static final long serialVersionUID = 1L;
    }
}
  • Default的源码在这里不重要,关键是它被注解Qualifier修饰了,这种被Qualifier修饰的注解,咱们姑且称之为Qualifier修饰符
  • 如果咱们新建一个注解,也用Qualifier来修饰,如下所示,这个MyQualifier也是个Qualifier修饰符
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MyQualifier {
    @Nonbinding String value();
}
  • 在quarkus容器中的每一个bean都应该有一个Qualifier修饰符在修饰,如下图红框,如果没有,就会被quarkus添加Default注解
    在这里插入图片描述
  • 依赖注入时,直接用Qualifier修饰符修饰注入对象,这样quarkus就会去寻找被这个Qualifier修饰符修饰的bean,找到就注入(找不到报错,找到多个也报错,错误逻辑和之前的一样)
  • 所以用修饰符匹配来选择bean的实现类,一共分三步:
  1. 假设有名为HelloQualifier的接口,有三个实现类:HelloQualifierA、HelloQualifierB、HelloQualifierC,业务需求是使用HelloQualifierA
  2. 第一步:自定义一个注解,假设名为MyQualifier,此注解要被Qualifier修饰
  3. 第二步:用MyQualifier修饰HelloQualifierA
  4. 第三步:在业务代码的注入点,用MyQualifier修饰HelloQualifier类型的成员变量,这样成员变量就会被注入HelloQualifierA实例
  • 仅凭文字描述,很难把信息准确传递给读者(毕竟欣宸文化水平极其有限),还是写代码实现上述场景吧,聪明的您一看就懂

编码演示修饰符匹配:准备工作

  • 先按照前面的假设将接口和实现类准备好,造成一个接口有多个实现bean的事实,然后,再用修饰符匹配来准确选定bean

  • 首先是接口HelloQualifier,如下所示

package com.bolingcavalry.service;

public interface HelloQualifier {
    String hello();
}
  • 实现类HelloQualifierA,返回自己的类名
package com.bolingcavalry.service.impl;

import com.bolingcavalry.service.HelloQualifier;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class HelloQualifierA implements HelloQualifier {
    @Override
    public String hello() {
        return this.getClass().getSimpleName();
    }
}
  • 实现类HelloQualifierB、HelloQualifierC的代码和上面的HelloQualifierA相同,都是返回自己类名,就不贴出来了
  • 关于使用HelloQualifier类型bean的代码,咱们就在单元测试类中注入吧,如下所示:
package com.bolingcavalry;

import com.bolingcavalry.service.HelloQualifier;
import com.bolingcavalry.service.impl.HelloQualifierA;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;

@QuarkusTest
public class QualifierTest {

    @Inject
    HelloQualifier helloQualifier;

    @Test
    public void testQualifier() {
        Assertions.assertEquals(HelloQualifierA.class.getSimpleName(),
                helloQualifier.hello());
    }
}
  • 上面的代码中,成员变量helloQualifier的类型是HelloQualifier,quarkus的bean容器中,HelloQualifierA、HelloQualifierB、HelloQualifierC等三个bean都符合注入要求,此时如果执行单元测试,应该会报错:同一个接口多个实现bean的问题
  • 执行单元测试,如下图,黄框中给出了两个线索:第一,错误原因是注入时发现同一个接口有多个实现bean,第二,这些bean都是用Default修饰的,然后是绿框,里面将所有实现bean列出来,方便开发者定位问题

image-20220320192140670

  • 现在准备工作完成了,来看如何用修饰符匹配解决问题:在注入点准确注入HelloQualifierA类型实例

编码演示修饰符匹配:实现匹配

  • 使用修饰符匹配,继续按照前面总结的三步走
  • 第一步:自定义一个注解,名为MyQualifier,此注解要被Qualifier修饰
package com.bolingcavalry.annonation;

import javax.enterprise.util.Nonbinding;
import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface MyQualifier {
    @Nonbinding String value();
}
  • 第二步:用MyQualifier修饰HelloQualifierA,下图红框是新增的代码
    在这里插入图片描述

  • 第三步:在业务代码的注入点,用MyQualifier修饰HelloQualifier类型的成员变量,下图红框是新增的代码
    在这里插入图片描述

  • 改动完成了,再次执行单元测试,顺利通过

在这里插入图片描述

修饰符匹配要注意的地方

  • 修饰符匹配的逻辑非常简单:bean定义和bean注入的地方用同一个修饰符即可,使用中有三个地方要注意
  1. 在注入bean的地方,如果有了Qualifier修饰符,可以把@Inject省略不写了
  2. 在定义bean的地方,如果没有Qualifier修饰符去修饰bean,quarkus会默认添加Default
  3. 在注入bean的地方,如果没有Qualifier修饰符去修饰bean,quarkus会默认添加Default

关于默认的@Default

  • 回头看刚才的代码,如果保留HelloQualifierA的MyQualifier修饰,但是删除QualifierTest的成员变量helloQualifier的MyQualifier修饰,会发生什么呢?咱们来分析一下:

  • 首先,QualifierTest的成员变量helloQualifier会被quarkus默认添加Default修饰

  • 其次,HelloQualifierB和HelloQualifierC都会被quarkus默认添加Default修饰

  • 所以,注入helloQualifier的时候,quarkus去找Default修饰的bean,结果找到了两个:HelloQualifierB和HelloQualifierC,因此启动会失败

  • 您可以自行验证结果是否和预期一致

  • 看到这里,您应该掌握了修饰符匹配的用法,也应该发现其不便之处:要新增注解,这样下去随着业务发展,注解会越来越多,有没有什么方法来解决这个问题呢?

  • 方法是有的,就是接下来要看的Named注解

Named注解的属性匹配

  • Named注解的功能与前面的Qualifier修饰符是一样的,其特殊之处在于通过注解属性来匹配修饰bean和注入bean

  • 以刚才的业务代码为例来演示Named注解,修改HelloQualifierA,如下图红框,将@MyQualifier("")换成@Named("A"),重点关注Named注解的属性值,这里等于A
    在这里插入图片描述

  • 接下来修改注入处的代码,如下图红框,在注入位置也用@Named("A")来修饰,和bean定义处的一模一样
    在这里插入图片描述

  • 如此,bean定义和bean注入的两个地方,通过Named注解的属性完成了匹配,至于单元测试您可以自行验证,这里就不赘述了

  • 至此,详细您已经知道了Named注解的作用:功能与前面的Qualifier修饰符一样,不过bean的定义和注入处的匹配逻辑是Named注解的属性值

  • 以上就是修饰符匹配的全部内容

根据优先级选择

  • 使用优先级来选择注入是一种简洁的方式,其核心是用AlternativePriority两个注解修饰所有备选bean,然后用Priority的属性值(int型)作为优先级,该值越大代表优先级越高

  • 在注入位置,quarkus会选择优先级最高的bean注入

  • 接下来编码演示

  • 新增演示用的接口HelloPriority.java

public interface HelloPriority {
    String hello();
}
  • HelloPriority的第一个实现类HelloPriorityA.java,注意它的两个注解AlternativePriority,前者表明这是个可供选择的bean,后者表明了它的优先级,数字1001用于和其他bean的优先级比较,数字越大优先级越高
@ApplicationScoped
@Alternative
@Priority(1001)
public class HelloPriorityA implements HelloPriority {
    @Override
    public String hello() {
        return this.getClass().getSimpleName();
    }
}
  • HelloPriority的第二个实现类HelloPriorityB,可见Priority属性值是1002,代表选择的时候优先级比HelloPriorityA更高
@ApplicationScoped
@Alternative
@Priority(1002)
public class HelloPriorityB implements HelloPriority {
    @Override
    public String hello() {
        return this.getClass().getSimpleName();
    }
}
  • HelloPriority的第二个实现类HelloPriorityC,可见Priority属性值是1003,代表选择的时候优先级比HelloPriorityA和HelloPriorityB更高
@ApplicationScoped
@Alternative
@Priority(1003)
public class HelloPriorityC implements HelloPriority {
    @Override
    public String hello() {
        return this.getClass().getSimpleName();
    }
}
  • 接下来是单元测试,验证注入的bean是否符合预期,理论上注入的应该是优先级最高的HelloPriorityC
@QuarkusTest
public class PriorityTest {

    @Inject
    HelloPriority helloPriority;

    @Test
    public void testSelectHelloInstanceA() {
        Assertions.assertEquals(HelloPriorityC.class.getSimpleName(),
                                helloPriority.hello());
    }
}
  • 单元测试结果如下,符合预期

在这里插入图片描述

  • 以上就是优先级选择bean的操作,如果这还不够用,那就祭出最后一招:写代码选择bean

写代码选择bean

  • 如果不用修饰符匹配,再回到最初的问题:有三个bean都实现了同一个接口,应该如何注入?
  • 在注入bean的位置,如果用Instance<T>来接收注入,就可以拿到T类型的所有bean,然后在代码中随心所欲的使用这些bean
  • 新增演示用的接口HelloInstance.java
package com.bolingcavalry.service;

public interface HelloInstance {
    String hello();
}
  • HelloInstance的第一个实现类HelloInstanceA.java
package com.bolingcavalry.service.impl;

import com.bolingcavalry.service.HelloInstance;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class HelloInstanceA implements HelloInstance {
    @Override
    public String hello() {
        return this.getClass().getSimpleName();
    }
}
  • HelloInstance的另外两个实现类HelloInstanceB、HelloInstanceC,代码与HelloInstanceA一样,就不贴出来了
  • 接下来的单元测试类演示了如何使用Instance接受注入,以及业务代码如何使用指定的实现类bean,可见select(Class).get()是关键,select方法指定了实现类,然后get取出该实例
package com.bolingcavalry;

import com.bolingcavalry.service.HelloInstance;
import com.bolingcavalry.service.impl.HelloInstanceA;
import com.bolingcavalry.service.impl.HelloInstanceB;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;

@QuarkusTest
public class InstanceTest {

    @Inject
    Instance<HelloInstance> instance;

    @Test
    public void testSelectHelloInstanceA() {
        Class<HelloInstanceA> clazz = HelloInstanceA.class;

        Assertions.assertEquals(clazz.getSimpleName(),
                instance.select(clazz).get().hello());
    }

    @Test
    public void testSelectHelloInstanceB() {
        Class<HelloInstanceB> clazz = HelloInstanceB.class;

        Assertions.assertEquals(clazz.getSimpleName(),
                instance.select(clazz).get().hello());
    }
}
  • 执行单元测试,顺利通过,符合预期
    在这里插入图片描述

  • 至此,连续两篇关于注入bean的方式全部验证完毕,如此丰富的手段,相信可以满足您日常开发的需要

欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

与quarkus依赖注入之四:选择注入bean的高级手段相似的内容:

quarkus依赖注入之四:选择注入bean的高级手段

除了前文的配置项、profile等手段,在注入点选择bean的时候,还有很多种灵活的选择方式,一起来掌握吧

quarkus依赖注入之五:拦截器(Interceptor)

掌握quarkus框架下如何开发和使用拦截器

quarkus依赖注入之六:发布和消费事件

quarkus框架下,进程内同步、异步发布和消费事件的操作

quarkus依赖注入之七:生命周期回调

介绍quarkus的bean生命周期回调方式,在bean的不同阶段(如创建、销毁)执行自定义方法

quarkus依赖注入之八:装饰器(Decorator)

quarkus框架提供了对装饰器模式的支持,本篇就来体验它的能力和适用场景

quarkus依赖注入之九:bean读写锁

quarkus以bean实例为对象提供读写锁,请随本文来体验此锁如何解决多线程同步问题

quarkus依赖注入之十:学习和改变bean懒加载规则

为了降低启动时间,quarkus下的常规作用域bean遵循懒加载规则,但有时我们希望bean可以更早实例化,本篇,咱们一起来了解懒加载规则和改变规则的方法

quarkus依赖注入之十一:拦截器高级特性上篇(属性设置和重复使用)

体验拦截器的高级功能,设置拦截器属性并在实现中使用此属性,这样同一个拦截器在不同位置可以发布不同效果,更可以将同一个拦截器设置不同属性,叠加使用在同一个地方

quarkus依赖注入之十二:禁用类级别拦截器

通过编码实战了解quarkus拦截器的另一个高级特性:禁用类级别拦截器,这样可以避免类级别和方法级别拦截器的叠加冲突

quarkus依赖注入之十三:其他重要知识点大串讲(终篇)

通过编码实战了解quarkus拦截器的另一个高级特性:禁用类级别拦截器,这样可以避免类级别和方法级别拦截器的叠加冲突