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正好有一个作用域。

可使用的作用域如下:

  1. @javax.enterprise.context.ApplicationScoped:单例的bean实例被用于应用程序,并在所有的注入点之间共享。这个实例是懒惰地创建的,也就是说,一旦在客户端代理上调用了一个方法,就会创建这个实例。
  2. @javax.inject.Singleton:就像@ApplicationScoped一样,除了不使用客户端代理。当解析到@Singleton Bean的注入点被注入时,该实例被创建。
  3. @javax.enterprise.context.RequestScoped:该bean实例与当前请求(通常是HTTP请求)相关联。
  4. @javax.enterprise.context.Dependent:这是个伪范围。实例是不共享的,每个注入点都会产生一个新的依赖Bean的实例。从属Bean的生命周期与注入它的Bean相联系--它将与注入它的Bean一起被创建和销毁。
  5. @javax.enterprise.context.SessionScoped:这个作用域是由一个javax.servlet.http.HttpSession对象支持的。它只有在使用quarkus-undertow扩展时才可用。

Singleton和ApplicationScoped区别:

  1. 一个@Singleton Bean没有客户端代理,因此当bean被注入时,会急切地创建一个实例。相比之下,一个@ApplicationScoped Bean的实例是懒惰地创建的,也就是说,当一个方法第一次在注入的实例上被调用时。
  2. 此外,客户端代理只委托方法调用,因此你不应该直接读/写一个注入的@ApplicationScoped Bean的字段。你可以安全地读/写一个注入的@Singleton的字段。
  3. @Singleton应该有更好的性能,因为没有间接关系(没有从上下文委托给当前实例的代理)。
  4. 另一方面,你不能用QuarkusMock来模拟@Singleton Bean。
  5. @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有以下类型:

  1. Class beans
  2. Producer methods
  3. Producer fields
  4. 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事件
posted @ 2022-12-13 22:07  shigp1  阅读(408)  评论(0编辑  收藏  举报