【问题标题】:How to use CDI qualifiers with multiple class implementations?如何将 CDI 限定符与多个类实现一起使用?
【发布时间】:2013-03-05 06:57:40
【问题描述】:

我是 Java EE/JSF 的新手,现在阅读 CDI 限定符 - 更改类实现的可能性。这很好,但我有一个问题。据我了解,我可以使用限定符更改类实现,但我需要在使用此实现的任何地方更改它。在一个地方进行此操作的最佳解决方案是什么?凭借我对 Java EE 的一点了解,我发现了这一点。

假设我们正在创建简单的计算器应用程序。我们需要创建几个类:

  1. Calculator(计算器的基本实现)
  2. ScientificCalculator(计算器的科学实现)
  3. MiniCalculator(潜力最小)
  4. MockCalculator(用于单元测试)
  5. 限定符@Calculator(将指示计算器的实际实现;我应该为每个实现创建限定符吗?)

问题来了。我有四种计算器实现,我想在少数地方使用其中一种,但每次只使用一种(在最初的项目阶段,我将使用MiniCalculator,然后使用Calculator 等等)。如何在注入对象的每个地方更改实现而不更改代码?我应该创建负责注入并以method injector 工作的工厂吗?我的解决方案正确且有意义吗?

工厂

@ApplicationScoped
public class CalculatorFctory implements Serializable {
    private Calculator calc;

    @Produces @Calculator Calculator getCalculator() {
        return new Calculator();
    }
}

使用计算器的类

public class CalculateUserAge {
    @Calculator
    @Inject
    private Calculator calc;
}

这是正确的解决方案吗?如果我错了或者有更好的解决方案,请纠正我。谢谢!。

【问题讨论】:

    标签: jsf jakarta-ee jsf-2 dependency-injection cdi


    【解决方案1】:

    这里有几个问题。

    1. 在整个应用程序中更改所需实现的最佳方法是什么?查看@Alternatives
    2. 每个实现都需要一个限定符吗?不,请参阅this 答案以获得冗长而详细的解释。
    3. 我应该使用生产者来决定注入哪个实现吗?可能是您想要的解决方案,但我对此表示怀疑。生产者通常用于执行某种无法在构造函数/@PostConstruct 中完成的初始化。您还可以使用它来检查注入点并在运行时决定注入什么。有关一些线索,请参阅链接 2。
    4. 这个解决方案正确吗?这会起作用,但是您仍然必须弄乱代码才能更改实现,因此请首先考虑 1.。 @Calculator Calculator 也似乎非常多余。同样,请参见 2 处的链接。

      @ApplicationScoped
      public class CalculatorFctory implements Serializable {
          private Calculator calc;
      
          @Produces @Calculator Calculator getCalculator() {
              return new Calculator();
          }
      }
      

    更新:

    CDI 使用限定符除了 到依赖解析的类型。换句话说,只要只有一种类型与注入点的类型匹配,单独的类型就足够了,不需要限定符。当仅使用类型还不够时,可以使用限定符来消除歧义。

    例如:

    public class ImplOne implements MyInterface {
        ...
    }
    
    public class ImplTwo implements MyInterface {
        ...
    }
    

    为了能够注入任一实现,您不需要任何限定符:

    @Inject ImplOne bean;
    

    @Inject ImplTwo bean;
    

    这就是为什么我说@Calculator Calculator 是多余的。如果您为每个实现定义一个限定符,您不会获得太多,还不如只使用类型。比如说,两个限定符@QualOne@QualTwo

    @Inject @QualOne ImplOne bean;
    

    @Inject @QualTwo ImplTwo bean;
    

    上面的例子没有任何收获,因为在前面的例子中已经不存在消除歧义了。

    当然,在您无权访问特定实现类型的情况下,您可以这样做:

    @Inject @QualOne MyInterface bean; // to inject TypeOne
    

    @Inject @QualTwo MyInterface bean; // to inject TypeTwo
    

    但是,当 OP 希望 Calculator 实现由 CDI 管理时,他不应该使用 @Produces。

    @Avinash Singh - CDI 管理 @Produces 以及它们返回的任何内容,只要调用该方法的是 CDI。请参阅this section of the spec。这包括返回 `@...Scoped beans,它将支持依赖注入、生命周期回调等。

    我在这里忽略了一些细节,所以考虑以下两个:

    public class SomeProducer {
    
        @Inject ImplOne implOne;
        @Inject ImplTwo implTwo;
        @Inject ImplThree implThree;
    
        @Produces
        public MyInterface get() {
            if (conditionOne()) {
                return implOne;
            } else if (conditionTwo()) {
                return implTwo;
            } else {
                return implThree;
            }
        }
    }
    

    public class SomeProducer {
    
        @Produces
        public MyInterface get() {
            if (conditionOne()) {
                return new ImplOne();
            } else if (conditionTwo()) {
                return new ImplTwo();
            } else {
                return new ImplThree;
            }
        }
    }
    

    然后,在第一个示例中,CDI 将管理从生产者返回的内容的生命周期(即@PostConstruct@Inject 支持),但在第二个示例中它不会。

    回到最初的问题 - 在不修改源代码的情况下切换实现的最佳方式是什么?假设您希望更改适用于整个应用程序。

    @Default
    public class ImplOne implements MyInterface {
        ...
    }
    
    @Alternative
    public class ImplTwo implements MyInterface {
        ...
    }
    
    @Alternative
    public class ImplThree implements MyInterface {
        ...
    }
    

    然后,任何@Inject MyInterface instanceImplOne 都会被注入,除非

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
        <alternatives>
            <class>ImplTwo</class>
        </alternatives>
    </beans>
    

    被指定,在这种情况下ImplTwo将被注入到任何地方。

    进一步更新

    Java EE 环境中确实有一些东西不受 CDI 管理,例如 EJB 和 Web 服务。

    如何将 Web 服务注入 CDI 托管 bean?真的很简单:

    @WebServiceRef(lookup="java:app/service/PaymentService")
    PaymentService paymentService;
    

    就是这样,您将获得对在 CDI 外部管理的支付服务的有效引用。

    但是,如果您不想在任何需要的地方使用完整的 @WebServiceRef(lookup="java:app/service/PaymentService") 怎么办?如果你只想按类型注入呢?然后你在某个地方这样做:

    @Produces @WebServiceRef(lookup="java:app/service/PaymentService")
    PaymentService paymentService;
    

    在任何需要引用该支付服务的 CDI bean 中,您可以像这样使用 CDI 简单地 @Inject 它:

    @Inject PaymentService paymentService;
    

    请注意,在定义生产者字段之前,PaymentService 将无法通过 CDI 方式进行注入。但它始终以旧方式可用。此外,在任何一种情况下,Web 服务都不是由 CDI 管理,但定义生产者字段只是使该 Web 服务引用可用于注入 CDI 方式。

    【讨论】:

    • 您似乎将普通注释与 CDI 限定符混淆了。是的,CDI 限定符是注释,但并非所有注释都是 CDI 限定符。要在依赖关系解析过程中被 CDI 考虑,注释必须继承自 docs.oracle.com/javaee/6/api/javax/inject/Qualifier.html。因此,使用 @WebServiceRef 注释生产者方法对 CDI 没有影响,因为它不继承自 Qualifier
    • 因此,就 CDI 而言,@Produces @WebServiceRef(lookup="java:app/service/PaymentService") MyWebServiceImpl get() 等同于 @Produces MyWebServiceImpl get()
    • 如何将此服务注入到 CDI bean 中?我们将使用@Produces @myservice @WebServiceRef(lookup="java:app/service/PaymentService") {} @Inject @myservice MyWebService; 在这种情况下容器不管理 MyWebServiceImpl 生命周期,它只是注入它
    • 感谢您的回答。还有一种情况,假设我们有两个 Webservice 的 PamentService,我们想使用 @Produces @service1 @WebServiceRef(lookup="java:app/service/PaymentService1") PaymentService paymentService;,那么我们需要在变量声明中使用限定符 @service1,否则它会抛出错误,因为它无法解析正确的实现。无论哪种方式,在我们使用注释 @Named 声明 bean 之前,都不会管理此 Web 服务或任何 bean 的生命周期
    • 是的,我认为在这种情况下它会抱怨不满足的依赖关系。这就是为什么我强烈建议不要使用限定符,以防按类型解析就足够了(关于冗余的一点)。这并不是说不应该使用限定词,有很多实例它们非常有用。 :) 至于由 CDI 管理的内容 - 它与命名无关。默认情况下,CDI 会扫描整个部署,并且能够注入它找到的所有有效 bean 类型,请参阅docs.jboss.org/cdi/spec/1.0/html_single/#whatclassesarebeans
    【解决方案2】:

    如果您想使用工厂方法交换代码中的实现,那么您的工厂方法是管理 bean 而不是 CDI,因此实际上不需要@Calculator

        @ApplicationScoped
         public class CalculatorFactory implements Serializable {
         enum CalculatorType{MiniCaculator,ScientificCaculator,MockCalculator};   
         Calculator getCalculator(CalculatorType calctype) {
                    switch(calctype)
                      case MiniCaculator : return new MiniCalculator();
                      case ScientificCalculator : new ScientificCalculator();
                      case MockCalculator : new MockCalculator();
                      default:return null;
                }
            }
    public class CalculatorScientificImpl {       
        private Calculator calc    =  
              CalculatorFactory.getCaclulator(CaclutorType.ScientificCalculator);
        doStuff(){}
    }
    
    public class CalculatorTest {       
        private Calculator calc    =
                   CalculatorFactory.getCaclulator(CaclutorType.MockCalculator);
        doStuff(){}
    }
    

    但是,如果您希望 Caclulator bean 由 CDI 管理以使用 @PostConstruct 等进行注入和生命周期管理,那么您可以使用以下方法之一。 p>

    方法一:

    优点:可以避免使用@Named("miniCalculator")创建注解

    缺点:如果名称从 miniCalculator 更改为 xyzCalculator,编译器不会给出错误。

    @Named("miniCalculator")
    class MiniCalculator implements Calculator{ ... }
    
    @ApplicationScoped
    public class CalculatorFactory implements Serializable {
        private calc;
    
        @Inject 
        void setCalculator(@Named("miniCalculator") Caclulator calc) {
            this.calc = calc;
        }
    }
    

    方法 2:推荐如果任何注入失败,编译器会跟踪注入

    @Qualifier
    @Retention(RUNTIME)
    @Target({FIELD, TYPE, METHOD})
    public @interface MiniCalculator{
    }
    
    @ApplicationScoped
    public class CalculatorFactory implements Serializable {
        private calc;
    
        @Inject 
        void setCalculator(@MiniCalculator calc) {
            this.calc = calc;
        }
    }
    

    方法 3: 如果您使用工厂方法生成对象。它的生命周期不会由 CDI 管理,但使用 @Inject 注入将正常工作。

    @ApplicationScoped
    public class CalculatorFactory implements Serializable {
        private Calculator calc;    
        @Produces Calculator getCalculator() {
            return new Calculator();
        }
    }    
    public class CalculateUserAge {
        @Inject
        private Calculator calc;
    }
    

    这三种方法都适用于测试,假设您有一个名为 CaculatorTest 的类,

    class ScientificCalculatorTest{        
        Caclulator scientificCalculator;        
        @Inject 
        private void setScientificCalculator(@ScientificCalculator calc) {
                    this.scientificCalculator = calc;
                }        
        @Test
        public void testScientificAddition(int a,int b){
          scientificCalculator.add(a,b);
          ....
        } 
        }
    

    如果您想在测试中使用模拟实现,请执行以下操作,

       class CalculatorTest{        
            Caclulator calc;        
            @PostConstruct 
                    init() {
                        this.calc = createMockCaclulator();
                    }
            @Test
            public void testAddition(int a,int b){
              calc.add(a,b);
              .....
            }
            }
    

    【讨论】:

    • 请根据您的评论查看我的更新。此外,生产者和他们返回的任何东西都由 CDI 管理......
    • 如果您要自己调用 @Produces,则没有理由注释任何内容。
    • 如何将此服务注入到 CDI bean 中?我们将使用@Produces @myservice @WebServiceRef(lookup="java:app/service/PaymentService") {} @Inject @myservice MyWebService; 在这种情况下容器不管理 MyWebServiceImpl 生命周期,它只是注入它
    • @Produces bean 仍由 CDI 管理,除非在方法中将其实例化为 new()。我已经更正了我对何时使用方法 3 的定义。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-13
    • 1970-01-01
    • 2017-04-30
    • 1970-01-01
    • 2021-06-25
    • 1970-01-01
    相关资源
    最近更新 更多