3.3 Bean的使用
根据CDI Bean规范,代码中可以有不同的方式来使用Bean。下面对Bean相关的使用机制进行介绍。
3.3.1 使用修饰符区分相同类型的Bean
对于每个注解@Inject声明的注入点,容器负责提供与之匹配的对象实例。在进行匹配时,容器需要考虑注入点的Bean类型和修饰符。可以被注入的Bean需要同时满足两个条件,首先是Bean具有注入点的类型,其次是Bean具有注入点全部的修饰符。
当容器在解析注入点时,会出现两种错误的情况。第一种错误是找不到满足条件的Bean,另外一种错误是存在多个Bean满足条件。对于第二种错误,可以使用修饰符来做进一步的区分。
每个Bean都有一个内置的修饰符@Any。如果Bean除了@Named和@Any之外没有其他修饰符,那么这个Bean具有另外一个修饰符@Default。如果一个注入点没有声明修饰符,那么等同于使用了@Default。
下面代码中的DbUserRepository接口有两个实现类,分别是LocalDbUserRepository和Remo-teUserDbRepository。
下面代码中UserService的对象实例需要注入类型为DbUserRepository的Bean的对象实例。LocalDbUserRepository和RemoteUserDbRepository两个Bean都满足注入点的类型要求。由于注入点没有声明修饰符,注入点的实际修饰符是@Default。两个Bean都同时满足注入点的类型和修饰符要求,容器无法进行选择,会在运行时产生错误。
为了解决这个问题,可以创建自定义的修饰符。自定义的修饰符是新的注解类型,并使用元注解@Qualifier来标注。下面代码中的注解@Local是自定义的修饰符。
把修饰符@Local添加到LocalDbUserRepository类上之后,LocalDbUserRepository无法满足UserService中的注入点的条件。因为LocalDbUserRepository的修饰符变成了@Local,而注入点的修饰符仍然是默认的@Default。容器会使用RemoteUserDbRepository的对象实例来作为依赖注入的选择。
如果希望使用LocalDbUserRepository,则需要在注入点添加@Local修饰符,如下面的代码所示。
除了创建自定义的修饰符注解之外,另外一种更简单的区分类型相同的不同Bean的方式是使用注解@Named。@Named是一种特殊的修饰符,可以为Bean添加名称。
下面代码中的LocalDbUserRepository类上添加了注解@Named("local"),指定了Bean的名称local。LocalUserService的注入点使用同样的注解来进行选择。
@Named的好处是使用简单,不需要创建新的注解。不足之处在于@Named使用字符串来进行区分,并不是类型安全的,在修改名称时容易出错。推荐的做法是使用类型安全的自定义修饰符。
3.3.2 使用生产方法和字段创建Bean
之前介绍的Bean实例都是通过构造器的方式由容器来创建。有些情况下,Bean实例的创建方式会比较复杂,比如实例的类型在运行时才能确定,或者实例创建时需要额外的初始化逻辑。对于这样的情况,可以使用生产方法。生产方法使用注解@Produces。容器负责调用生产方法来创建对象实例。
在下面的代码中,persistenceService是创建PersistenceService类型的对象实例的生产方法。生产方法上同样可以使用CDI注解来声明所创建对象的作用域和修饰符等。
生产方法的每个参数都是注入点,不需要显式地添加注解@Inject。在下面的代码中,在调用生产方法readSecrectKey时,容器会提供类型为SecretKeyDecoder的参数decoder的对象实例。
除了生产方法之外,还可以使用生产字段来声明对象实例。下面代码中的字段admin声明一个类型为User的对象。
Quarkus的CDI实现对生产方法的声明进行了简化。如果生产方法上添加了声明作用域的注解,那么可以省略注解@Produces。
与生产方法和字段对应的是销毁方法。如果生产方法或字段创建的对象实例需要添加自定义的销毁逻辑,可以添加对应的销毁方法。
下面代码中的SharedResource表示共享的资源,其close方法用来释放资源。
在下面的代码中,create方法是创建SharedResource类型的对象实例的生产方法,对应的作用域是@RequestScoped。而close方法是create对应的销毁方法。销毁方法只能有一个参数,该参数的类型是生产方法创建的实例的类型,并且添加注解@Disposes。
对于一个新的请求,create方法会被调用来创建SharedResource类型的对象实例。当该请求结束时,close方法会被调用来销毁之前创建的SharedResource对象实例。
3.3.3 使用默认Bean和替代Bean
默认Bean在作用上类似于Spring Boot提供的自动配置功能。默认Bean为特定的Bean类型提供了默认实现。当容器在解析特定Bean类型时,如果找不到其他自定义的Bean实现,会使用默认Bean的实现。默认Bean在框架的使用场景比较多。框架可以对很多类型的Bean提供默认实现,并允许应用的代码根据需要进行替换。
下面代码中的接口ErrorHandler表示对错误的处理逻辑。
下面代码中的ConsoleErrorHandler是ErrorHandler的实现,负责输出错误信息到控制台。Con-soleErrorHandler上的注解@DefaultBean声明了它是一个默认Bean。如果没有其他的ErrorHandler的实现,那么ConsoleErrorHandler会作为Bean类型ErrorHandler的实现。
如果添加了下面代码中给出的另外一个ErrorHandler的实现LoggingErrorHandler,那么默认Bean会被忽略,而实际使用的是LoggingErrorHandler类的对象实例。
替代Bean是添加了注解@javax.enterprise.inject.Alternative的特殊Bean。替代Bean的特殊之处在于容器不会自动地把它们作为查找和依赖注入的候选,而是需要显式地选择。选择的方式是使用注解@Priority来添加优先级。
下面代码中的MockUserService是UserService类型的替代Bean。@Priority(1)表明了替代Bean的优先级。
在容器解析Bean的过程中,如果出现了多个匹配的Bean实现,非替代Bean的实现首先被移出考虑的范围,然后在替代Bean中选择优先级最高的作为匹配的Bean。在上面的例子中,Bean类型UserService有两个实现,正常的实现DefaultUserService和作为替代Bean的MockUse-rService。容器在解析UserService类型时,发现了两个满足条件的Bean。非替代Bean的Default-UserService首先被排除,只留下一个替代Bean,因此被选中作为Bean实现。
替代Bean的一个重要作用是在测试中替代正常的Bean。在第5章介绍单元测试时会进行详细介绍。
3.3.4 在代码中选择Bean实例
有些情况下,使用注解@Inject并不能满足依赖注入的需求。比如,Bean的类型或修饰符在运行时才能确定,或者需要遍历某个Bean类型的全部实现。在这种情况下,可以注入javax.enterprise.inject.Instance类型的对象。Instance提供了select方法来根据子类型或修饰符来进行选择。
下面代码中的TextTransformService接口用来对字符串进行转换,其中定义了两个操作,分别是把字符串转换成大写形式和小写形式。枚举类型Action表示操作类型。
与两个操作对应的是两个TextTransformService接口的实现。下面代码中的UpperCaseTrans-formService是转换为大写形式的实现类。
下面代码中的TextTransformResource是一个进行字符串转换的REST API的实现。注入的Instance对象代表所有类型为TextTransformService的Bean。在textTransform方法中,根据请求中的操作名称来确定TextTransformService的具体实现类的名称。使用Instance.select方法选择子类型之后,再使用get方法得到实际的对象实例,最后调用transform方法完成转换。
由于Instance接口继承自Iterable,可以通过遍历的方式来查看全部的Bean实现。对于上面的字符串转换的例子,可以换一种实现方式。在TextTransformService接口中新增的getAction方法返回对应的操作。通过Instance的stream方法可以得到包含全部TextTransformService实例的Stream对象,再根据请求中的操作来过滤流中的实例,最后使用找到的TextTransformService对象实例来完成转换,如下面的代码所示。
在进行依赖注入时,被注入的对象和注入的目标都需要由CDI容器来创建。如果使用CDI Bean的对象不由容器来管理,那么可以直接从容器中获取。类javax.enterprise.inject.spi.CDI的current方法可以获取到当前的CDI容器。CDI继承自Instance,可以使用select方法来选择特定类型的Bean。在下面的代码中,OrderFactory类上没有作用域相关的声明,不由容器来创建。OrderFactory直接访问CDI容器来获取OrderService的对象实例。