前面幾節學習到的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 }
Connection
該接口模拟db連接配接,裡面有一個connect方法,用來連接配接db.
1 package contract;
2
3 public interface DriveService {
4
5 String drive();
6
7 }
DriveService
該接口模拟遊戲應用中,有些人物具有駕駛技能。
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 }
OracleConnection
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 }
SqlServerConnection
注: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 }
CarDriveImpl
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 }
MotorcycleDriveImpl
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 }
TractorDriveImpl
注: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 }
ConnectionController
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
注: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>
Index.xhtml
運作結果:
修改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>
beans.xml
重新在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 }
BaseDto
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 }
Product
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 }
Computer
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 }
Cloth
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 }
@DtoType
以及枚舉:
1 package dto;
2
3 public enum ProductType {
4 Product,Computer,Cloth
5 }
ProductType
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 }
BaseDtoExtension
該擴充,将讀取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 }
XmlWrappedInjection
注:這裡僅隻是示範,是以處理得相對比較簡單,如果一個類繼承自父類,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 }
ExtensionController
注:這裡思考一下,為什麼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>
ext.xhtml
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>
inject-beans.xml
該檔案設計時,要放在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
作者:菩提樹下的楊過