Quarkus-CDI
一、bean注入
Quarkus使用CDI的子集实现依赖注入。java对象不需要手动创建,java对象的生命周期及与其他对象的依赖关系都不需要我们关心。将java对象的创建,对象间的依赖关系交由框架管理。我们只需要关注业务逻辑实现。如:
import javax.inject.Inject;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.metrics.annotation.Counted;
@ApplicationScoped
public class Translator {
@Inject
Dictionary dictionary;
@Counted
String translate(String sentence) {
// ...
}
}
@ApplicationScoped表示将Translator的创建及其他对象依赖交由上下文管理,即管理bean。@ApplicationScoped创建了单例对象,即只有一个对象。@Inject表示Translator依赖Dictionary,自动将Dictionary对象注入Translator中。如果有多个类型呢?有一个简单的规则:必须有一个Bean可以被分配到一个注入点,否则构建就会失败。如果没有一个是可分配的,构建就会以UnsatisfiedResolutionException失败。如果有多个可分配的Bean,则会以AmbiguousResolutionException失败。这非常有用,因为只要容器不能为任何注入点找到明确的依赖关系,你的应用程序就会快速失败。
你可以通过javax.enterprise.inject.Instance使用编程式查找,在运行时解决模糊不清的问题,甚至可以遍历实现特定类型的所有bean。例如:
public interface HelloService {
void sayHello();
}
@Slf4j //lombok注解
@ApplicationScoped
public class HelloGreenServiceImpl implements HelloService {
@Override
public void sayHello() {
log.info("green");
}
}
@Slf4j
@ApplicationScoped
public class HelloRedServiceImpl implements HelloService {
@Override
public void sayHello() {
log.info("red");
}
}
在GreetingResource中使用:
@Inject
Instance<HelloService> helloServices;
将所有类型HelloService且由框架管理的bena注入到helloServices中。Instance实现了迭代器。
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/sayHello")
public void sayHello() {
System.out.println("sayHello");
for (HelloService helloService : helloServices) {
if (helloService.getClass().getSimpleName().contains("Red")) {
helloService.sayHello();
return;
}
}
}
使用时遍历helloServices选取自己想要的HelloService实现类。运行如下:
可以使用setter和构造函数注入。例如:
public interface ConstructService {
void findUserById(Long id);
}
@Slf4j
@ApplicationScoped
public class ConstructServiceImpl implements ConstructService {
private UserDao userDao;
@Override
public void findUserById(Long id) {
log.info("user:{}", userDao.findUserById(id));
}
public ConstructServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
}
@ApplicationScoped
public class UserDao {
private List<User> list = new ArrayList<>();
public UserDao() {
Long id = 1L;
list.add(User.builder().id(id++).name("张三").age(18).build());
list.add(User.builder().id(id++).name("李四").age(20).build());
list.add(User.builder().id(id++).name("王五").age(30).build());
}
public User findUserById(Long id) {
return list.stream().filter(user -> user.getId().equals(id)).findFirst().orElse(null);
}
}
@Data
@Builder
@ToString
public class User implements Serializable {
private Long id;
private String name;
private Integer age;
}
GreetingResource中使用:
@Inject
private ConstructService constructService;
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/findUserById/{id}")
public void findUserById(Long id) {
constructService.findUserById(id);
}
结果如下:
2022-12-11 19:53:44,343 INFO [org.acm.ser.imp.ConstructServiceImpl] (executor-thread-0) user:User(id=1, name=张三, age=18)
这是一个构造函数注入。事实上,这段代码在常规的CDI实现中是行不通的,因为正常作用域的Bean必须始终声明一个无args的构造函数,而且Bean的构造函数必须用@Inject来注释。然而,在Quarkus中,我们会检测到没有no-args构造函数的情况,并在字节码中直接 "添加 "它。如果只有一个构造函数存在,也没有必要添加@Inject。
二、bean限定符
bean 限定符是帮助容器区分实现相同类型的Bean的注解。如果一个Bean拥有所有需要的限定符,它就可以被分配到一个注入点。如果你在一个注入点上没有声明任何限定符,那么就假定@Default限定符。一个限定符类型是一个定义为@Retention(RUNTIME)并带有@javax.inject.Qualifier元注解的Java注解。例如:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Superior {
}
@Slf4j
@Superior
@ApplicationScoped
public class CatService {
public String cry() {
return "喵喵";
}
}
@ApplicationScoped
@Slf4j
public class GarfieldCatService extends CatService{
@Override
public String cry() {
return "加菲猫 喵喵";
}
}
在GreetingResource中:
@Inject
private CatService catService;
@GET
@Produces(MediaType.MEDIA_TYPE_WILDCARD)
@Path("/cry")
public String cry() {
System.out.println("调用cry");
return catService.cry();
}
运行结果是:加菲猫 喵喵。
在GreetingResource将
@Inject
private CatService catService;
改为
@Inject
@Superior
private CatService catService;
运行结果为:喵喵
用@Inject注入bean的时候如果没有bean 限定符则注入默认限定符的bean。如果有bean 限定符则只注入特定bean 限定符的bean。bean 限定符需要自己定义。
三、bean作用域
Bean的作用域决定了其实例的生命周期,也就是说,何时何地应该创建和销毁一个实例。每个bean正好有一个作用域。
可使用的作用域如下:
- @javax.enterprise.context.ApplicationScoped:单例的bean实例被用于应用程序,并在所有的注入点之间共享。这个实例是懒惰地创建的,也就是说,一旦在客户端代理上调用了一个方法,就会创建这个实例。
- @javax.inject.Singleton:就像@ApplicationScoped一样,除了不使用客户端代理。当解析到@Singleton Bean的注入点被注入时,该实例被创建。
- @javax.enterprise.context.RequestScoped:该bean实例与当前请求(通常是HTTP请求)相关联。
- @javax.enterprise.context.Dependent:这是个伪范围。实例是不共享的,每个注入点都会产生一个新的依赖Bean的实例。从属Bean的生命周期与注入它的Bean相联系--它将与注入它的Bean一起被创建和销毁。
- @javax.enterprise.context.SessionScoped:这个作用域是由一个javax.servlet.http.HttpSession对象支持的。它只有在使用quarkus-undertow扩展时才可用。
Singleton和ApplicationScoped区别:
- 一个@Singleton Bean没有客户端代理,因此当bean被注入时,会急切地创建一个实例。相比之下,一个@ApplicationScoped Bean的实例是懒惰地创建的,也就是说,当一个方法第一次在注入的实例上被调用时。
- 此外,客户端代理只委托方法调用,因此你不应该直接读/写一个注入的@ApplicationScoped Bean的字段。你可以安全地读/写一个注入的@Singleton的字段。
- @Singleton应该有更好的性能,因为没有间接关系(没有从上下文委托给当前实例的代理)。
- 另一方面,你不能用QuarkusMock来模拟@Singleton Bean。
- @ApplicationScoped beans也可以在运行时被销毁和重新创建。现有的注入点只是工作,因为注入的代理委托给了当前实例。
因此,我们建议默认坚持使用@ApplicationScoped,除非有很好的理由使用@Singleton。
四、客户端代理
客户端代理基本上是一个将所有方法调用委托给目标Bean实例的对象。它是一个实现io.quarkus.arc.ClientProxy并扩展了bean类的容器结构。客户端代理只委托方法调用。因此,永远不要读或写一个正常范围的Bean的字段,否则你将与非上下文或陈旧的数据一起工作。例子:
@ApplicationScoped
class Translator {
String translate(String sentence) {
// ...
}
}
// The client proxy class is generated and looks like...
class Translator_ClientProxy extends Translator {
String translate(String sentence) {
// Find the correct translator instance...
Translator translator = getTranslatorInstanceFromTheApplicationContext();
// And delegate the method invocation...
return translator.translate(sentence);
}
}
客户端代理允许:
1、懒惰的实例化 - 一旦在代理上调用一个方法,就会创建实例。
2、能够将具有 "较窄 "范围的Bean注入具有 "较宽 "范围的Bean;即你可以将一个@RequestScoped Bean注入一个@ApplicationScoped Bean。
3、解决循环依赖关系。
4、手动销毁Bean。
五、bean的类型
bean有以下类型:
- Class beans
- Producer methods
- Producer fields
- Synthetic beans
Producer methods和Producer fields例子如下:
import javax.enterprise.inject.Produces;
import java.util.ArrayList;
import java.util.List;
public class DemoProducers {
@Produces
double pi = Math.PI;
@Produces
List<String> names() {
List<String> names = new ArrayList<>();
names.add("Andy");
names.add("Adalbert");
names.add("Joachim");
return names;
}
}
@ApplicationScoped
public class Consumer {
@Inject
double pi;
@Inject
List<String> names;
public void printPi() {
System.out.println(pi);
}
public void printNames() {
System.out.println(names);
}
}
GreetingResource导入
@Inject
private Consumer consumer;
后使用:
@GET
@Path("/consumer")
public void comsumer() {
consumer.printPi();
consumer.printNames();
}
@Produces标注在方法上则以方法返回值作为bean。@Produces标注在字段上则以字段作为bean。
六、生命周期回调
一个bean类可以声明生命周期的@PostConstruct和@PreDestroy回调。
@ApplicationScoped
public class Translator {
@PostConstruct
void init() {
System.out.println("初始化bean");
}
@PreDestroy
void destroy() {
System.out.println("销毁bean");
}
public void testLifecycle() {
System.out.println("调用Lifecycle");
}
}
@PostConstruct标注的方法在实例化bean之后使用bean之前调用,可以做初始化工作。@PreDestroy在bean销毁之前调用。
在GreetingResource注入Translator后调用Translator的testLifecycle方法。显示:
初始化bean
调用Lifecycle
关闭项目模拟销毁bean的情况显示:销毁bean
七、拦截器
拦截器被用来将跨领域的关注点与业务逻辑分开。有一个单独的规范--Java拦截器,它定义了基本的编程模型和语义。
例子:
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Inherited
public @interface Logged {
}
@InterceptorBinding表示这是个拦截器绑定。拦截器注解可以标注在类上或方法上。@Inherited表示可继承。
@Logged
@Interceptor
@Priority(100)
public class LoggingInterceptor {
@AroundInvoke
Object logInvocation(InvocationContext context) throws Exception {
System.out.println("执行实际方法之前调用");
Object ret = context.proceed();
System.out.println("执行实际方法之后调用,返回值:" + ret);
return ret;
}
}
拦截器绑定注解是用来将我们的拦截器绑定到Bean上的。简单地用@Logged来注解一个bean类、@Priority启用拦截器并影响拦截器的排序。优先级值较小的拦截器会被首先调用。@Interceptor表示这是个拦截器。AroundInvoke表示一种插手业务方法的方法。proceed是进入拦截器链中的下一个拦截器或调用被拦截的业务方法。拦截器的实例是它们所拦截的Bean实例的依赖对象,也就是说,为每个被拦截的Bean创建一个新的拦截器实例。
@Logged
@ApplicationScoped
public class MyService {
public void doSomething() {
System.out.println("执行实际业务方法");
}
}
@Logged是可继承的。如果MyChildService继承了MyService则MyChildService同样适用拦截器。在GreetingResource中注入MyService后调用doSomething方法。可看到:
执行实际方法之前调用
执行实际业务方法
执行实际方法之后调用,返回值:null
由上可知@Logged拦截器绑定将业务服务与拦截器关联。
@Logged
@Interceptor
@Priority(2)
public class ValidatorInterceptor {
@AroundInvoke
Object proceed(InvocationContext context) throws Exception {
System.out.println("ValidatorInterceptor执行proceed之前");
Object ret = context.proceed();
System.out.println("ValidatorInterceptor执行proceed之后");
return ret;
}
}
新增以上拦截器后重调用MyService.doSomething。可看到:
ValidatorInterceptor执行proceed之前
执行实际方法之前调用
执行实际业务方法
执行实际方法之后调用,返回值:null
ValidatorInterceptor执行proceed之后
八、装饰器
装饰器类似于拦截器,但由于它们实现了具有业务语义的接口,所以能够实现业务逻辑。例子:
public interface Account {
void withdraw(BigDecimal amount);
}
@ApplicationScoped
public class MyAccount implements Account{
@Override
public void withdraw(BigDecimal amount) {
amount = amount.subtract(BigDecimal.valueOf(100));
}
}
@Slf4j
@Decorator
public class LargeTxAccount implements Account{
@Inject
@Delegate
private Account account;
@Override
public void withdraw(BigDecimal amount) {
account.withdraw(amount);
if (amount.compareTo(BigDecimal.ZERO) > 0) {
log.info("金额大于0");
}
}
}
在GreetingResource中注入Account并调用withdraw方法,传入参数200,得到:2022-12-13 21:46:13,905 INFO [org.acm.LargeTxAccount] (executor-thread-0) 金额大于0。可知注入的是装饰后的bean。@Decorator表示是装饰器。
@Delegate表示委托注入点。MyAccount是实际的业务执行服务。
九、事件和观察者
Bean也可以产生和消费事件,以完全解耦的方式进行交互。任何Java对象都可以作为一个事件的有效载荷。可选的限定词充当主题选择器。
public class TaskCompleted {
}
TaskCompleted表示一个事件。
@ApplicationScoped
public class ComplicatedService {
@Inject
Event<TaskCompleted> event;
void doSomething() {
System.out.println("调用doSomething");
event.fire(new TaskCompleted());
}
}
调用Event.fire同步发布一个事件。
@ApplicationScoped
public class Logger {
void onTaskCompleted(@Observes TaskCompleted task) {
System.out.println("监听到TaskCompleted事件");
}
}
@Observes监听TaskCompleted事件。
在GreetingResource注入ComplicatedService并调用doSomething方法得到:
调用doSomething
监听到TaskCompleted事件