1. 門面+模版方法+責任鏈+策略
1.1 項目背景
在公司的一個實際項目中,需要做一個第三方公司(以下簡稱XHF)的系統集成工作,把該公司的一些訂單數據集成到自己公司平臺下,各個訂單具有一些共性,但是也有其特有的特徵。 經過設計,目前我把訂單分為POLICY和XHF類型(暫且這麼說吧,反正就是一種訂單類型,大家參照著看就OK)。
在訂單數據集成到公司平臺前,需要對訂單數據進行一些必要的業務邏輯校驗操作,並且每個訂單都有自己的校驗邏輯(包含公共的校驗邏輯)。 本節介紹的便是整個訂單集成系統中的校驗邏輯在綜合利用設計模式的基礎上進行架構設計。
1.2 校驗邏輯
本校驗邏輯主要分為四個部分:
- 校驗文件名稱(RequestValidator.validateFileInfo)
- 校驗文件內容中的概要部分(RequestValidator.validateSummary)
- 校驗文件內容中的列名稱(RequestValidator.validateHeaders)
- 校驗文件內容中的明細(RequestValidator.validateDetails)
其實上面的RequestValidator的實現邏輯最後都是委託給RequestValidationFacade這個門面類進行相應的校驗操作。
1.3 實現細節
1.3.1 domain介紹
主要分為RequestFile和RequestDetail兩個domain,RequestFile接收泛型的類型(即RequestFile), 使得其子類能夠自動識別相應的RequestDetail的子類。RequestFile為抽象類,定義了以下抽象方法,由子類實現:
<code>//由子類實現具體的獲取文件明細內容 public abstract List getRequestDetails(); //由子類實現具體的獲取workflow的值 public abstract WorkflowEnum getProcessWorkFlow(); //由子類實現文件列字段名列表 public abstract String[] getDetailHeaders(); /<code>
RequestDetail及其子類就是workflow對應文件的明細內容。
1.3.2 WorkflowEnum枚舉策略
本例中如下規定:
- workflow為WorkflowEnum.POLICY對應文件名為:xhf_policy_yyyyMMdd_HHmmss_count.txt
- workflow為WorkflowEnum.XHF對應文件名為:xhf_integration_yyyyMMdd_HHmmss_count.txt
以上校驗邏輯在AbstractRequestValidation類相應的子類中實現(validateFileName方法),其實這個枚舉貫穿整個校驗組件,它就是一個針對每個業務流程定義的一個枚舉策略。
1.3.3 涉及到的設計模式實現思路
1.3.3.1 門面模式
在客戶端調用程序中,採用門面模式進行統一的入口(門面模式講究的是脫離具體的業務邏輯代碼)。門面模式封裝的結果就是避免高層模塊深入子系統內部,同時提供系統的高內聚、低耦合的特性。
此案例中,門面類為RequestValidationFacade,然後各個門面方法的參數均為抽象類RequestFile,通過RequestFile->getProcessWorkFlow()決定調用AbstractRequestValidation中的哪個子類。 AbstractRequestValidation類構造方法中定義瞭如下邏輯:
<code>requestValidationHandlerMap.put(this.accessWorkflow(),this.accessBeanName()); /<code>
把子類中Spring自動注入的實體bean緩存到requestValidationHandlerMap中,key即為WorkflowEnum枚舉值,value為spring bean name, 然後在門面類中可以通過對應的枚舉值取得BeanName,進而得到AbstractRequestValidation相應的子類對象,進行相應的校驗操作。
注:這邊動態調用到AbstractRequestValidation相應的子類對象,其實也是隱藏著【策略模式】的影子。
1.3.3.2 模版方法模式
在具體的校驗邏輯中,用到核心設計模式便是模版方法模式,AbstractRequestValidation抽象類中定義了以下抽象方法:
<code> /** * validate the file details * @param errMsg * @param requestFile * @return */ protected abstract StringBuilder validateFileDetails(StringBuilder errMsg,RequestFile requestFile); /** * validate the file name * @param fileName * @return */ protected abstract String validateFileName(String fileName); /** * return the current xhf_UPDATE_WORKFLOW.UPDATE_WORKFLOW_ID * @return */ protected abstract WorkflowEnum accessWorkflow(); /** * return the current file name's format ,such as: xhf_policy_yyyyMMdd_HHmmss_count.txt * @return */ protected abstract String accessFileNameFormat(); /** * return the subclass's spring bean name * @return */ protected abstract String accessBeanName(); /<code>
以上抽象方法就類似我們常說的鉤子函數,由子類實現即可。
1.3.3.3 責任鏈模式
在AbstractRequestValidation抽象類中有個抽象方法validateFileDetails,校驗的是文件的明細內容中的相應業務規則,此為核心校驗, 較為複雜,而且針對每個業務流程,其校驗邏輯相差較大,在此處,利用了責任鏈模式進行處理。
Validator為校驗器的父接口,包含兩個泛型參數(即:),其實現類可以方便的轉換需要校驗的文件明細。
<code>String doValidate(R detail, F file, ValidatorChain chain) throws BusinessValidationException; /<code>
該方法含有一個ValidatorChain參數,就自然而然的為該校驗器形成一個鏈條提供便利條件。
ValidatorChain為校驗器鏈,含有兩個接口方法:
<code> String doValidate(T requestDetail, F requestFile) throws BusinessValidationException; ValidatorChain addValidator(Validator validator, WorkflowEnum workflowId); /<code>
該處有一個addValidator方法,為ValidatorChain對象添加校驗器的方法,返回本身。對應於每個業務流程需要哪些校驗器就在此實現即可(即AbstractRequestValidation的子類方法validateFileDetails)。
1.3.3.4 策略模式
如果單單從上面的校驗器實現上來看,如果需要增加一個校驗器,就需要在AbstractRequestValidation的子類方法validateFileDetails中添加,然後進行相應的校驗操作。這樣就會非常的麻煩,沒有做到真正的解耦。 此時,策略模式就發揮到了可以動態選擇某種校驗策略的作用(Validator的實現類就是一個具體的校驗策略)。
AbstractValidatorHandler抽象類持有FileDetailValidatorChain類的對象,並且實現累Spring的一個接口ApplicationListener(是為了Spring容器啟動完成的時候自動把相應的校驗器加入到校驗器鏈中)。 核心就是WorkflowEnum這個策略枚舉的作用,在子類可以動態的取得相應的校驗器對象。
根據子類提供需要的校驗器所在的包名列表和不需要的校驗器列表,動態配置出需要的校驗器鏈表。核心實現邏輯如下:
<code>private void addValidators() { List> validators = getValidators(); validators.forEach((validator) -> { String simpleName = validator.getSimpleName(); String beanName = simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1); LOGGER.info("Added validator:{},spring bean name is:{}",simpleName,beanName); Validator validatorInstance = ApplicationUtil.getApplicationContext().getBean(beanName,validator); fileDetailValidatorChain.addValidator(validatorInstance,getWorkflowId()); }); } /<code>
具體實現可以參考代碼即可。
該類含有以下幾個抽象方法:
<code>protected abstract WorkflowEnum getWorkflowId(); /** * the package need to be added the validators * @return */ protected abstract Set getBasePackages(); /** * the classes need to be excluded * @return */ protected abstract Set excludeClasses(); /<code>
2. 門面+模版方法+責任鏈+策略+工廠方法
上一節在實現策略模式的實現上,發現了一個弊端:那就是如果在後續業務發展中,需要再次增加一個業務策略的時候,則需要再次繼承AbstractValidatorHandler類(詳情請參見上篇文章),這樣就會造成一定的類膨脹。今天我利用註解的方式改造成動態策略模式,這樣就只需要關注自己的業務類即可,無需再實現一個類似的Handler類。
2.1 項目背景
2.1.1 項目簡介
在公司的一個業務系統中,有這樣的一個需求,就是根據不同的業務流程,可以根據不同的組合主鍵策略進行動態的數據業務查詢操作。在本文中,我假設有這樣兩種業務,客戶信息查詢和訂單信息查詢,對應以下枚舉類:
<code>/** * 業務流程枚舉 * @author landyl * @create 11:18 AM 05/07/2018 */ public enum WorkflowEnum { ORDER(2), CUSTOMER(3), ; }/<code>
每種業務類型都有自己的組合主鍵查詢規則,並且有自己的查詢優先級,比如客戶信息查詢有以下策略:
- customerId
- requestId
- birthDate+firstName
以上僅是假設性操作,實際業務規則比這複雜的多
2.1.2 流程梳理
主要業務流程,可以參照以下簡單的業務流程圖。
2.1.2.1 查詢抽象模型
2.1.2.2 組合主鍵查詢策略
2.1.2.3 組合主鍵查詢責任鏈
2.2 Java註解簡介
註解的語法比較簡單,除了@符號的使用之外,它基本與Java固有語法一致。
2.2.1 元註解
JDK1.5提供了4種標準元註解,專門負責新註解的創建。
註解說明@Target表示該註解可以用於什麼地方,可能的ElementType參數有:
CONSTRUCTOR:構造器的聲明
FIELD:域聲明(包括enum實例)
LOCAL_VARIABLE:局部變量聲明
METHOD:方法聲明
PACKAGE:包聲明
PARAMETER:參數聲明
TYPE:類、接口(包括註解類型)或enum聲明@Retention表示需要在什麼級別保存該註解信息。可選的RetentionPolicy參數包括:
SOURCE:註解將被編譯器丟棄
CLASS:註解在class文件中可用,但會被VM丟棄
RUNTIME:JVM將在運行期間保留註解,因此可以通過反射機制讀取註解的信息@Document將註解包含在Javadoc中@Inherited允許子類繼承父類中的註解
2.2.2 自定義註解
定義一個註解的方式相當簡單,如下代碼所示:
<code>@Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented //使用@interface關鍵字定義註解 public @interface Description { /* * 註解方法的定義(其實在註解中也可以看做成員變量)有如下的規定: * 1.不能有參數和拋出異常 * 2.方法返回類型只能為八種基本數據類型和字符串,枚舉和註解以及這些類型構成的數組 * 3.可以包含默認值,通過default實現 * 4.如果只有一個方法(成員變量),最好命名為value */ String value(); int count() default 1; //默認值為1 }/<code>
註解的可用的類型包括以下幾種:所有基本類型、String、Class、enum、Annotation、以上類型的數組形式。元素不能有不確定的值,即要麼有默認值,要麼在使用註解的時候提供元素的值。而且元素不能使用null作為默認值。註解在只有一個元素且該元素的名稱是value的情況下,在使用註解的時候可以省略“value=”,直接寫需要的值即可。
2.2.3 使用註解
如上所示的註解使用如下:
<code>/** * @author landyl * @create 2018-01-12:39 PM */ //在類上使用定義的Description註解 @Description(value="class annotation",count=2) public class Person { private String name; private int age; //在方法上使用定義的Description註解 @Description(value="method annotation",count=3) public String speak() { return "speaking..."; } }/<code>
使用註解最主要的部分在於對註解的處理,那麼就會涉及到註解處理器。從原理上講,註解處理器就是通過反射機制獲取被檢查方法上的註解信息,然後根據註解元素的值進行特定的處理。
<code>/** * @author landyl * @create 2018-01-12:35 PM * 註解解析類 */ public class ParseAnnotation { public static void main(String[] args){ //使用類加載器加載類 try { Class c = Class.forName("com.annatation.Person");//加載使用了定義註解的類 //找到類上的註解 boolean isExist = c.isAnnotationPresent(Description.class); if(isExist){ //拿到註解示例 Description d = (Description)c.getAnnotation(Description.class); System.out.println(d.value()); } //找到方法上的註解 Method[] ms = c.getMethods(); for(Method m : ms){ boolean isMExist = m.isAnnotationPresent(Description.class); if(isMExist){ Description d = m.getAnnotation(Description.class); System.out.println(d.value()); } } //另外一種註解方式 for(Method m:ms){ Annotation[] as = m.getAnnotations(); for(Annotation a:as){ if(a instanceof Description){ Description d = (Description)a; System.out.println(d.value()); } } } } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }/<code>
2.3 策略模式升級版
2.3.1 策略模式實現方式
- 使用工廠進行簡單的封裝
- 使用註解動態配置策略
- 使用模版方法模式配置策略(參見1. 門面+模版方法+責任鏈+策略)
- 使用工廠+註解方式動態配置策略(利用Spring加載)
其中第1、2點請參見org.landy.strategy 包下的demo事例即可,而第4點的方式其實就是結合第1、2、3點的優點進行整合的方式。
2.3.2 註解方式優點
使用註解方式可以極大的減少使用模版方法模式帶來的擴展時需要繼承模版類的弊端,工廠+註解的方式可以無需關心其他業務類的實現,而且減少了類膨脹的風險。
2.3.3 組合主鍵查詢策略
本文以組合主鍵查詢策略這一策略進行說明,策略註解如下:
<code>/** * 組合主鍵查詢策略(根據不同業務流程區分組合主鍵查詢策略,並且每個業務流程都有自己的優先級策略) * @author landyl * @create 2:22 PM 09/29/2018 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface KeyIdentificationStrategy { /** * 主鍵策略優先級 * @return */ int priority() default 0; /** * 業務流程類型(如:訂單信息,會員信息等業務流程) * @return */ WorkflowEnum workflowId(); /** * the spring bean name * @return */ String beanName(); }/<code>
2.3.4 策略工廠
既然定義了組合主鍵查詢策略註解,那必然需要一個註解處理器進行解析註解的操作,本文以工廠的方式進行。主要邏輯如下:
- 掃描指定包下的Java類,找出相應接口(即KeyIdentification)下的所有Class對象。private List> getIdentifications() { Set packageNames = this.getBasePackages(); List> identifications = new ArrayList<>(); if(packageNames != null) { packageNames.forEach((packageName) -> identifications.addAll(getIdentifications(packageName))); } return identifications; }
- 解析註解KeyIdentificationStrategy,定義一個排序對象(KeyIdentificationComparator),指定優先級。private class KeyIdentificationComparator implements Comparator { @Override public int compare(Object objClass1, Object objClass2) { if(objClass1 != null && objClass2 != null) { Optional strategyOptional1 = getPrimaryKeyIdentificationStrategy((Class)objClass1); Optional strategyOptional2 = getPrimaryKeyIdentificationStrategy((Class)objClass2); KeyIdentificationStrategy ip1 = strategyOptional1.get(); KeyIdentificationStrategy ip2 = strategyOptional2.get(); Integer priority1 = ip1.priority(); Integer priority2 = ip2.priority(); WorkflowEnum workflow1 = ip1.workflowId(); WorkflowEnum workflow2 = ip2.workflowId(); //先按業務類型排序 int result = workflow1.getValue() - workflow2.getValue(); //再按優先級排序 if(result == 0) return priority1.compareTo(priority2); return result; } return 0; } }
- 根據註解,把相應業務類型的組合主鍵查詢策略對象放入容器中(即DefaultKeyIdentificationChain)。KeyIdentificationStrategy strategy = strategyOptional.get(); String beanName = strategy.beanName(); //業務流程類型 WorkflowEnum workflowId = strategy.workflowId(); KeyIdentificationStrategy priority = getPrimaryKeyIdentificationStrategy(v).get(); LOGGER.info("To add identification:{},spring bean name is:{},the identify priority is:{},workflowId:{}",simpleName,beanName,priority.priority(),workflowId.name()); KeyIdentification instance = ApplicationUtil.getApplicationContext().getBean(beanName,v); defaultKeyIdentificationChain.addIdentification(instance,workflowId);
- 後續,在各自對應的業務查詢組件對象中即可使用該工廠對象調用如下方法,即可進行相應的查詢操作。
<code>public IdentificationResultType identify(IdentifyCriterion identifyCriterion,WorkflowEnum workflowId) { defaultKeyIdentificationChain.doClearIdentificationIndex(workflowId); return defaultKeyIdentificationChain.doIdentify(identifyCriterion,workflowId); }/<code>
3. 動態代理+Spring AOP
AOP設計模式通常運用在日誌,校驗等業務場景,本文將簡單介紹基於Spring的AOP代理模式的運用。
3.1 Spring AOP
3.1.1 Spring AOP原理
AOP實現的關鍵在於AOP框架自動創建的AOP代理,AOP代理主要分為靜態代理和動態代理,靜態代理的代表為AspectJ;而動態代理則以Spring AOP為代表。本文以Spring AOP的實現進行分析和介紹。
Spring AOP使用的動態代理,所謂的動態代理就是說AOP框架不會去修改字節碼,而是在內存中臨時為方法生成一個AOP對象,這個AOP對象包含了目標對象的全部方法,並且在特定的切點做了增強處理,並回調原對象的方法。
Spring AOP中的動態代理主要有兩種方式,JDK動態代理和CGLIB動態代理。JDK動態代理通過反射來接收被代理的類,並且要求被代理的類必須實現一個接口。JDK動態代理的核心是InvocationHandler接口和Proxy類。
如果目標類沒有實現接口,那麼Spring AOP會選擇使用CGLIB來動態代理目標類。CGLIB(Code Generation Library),是一個代碼生成的類庫,可以在運行時動態的生成某個類的子類,注意,CGLIB是通過繼承的方式做的動態代理,因此如果某個類被標記為final,那麼它是無法使用CGLIB做動態代理的。
注意:以上片段引用自文章Spring AOP的實現原理,如有冒犯,請聯繫筆者刪除之,謝謝!
Spring AOP判斷是JDK代理還是CGLib代理的源碼如下(來自org.springframework.aop.framework.DefaultAopProxyFactory):
<code>@Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } }/<code>
由代碼發現,如果配置proxyTargetClass = true了並且目標類非接口的情況,則會使用CGLib代理,否則使用JDK代理。
3.1.2 Spring AOP配置
Spring AOP的配置有兩種方式,XML和註解方式。
3.1.2.1 XML配置
首先需要引入AOP相關的DTD配置,如下:
<code>/<code>
然後需要引入AOP自動代理配置:
<code> /<code>
3.1.2.2 註解配置
Java配置類如下:
<code>/** * 相當於Spring.xml配置文件的作用 * @author landyl * @create 2:44 PM 09/30/2018 */ @Configuration //@EnableLoadTimeWeaving(aspectjWeaving = EnableLoadTimeWeaving.AspectJWeaving.ENABLED) @EnableAspectJAutoProxy(proxyTargetClass = true) //@EnableAspectJAutoProxy @ComponentScan(basePackages = "org.landy") public class ApplicationConfigure { @Bean public ApplicationUtil getApplicationUtil() { return new ApplicationUtil(); } }/<code>
3.1.2.3 依賴包
需要使用Spring AOP需要引入以下Jar包:
<code> 5.0.8.RELEASE 1.8.7 org.aspectj aspectjrt ${aspectj.version} org.aspectj aspectjweaver ${aspectj.version} org.springframework spring-aop ${spring.version} compile /<code>
3.1.2.4 配置單元測試
以上兩種配置方式,單元測試需要注意一個地方就是引入配置的方式不一樣,區別如下:
- XML方式@ContextConfiguration(locations = { "classpath:spring.xml" }) //加載配置文件 @RunWith(SpringJUnit4ClassRunner.class) //使用junit4進行測試 public class SpringTestBase extends AbstractJUnit4SpringContextTests { }
- 註解方式
<code>@ContextConfiguration(classes = ApplicationConfigure.class) @RunWith(SpringJUnit4ClassRunner.class) //使用junit4進行測試 public class SpringTestBase extends AbstractJUnit4SpringContextTests { }/<code>
配置好了以後,以後所有的測試類都繼承SpringTestBase類即可。
3.2 項目演示
3.2.1 邏輯梳理
本文將以校驗某個業務邏輯為例說明Spring AOP代理模式的運用。
按照慣例,還是以客戶信息更新校驗為例,假設有個校驗類如下:
<code>/** * @author landyl * @create 2:22 PM 09/30/2018 */ @Component public class CustomerUpdateRule implements UpdateRule { //利用自定義註解,進行AOP切面編程,進行其他業務邏輯的校驗操作 @StatusCheck public CheckResult check(String updateStatus, String currentStatus) { System.out.println("CustomerUpdateRule:在此還有其他業務校驗邏輯。。。。"+updateStatus + "____" + currentStatus); return new CheckResult(); } }/<code>
此時我們需要定義一個註解StatusCheck類,如下:
<code>/** * @author landyl * @create 2:37 PM 09/23/2018 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface StatusCheck { }/<code>
此註解僅為一個標記註解。最為主要的就是定義一個更新校驗的切面類,定義好切入點。
<code>@Component @Aspect public class StatusCheckAspect { private static final int VALID_UPDATE = Constants.UPDATE_STATUS_VALID_UPDATE; private static final Logger LOGGER = LoggerFactory.getLogger(StatusCheckAspect.class); //定義切入點:定義一個方法,用於聲明切面表達式,一般地,該方法中不再需要添加其他的代碼 @Pointcut("execution(* org.landy.business.rules..*(..)) && @annotation(org.landy.business.rules.annotation.StatusCheck)") public void declareJoinPointExpression() {} /** * 前置通知 * @param joinPoint */ @Before("declareJoinPointExpression()") public void beforeCheck(JoinPoint joinPoint) { System.out.println("before statusCheck method start ..."); System.out.println(joinPoint.getSignature()); //獲得自定義註解的參數 String methodName = joinPoint.getSignature().getName(); List args = Arrays.asList(joinPoint.getArgs()); System.out.println("The method " + methodName + " begins with " + args); System.out.println("before statusCheck method end ..."); } } /<code>
具體代碼請參見github。
3.2.2 邏輯測試
3.2.2.1 JDK動態代理
JDK動態代理必須實現一個接口,本文實現UpdateRule為例,
<code>public interface UpdateRule { CheckResult check(String updateStatus, String currentStatus); }/<code>
並且AOP需要做如下配置:
XML方式:
<code> /<code>
註解方式:
<code>@Configuration @EnableAspectJAutoProxy @ComponentScan(basePackages = "org.landy") public class ApplicationConfigure { }/<code>
在測試類中,必須使用接口方式注入:
<code>/** * @author landyl * @create 2:32 PM 09/30/2018 */ public class CustomerUpdateRuleTest extends SpringTestBase { @Autowired private UpdateRule customerUpdateRule; //JDK代理方式必須以接口方式注入 @Test public void customerCheckTest() { System.out.println("proxy class:" + customerUpdateRule.getClass()); CheckResult checkResult = customerUpdateRule.check("2","currentStatus"); AssertUtil.assertTrue(checkResult.getCheckResult() == 0,"與預期結果不一致"); } }/<code>
測試結果如下:
<code>proxy class:class com.sun.proxy.$Proxy34 2018-10-05 14:18:17.515 [main] INFO org.landy.business.rules.aop.StatusCheckAspect - Status check around method start .... before statusCheck method start ... CheckResult org.landy.business.rules.stategy.UpdateRule.check(String,String) The method check begins with [2, currentStatus] before statusCheck method end ... CustomerUpdateRule:在此還有其他業務校驗邏輯。。。。2____currentStatus 2018-10-05 14:18:17.526 [main] INFO org.landy.business.rules.aop.StatusCheckAspect - execute the target method,the return result_msg:null 2018-10-05 14:18:17.526 [main] INFO org.landy.business.rules.aop.StatusCheckAspect - Status check around method end .... /<code>
以上結果說明它生成的代理類為$Proxy34,說明是JDK代理。
3.2.2.2 CGLib動態代理
使用CGlib可以不用接口(經測試,用了接口好像也沒問題)。在測試類中,必須使用實現類方式注入:
<code> @Autowired private CustomerUpdateRule customerUpdateRule;/<code>
並且AOP需要做如下配置:
XML方式:
<code> /<code>
註解方式:
<code>@Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) @ComponentScan(basePackages = "org.landy") public class ApplicationConfigure { }/<code>
不過發現我並未配置proxyTargetClass = true也可以正常運行,有點奇怪。(按理說,默認是為false)
運行結果生成的代理類為:
<code>proxy class:class org.landy.business.rules.stategy.CustomerUpdateRule$EnhancerBySpringCGLIB$d1075aca /<code>
說明是CGLib代理。
經過進一步測試,發現如果我實現接口UpdateRule,但是注入方式使用類注入方式:
<code>@Autowired private CustomerUpdateRule customerUpdateRule;/<code>
並且把proxyTargetClass設置為false,則運行就報如下錯誤:
<code>嚴重: Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@6a024a67] to prepare test instance [org.landy.business.rules.CustomerUpdateRuleTest@7fcf2fc1] org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.landy.business.rules.CustomerUpdateRuleTest': Unsatisfied dependency expressed through field 'customerUpdateRule'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'customerUpdateRule' is expected to be of type 'org.landy.business.rules.stategy.CustomerUpdateRule' but was actually of type 'com.sun.proxy.$Proxy34' at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:586) /<code>
以上說明了一個問題,使用接口實現的方式則會被默認為JDK代理方式,如果需要使用CGLib代理,需要把proxyTargetClass設置為true。
3.2.2.3 綜合測試
為了再次驗證Spring AOP如何選擇JDK代理還是CGLib代理,在此進行一個綜合測試。
測試前提:
- 實現UpdateRule接口
- 測試類使用接口方式注入@Autowired private UpdateRule customerUpdateRule; //JDK代理方式必須以接口方式注入
測試:
配置proxyTargetClass為true,運行結果如下:
<code>customerCheckTest proxy class:class org.landy.business.rules.stategy.CustomerUpdateRule$EnhancerBySpringCGLIB$f5a34953 2018-10-05 15:28:42.820 [main] INFO org.landy.business.rules.aop.StatusCheckAspect - Status check around method start .... 2018-10-05 15:28:42.823 [main] INFO org.landy.business.rules.aop.StatusCheckAspect - Status check dynamic AOP,paramValues:2 AOP實際校驗邏輯。。。。2----currentStatus before statusCheck method start ... target class:org.landy.business.rules.stategy.CustomerUpdateRule@7164ca4c /<code>
說明為CGLIb代理。
配置proxyTargetClass為false,運行結果如下:
<code>proxy class:class com.sun.proxy.$Proxy34 2018-10-05 15:20:59.894 [main] INFO org.landy.business.rules.aop.StatusCheckAspect - Status check around method start .... before statusCheck method start ... target class:org.landy.business.rules.stategy.CustomerUpdateRule@ae3540e /<code>
說明為JDK代理。
以上測試說明,指定proxy-target-class為true可強制使用cglib。
3.2.3 常見問題
如果使用JDK動態代理,未使用接口方式注入(或者使用接口實現,並未配置proxyTargetClass為true),則會出現以下異常信息:
<code>嚴重: Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@6a024a67] to prepare test instance [org.landy.business.rules.CustomerUpdateRuleTest@7fcf2fc1] org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.landy.business.rules.CustomerUpdateRuleTest': Unsatisfied dependency expressed through field 'customerUpdateRule'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'customerUpdateRule' is expected to be of type 'org.landy.business.rules.stategy.CustomerUpdateRule' but was actually of type 'com.sun.proxy.$Proxy34' /<code>
與生成的代理類型不一致,有興趣的同學可以Debug DefaultAopProxyFactory類中的createAopProxy方法即可自動兩種動態代理的區別。