JAVA CDI 学习(4) - @Alternative/@Default/@Any & Extension

时间:2022-04-24
本文章向大家介绍JAVA CDI 学习(4) - @Alternative/@Default/@Any & Extension,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前面几节学习到的CDI内容,基本上都是hard-code,以硬编码的方式在代码里指定注入类型,这并非依赖注入的本意,依赖注入的优势之一在于“解耦”,这一节我们将学习如何利用配置来动态注入的类型及属性初始化。

一、@Alternative/@Default/@Any

当一个服务接口(也称契约)有多个实现时,可以在代码里指定一个缺省的实现类型(即:标注成@Default或@Any),其它实现类标注成@Alternative,以后如果需要动态切换实现类,只要在webapp/WEB-INF/beans.xml中配置即可。

1.1 新建二个示例接口

1 package contract;
2 
3 public interface Connection {
4 
5     String connect();
6 
7 }

该接口模拟db连接,里面有一个connect方法,用来连接db.

1 package contract;
2 
3 public interface DriveService {
4     
5     String drive();
6 
7 }

该接口模拟游戏应用中,有些人物具有驾驶技能。

1.2 提供接口实现

假设Connection有二个实现,一个用来连接到Oracle Database,另一个用来连接Microsoft Sql Server

 1 package contract.impl;
 2 
 3 import javax.enterprise.inject.Default;
 4 
 5 import contract.Connection;
 6 
 7 @Default
 8 public class OracleConnection implements Connection {
 9 
10     @Override
11     public String connect() {
12 
13         return "Oracle Database is connecting...";
14     }
15 
16 }
 1 package contract.impl;
 2 
 3 import javax.enterprise.inject.Alternative;
 4 
 5 import contract.Connection;
 6 
 7 @Alternative
 8 public class SqlServerConnection implements Connection {
 9 
10     @Override
11     public String connect() {
12 
13         return "Microsoft SqlServer is connecting...";
14     }
15 
16 }

注:OracleConnection上应用了注解@Default,表示这是接口Connection的默认实现类(@Default实质上是系统的默认注解,其实也可以省略,系统会自动默认为@Default);SqlServerConnection上应用了注解@Alternative,表示它是候选项,俗称:备胎:),所有非@Default的实现类,都必须标识@Alternative,否则注入时,会提示“不明确的类型”

再来看DriveService的实现,我们提供三种实现:驾驶汽车、摩托车、拖拉机

 1 package contract.impl;
 2 
 3 import contract.DriveService;
 4 
 5 public class CarDriveImpl implements DriveService {
 6 
 7     @Override
 8     public String drive() {
 9         String msg = "Drive a car...";
10         System.out.println(msg);
11         return msg;
12     }
13 
14 }
 1 package contract.impl;
 2 
 3 import javax.enterprise.inject.Alternative;
 4 
 5 import contract.DriveService;
 6 
 7 @Alternative
 8 public class MotorcycleDriveImpl implements DriveService {
 9 
10     @Override
11     public String drive() {
12         String msg = "Drive a motocycle...";
13         System.out.println(msg);
14         return msg;
15     }
16 
17 }
 1 package contract.impl;
 2 
 3 import javax.enterprise.inject.Alternative;
 4 
 5 import contract.DriveService;
 6 
 7 @Alternative
 8 public class TractorDriveImpl implements DriveService {
 9 
10     @Override
11     public String drive() {
12         String msg = "Drive a tractor...";
13         System.out.println(msg);
14         return msg;
15     }
16 
17 }

注:MotocycleDriveImpl、TractorDriveImpl这二个类使用了@Alternative,即它们俩是候选,剩下的CarDriveImpl上未使用任何注解,即默认的@Default

1.3 编写Controller类

 1 package controller;
 2 
 3 import javax.inject.Inject;
 4 import javax.inject.Named;
 5 
 6 import contract.Connection;
 7 
 8 @Named("Conn")
 9 public class ConnectionController {
10 
11     @Inject
12     private Connection conn;
13 
14     public Connection getConn() {
15         return conn;
16     }
17 
18 }
 1 package controller;
 2 
 3 import javax.enterprise.inject.*;
 4 import javax.inject.Inject;
 5 import javax.inject.Named;
 6 
 7 import contract.DriveService;
 8 
 9 @Named("Drive")
10 public class DriveController {
11 
12     @Inject
13     private DriveService driveService;
14 
15     public DriveService getDriveService() {
16         return driveService;
17     }
18 
19     @Inject
20     @Any
21     private Instance<DriveService> anySerInstance;
22 
23     public DriveService getAnySerInstance() {
24         return anySerInstance.get();
25     }
26 
27 }

注:DriveController中anySerInstance成员上使用了@Any,从本例最终使用的效果上看,它跟@Default一样,只不过细节要留意一下,需要使用Instance<T>接口,这点跟@Default有点不同。

1.4 UI层

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
 2 <html xmlns="http://www.w3.org/1999/xhtml"
 3       xmlns:h="http://java.sun.com/jsf/html"
 4       xmlns:f="http://java.sun.com/jsf/core"
 5       xmlns:ui="http://java.sun.com/jsf/facelets"> 
 6 
 7 <h:head>
 8     <title>CDI - Alternative/Default/Any</title>
 9 </h:head> 
10 <body> 
11     #{Drive.driveService.drive()}
12     <br />
13     <br />#{Drive.anySerInstance.drive()}
14     <br />
15     <br /> #{Conn.conn.connect()}
16 </body> 
17 </html>

运行结果:

修改beans.xml的内容如下:

1 <?xml version="1.0" encoding="UTF-8"?>
2 
3 <beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4     xsi:schemaLocation="         http://java.sun.com/xml/ns/javaee          http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
5     <alternatives>
6         <class>contract.impl.SqlServerConnection</class>
7         <class>contract.impl.TractorDriveImpl</class>
8     </alternatives>
9 </beans>

重新在Jboss里部署、运行,结果如下:

在不修改java源代码的前提下,仅通过配置文件beans.xml的修改,就动态切换了接口的实现类。

二、Extension

不仅注入的类型可以由配置文件来动态切换,也可以由配置文件来直接初始化注入对象的属性值(虽然我个人认为这种场景在实际开发中其实并不多见)

2.1 先来定义几个类:

BaseDto.java

 1 package dto;
 2 
 3 import java.io.Serializable;
 4 
 5 public class BaseDto implements Serializable {
 6 
 7     private static final long serialVersionUID = 804047416541420712L;
 8 
 9     public BaseDto() {
10         System.out.println("BaseDto's constructor is called...");
11 
12     }
13 
14 }
 1 package dto;
 2 
 3 @DtoType(ProductType.Product)
 4 public class Product extends BaseDto {
 5 
 6     public Product() {
 7         System.out.println("Product's constructor is called...");
 8 
 9     }
10 
11     private static final long serialVersionUID = 7364741422914624828L;
12     private String productNo;
13     private String productName;
14 
15     public String getProductName() {
16         return productName;
17     }
18 
19     public void setProductName(String productName) {
20         this.productName = productName;
21     }
22 
23     public String getProductNo() {
24         return productNo;
25     }
26 
27     public void setProductNo(String productNo) {
28         this.productNo = productNo;
29     }
30 
31     @Override
32     public String toString() {
33         return "productNo:" + productNo + " , productName: " + productName
34                 + " , serialVersionUID:" + serialVersionUID;
35     }
36 }
 1 package dto;
 2 
 3 //@DtoType(ProductType.Computer)
 4 public class Computer extends Product {
 5 
 6     public Computer() {
 7         System.out.println("Computer's constructor is called...");
 8     }
 9 
10     private static final long serialVersionUID = -5323881568748028893L;
11 
12     private String cpuType;
13 
14     private int hardDiskCapacity;
15 
16     public String getCpuType() {
17         return cpuType;
18     }
19 
20     public void setCpuType(String cpuType) {
21         this.cpuType = cpuType;
22     }
23 
24     public int getHardDiskCapacity() {
25         return hardDiskCapacity;
26     }
27 
28     public void setHardDiskCapacity(int hardDiskCapacity) {
29         this.hardDiskCapacity = hardDiskCapacity;
30     }
31 
32     @Override
33     public String toString() {
34         return "productNo:" + getProductNo() + " , productName: "
35                 + getProductName() + " , cpuType:" + getCpuType()
36                 + " , hardDiskCapacity: " + getHardDiskCapacity()
37                 + " , serialVersionUID:" + serialVersionUID;
38     }
39 }
 1 package dto;
 2 
 3 //@DtoType(ProductType.Cloth)
 4 public class Cloth extends Product {
 5 
 6     private static final long serialVersionUID = -8799705022666106476L;
 7     private String brand;
 8 
 9     public String getBrand() {
10         return brand;
11     }
12 
13     public void setBrand(String brand) {
14         this.brand = brand;
15     }
16 
17     @Override
18     public String toString() {
19         return "productNo:" + getProductNo() + " , productName: "
20                 + getProductName() + " , brand:" + getBrand()
21                 + " , serialVersionUID:" + serialVersionUID;
22     }
23 
24 }

Product上使用了一个自定义的注解:@DtoType

 1 package dto;
 2 
 3 import javax.inject.Qualifier;
 4 import java.lang.annotation.Retention;
 5 import java.lang.annotation.RetentionPolicy;
 6 
 7 @Qualifier
 8 @Retention(RetentionPolicy.RUNTIME)
 9 public @interface DtoType {
10 
11     public ProductType value();
12 
13 }

以及枚举:

1 package dto;
2 
3 public enum ProductType {
4     Product,Computer,Cloth
5 }

2.2 BaseDtoExtension

为了实现注入配置化,我们还需要对BaseDto写一个扩展类:

 1 package dto.extension;
 2 
 3 import java.io.IOException;
 4 import java.io.InputStream;
 5 import java.util.logging.Logger;
 6 
 7 import javax.enterprise.event.Observes;
 8 
 9 import javax.enterprise.inject.spi.*;
10 import javax.xml.parsers.*;
11 
12 import dto.*;
13 import org.w3c.dom.*;
14 
15 import org.xml.sax.SAXException;
16 
17 
18 public class BaseDtoExtension implements Extension {
19     private final Document document;
20     private final Logger log = Logger.getLogger(BaseDtoExtension.class
21             .getName());
22 
23     public BaseDtoExtension() {
24         try {
25             InputStream creatureDefs = BaseDtoExtension.class.getClassLoader()
26                     .getResourceAsStream("inject-beans.xml");
27             DocumentBuilderFactory factory = DocumentBuilderFactory
28                     .newInstance();
29             DocumentBuilder builder = factory.newDocumentBuilder();
30             document = builder.parse(creatureDefs);
31         } catch (ParserConfigurationException e) {
32             throw new RuntimeException("Error building xml parser, aborting", e);
33         } catch (SAXException e) {
34             throw new RuntimeException("SAX exception while parsing xml file",
35                     e);
36         } catch (IOException e) {
37             throw new RuntimeException("Error reading or parsing xml file", e);
38         }
39     }
40 
41 
42     <X extends BaseDto> void processInjectionTarget(
43             @Observes ProcessInjectionTarget<X> pit) {
44         Class<? extends BaseDto> klass = pit.getAnnotatedType().getJavaClass();
45         log.info("Setting up injection target for " + klass);
46         final Element entry = (Element) document.getElementsByTagName(
47                 klass.getSimpleName().toLowerCase()).item(0);
48         pit.setInjectionTarget(new XmlWrappedInjection<X>(pit
49                 .getInjectionTarget(), entry));
50     }
51 }

该扩展,将读取resources/inject-beans.xml文件的内容,并完成BaseDto以及所有子类的加载,包括Inject,该类还使用了另一个辅助类:

 1 package dto.extension;
 2 
 3 import java.lang.reflect.Field;
 4 import java.util.Set;
 5 
 6 import javax.enterprise.context.spi.CreationalContext;
 7 import javax.enterprise.inject.InjectionException;
 8 import javax.enterprise.inject.spi.*;
 9 
10 import dto.*;
11 import org.w3c.dom.Element;
12 
13 public class XmlWrappedInjection<X extends BaseDto> implements
14         InjectionTarget<X> {
15     private final InjectionTarget<X> wrapped;
16     private final Element xmlBacking;
17 
18     public XmlWrappedInjection(InjectionTarget<X> it, Element xmlElement) {
19         wrapped = it;
20         xmlBacking = xmlElement;
21     }
22 
23     @Override
24     public void inject(X instance, CreationalContext<X> ctx) {
25         wrapped.inject(instance, ctx);
26 
27         final Class<? extends BaseDto> klass = instance.getClass();
28         //yjm注:出于演示目的,这里仅反射了本类中声明的field,所以注入时,父类中的field会被忽略,大家可以自行修改,逐层向上反射,直到BaseDto类为止
29         for (Field field : klass.getDeclaredFields()) {
30             field.setAccessible(true);
31             final String fieldValueFromXml = xmlBacking.getAttribute(field
32                     .getName());
33             try {
34                 //System.out.println("the filed name is :" + field.getName());
35                 if (field.getName().toLowerCase().equals("serialversionuid")) {
36                     continue;
37                 }
38                 //注:出于演示目的,这里只处理了int、long、String这三种类型,其它类型大家可自行扩展
39                 if (field.getType().isAssignableFrom(Integer.TYPE)) {
40                     field.set(instance, Integer.parseInt(fieldValueFromXml));
41                 } else if (field.getType().isAssignableFrom(Long.TYPE)) {
42                     field.set(instance, Long.parseLong(fieldValueFromXml));
43                 } else if (field.getType().isAssignableFrom(String.class)) {
44                     field.set(instance, fieldValueFromXml);
45                 } else {
46                     throw new InjectionException("Cannot convert to type "
47                             + field.getType());
48                 }
49             } catch (IllegalAccessException e) {
50                 throw new InjectionException("Cannot access field " + field);
51             }
52         }
53     }
54 
55     @Override
56     public void postConstruct(X instance) {
57         wrapped.postConstruct(instance);
58     }
59 
60     @Override
61     public void preDestroy(X instance) {
62         wrapped.preDestroy(instance);
63     }
64 
65     @Override
66     public X produce(CreationalContext<X> ctx) {
67         return wrapped.produce(ctx);
68     }
69 
70     @Override
71     public void dispose(X instance) {
72         wrapped.dispose(instance);
73     }
74 
75     @Override
76     public Set<InjectionPoint> getInjectionPoints() {
77         return wrapped.getInjectionPoints();
78     }
79 }

注:这里仅只是演示,所以处理得相对比较简单,如果一个类继承自父类,Inject时,上面的代码,只反射了子类本身声明的field,对于父类的属性,未逐层向上反射,大家可以自行改进。

2.3 控制器

 1 package controller;
 2 
 3 import javax.inject.Inject;
 4 import javax.inject.Named;
 5 
 6 import dto.Cloth;
 7 import dto.Computer;
 8 import dto.DtoType;
 9 import dto.Product;
10 import dto.ProductType;
11 
12 @Named("Ext")
13 public class ExtensionController {
14 
15     @Inject
16     //@DtoType(ProductType.Computer)
17     private Computer computer;
18 
19     @Inject
20     //@DtoType(ProductType.Cloth)
21     private Cloth cloth;
22 
23     @Inject
24     @DtoType(ProductType.Product)
25     private Product product;
26 
27     public Computer getComputer() {
28         return computer;
29     }
30 
31     public Cloth getCloth() {
32         return cloth;
33     }
34 
35     public Product getProduct() {
36         return product;
37     }
38 
39 }

注:这里思考一下,为什么Product上必须使用注解@DtoType(ProductType.Product),而其它二个Inject的field不需要?如果暂时没想明白的朋友,建议回到第一节 ,看下1.7节的内容,因为Computer、Cloth都继承自Product类,所以在实例Product类时,系统有3个选择:Computer、Cloth、Product,它不知道该选哪一个?所以运行时,系统会罢工,so,需要额外的注释给它一点提示。

2.4 ext.xhtml

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml"
 3     xmlns:h="http://java.sun.com/jsf/html"
 4     xmlns:f="http://java.sun.com/jsf/core"
 5     xmlns:ui="http://java.sun.com/jsf/facelets">
 6 
 7 <h:head>
 8     <title>Extension Test</title>
 9 </h:head>
10 <body>
11     #{Ext.product.toString()}
12     <br /> #{Ext.computer.toString()}
13     <br /> #{Ext.cloth.toString()}
14 
15 </body>
16 </html>

2.5 inject-beans.xml

1 <?xml version="1.0" encoding="UTF-8"?>
2 <basedtos>
3     <product productNo="001" productName="A Unknown New Product" />
4     <computer productNo="T60" productName="ThinkPad" cpuType="2-cores"
5         hardDiskCapacity="320" />
6     <cloth productNo="XX" productName="Underware" brand="JackJohns" />
7 </basedtos>

该文件设计时,要放在main/java/resources/目录下,部署时,会自动复制到webapp/resources/

2.6 javax.enterprise.inject.spi.Extension

/main/java/resources/META-INF/services目录下,新建一个文件:javax.enterprise.inject.spi.Extension,内容如下:

dto.extension.BaseDtoExtension

该文件的作用是在运行时,告诉系统根据BaseDtoExtension类的定义去找inject-beans.xml,它相当于入口。

2.7 运行效果:浏览地址 http://localhost:8080/cdi-alternative-sample/ext.jsf

跟预期结果完全一样,不过正如文中指出的一样,父类的属性被忽略了,如果父类成员也需要初始化,需要大家自行修改XmlWrappedInjection类

最后附示例源代码:cdi-alternative-sample.zip