標籤:
在任何一個項目中都不可或缺的存在兩種bean,一種是實現系統核心功能的bean,我們稱之為業務類,另外一種是與系統核心業務無關但同時又提供十分重要服務bean,我們稱之為服務類。業務類的bean根據每個系統自身核心功能的不同可以有任意多個,但是服務類的種類在各個系統之間的差異卻並不是很大。在系統中經常用到的服務有以下幾種,許可權服務,Log Service,快取服務,事務服務以及預警服務等。在整個系統的不斷進化過程中,服務類與業務類的關係也不斷的發生著變化,由當初垂直模式變為橫切模式,這也是編程思想不斷演化過程。服務類與業務類本來就不應耦合在一起,否則不但會造成大量的代碼冗餘同時也難以對服務進行可控性變更。那麼如何解決根據不同業務類自身要求為其提供不同服務的問題呢?spring aop給出了完美解決 方案。Spring解決這個難題的核心思想是將服務類與業務類統統交由spring容器管理,根據不同業務類的不同要求動態佈建服務類,也即動態聯編服務類與業務類實現兩者的自由組合。至於業務類與服務類之間的關係演變可以用簡單呈現一下:
這張圖是最原始的業務-服務關係圖,看到這連想都不敢想了,同樣的服務重複出現在N多個業務中,想想也是醉了,如果哪天服務內容改變除了跳樓估計也沒有別的選擇了,還好Spring AOP的出現將這個讓人頭疼的問題。使用Spring 註解方式將切面類橫切到各個服務類中就可以一絕解決代碼冗餘,難於維護的問題
本圖在代碼中的提現如下:
package com.test.util;@Aspectpublic class AuthorityService {@Autowiredprivate LogManager logManager;@Before("execution(* com.test.web.*.*(..))")public void logAll(JoinPoint point) throws Throwable {System.out.println("======authority-before======");}@After("execution(* com.test.web.*.*(..))")public void after() {System.out.println("=========authority-after=========");}// 方法執行的前後調用@Around("execution(* com.test.web.*.*(..))")public Object around(ProceedingJoinPoint point) throws Throwable {System.out.println("======authority-around開始之前before======");HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 獲得詳細時間SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// Calendar ca = Calendar.getInstance();// String operDate = df.format(ca.getTime());Log sysLog = new Log();// 開始時間sysLog.setStartTime(df.format(new Date()));// 擷取ip地址String ip = TCPIPUtil.getIpAddr(request);String loginName;String name;String methodRemark = getMthodRemark(point);String methodName = point.getSignature().getName();String packages = point.getThis().getClass().getName();if (packages.indexOf("$$EnhancerByCGLIB$$") > -1) { // 如果是CGLIB動態產生的類try {packages = packages.substring(0, packages.indexOf("$$"));} catch (Exception ex) {ex.printStackTrace();}}Object object;try {// method_param = point.getArgs(); //擷取方法參數// String param=(String) point.proceed(point.getArgs());object = point.proceed();} catch (Exception e) {// 異常處理記錄日誌..log.error(e);throw e;}sysLog.setIp(ip);sysLog.setClazz(packages);sysLog.setMethod(methodName);sysLog.setMessage(methodRemark);// 結束時間sysLog.setEndTime(df.format(new Date()));// 返回結果if (object == null) {sysLog.setResult("無傳回值");} else {sysLog.setResult(object.toString());}logManager.addLog(sysLog);System.out.println("======authority-around開始之前after======");return object;}@AfterReturning("execution(* com.test.web.*.*(..))")public void x(){System.out.println("-------authority-afterreturning-------");}// 擷取方法的中文備忘____用於記錄使用者的動作記錄描述public static String getMthodRemark(ProceedingJoinPoint joinPoint)throws Exception {String targetName = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();Object[] arguments = joinPoint.getArgs();Class targetClass = Class.forName(targetName);Method[] method = targetClass.getMethods();String methode = "";for (Method m : method) {if (m.getName().equals(methodName)) {Class[] tmpCs = m.getParameterTypes();if (tmpCs.length == arguments.length) {Logger methodCache = m.getAnnotation(Logger.class);// 獲得標記,為空白時沒有標記if (methodCache != null) {methode = methodCache.remark();}break;}}}return methode;}}
從上面代碼可以看出該許可權服務類切入到了com.test.web包下的所有類的所有方法上,同理我們可以再建立快取服務類,Log Service類等分別切入到其他業務類中,現在看來這個令人頭疼的問題似乎被spring基於@Aspect風格的aop解決掉了,但是我們仔細想一想問題真的解決了嗎?大家都知道這樣的切入方式粒度實在是太粗了,並不是相同包下的所有類的所有方法都需要這樣的服務,舉個很簡單的例子,就拿快取服務和Log Service來講,Log Service是需要詳細記錄的它可以切入任何類的任何方法上,但是快取服務卻並不是這樣,快取服務只可能存在於類的擷取資料的方法上,此時將快取服務切入到類的修改,刪除等方法上就顯得非常奇葩了,再通俗的將就是任何一個服務都可能只存在於特定類的特定方法上,這個不確定性決定了只使用Spring @Aspect註解方式是難以解決問題的
此時我們需要怎樣做呢?還好Spring同時提供了基於xml配置方式的aop,這種方式在相當大的程度上彌補了@Aspect不足,他可以根據不同的配置方式將不同的服務切入到不同類的不同方法上,這樣細的粒度足以讓我們實現各種服務對業務的動態組合。說道這可能會有人問既然spring提供了基於xml配置的aop,那我們只需要將不同的服務均配置在xml檔案中不是同樣可以實現業務與服務的動態組合嗎?聽上去似乎很有道理,果真這樣的話似乎基於註解的aop似乎就可以被替代了。但是程式效率的問題又讓我們不得不對xml配置方式和註解方式進行再次衡量。由於xml是在運行期動態聯編確定切面的,解析xml的過程毫無疑問的會佔用系統資源,如果將大量的aop配置配置在xml檔案中將會得不償失,而@Aspect方式支援編譯期織入且不需要組建代理程式,這樣就使得效率上會有優勢。如此來看只要將xml方式與@Aspect方式混合使用,將粗粒度的服務(如日誌和許可權服務)使用@Aspect方式進行切入,對於細粒度的服務(如快取服務)使用xml方式配置在spring中就可以完美解決以上問題了。下面是兩種方式結合使用的執行個體,先來看設定檔:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.2.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd"><!-- 註解掃描包 --><context:component-scan base-package="com.test" /><!-- 開啟註解 --><mvc:annotation-driven /><!-- 靜態資源(js/image)的訪問 --><mvc:resources location="/js/" mapping="/js/**" /><!-- 定義視圖解析器 --><bean id="viewResolver"class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/"></property><property name="suffix" value=".jsp"></property></bean><!-- 啟用Spring對基於@AspectJ aspects的配置支援 --><aop:aspectj-autoproxy></aop:aspectj-autoproxy><bean id="logService" class="com.test.util.LogService"></bean><bean id="cacheService" class="com.test.util.CacheService"></bean> <aop:config proxy-target-class="true"> <aop:aspect ref="cacheService"> <aop:pointcut id="log" expression="execution(* com.test.web.UserController.getAllUser(..))"/> <aop:before pointcut-ref="log" method="logAll"/> <aop:after pointcut-ref="log" method="after"/> <aop:after-returning pointcut-ref="log" method="x"/> <aop:around pointcut-ref="log" method="around"/> </aop:aspect> </aop:config></beans>
接下來是Log Service類:
package com.test.util;@Aspectpublic class LogService {@Autowiredprivate LogManager logManager;@Before("execution(* com.test.web.*.*(..))")public void logAll(JoinPoint point) throws Throwable {System.out.println("======log-before======");}@After("execution(* com.test.web.*.*(..))")public void after() {System.out.println("=========cache-after=========");}// 方法執行的前後調用@Around("execution(* com.test.web.*.*(..))")public Object around(ProceedingJoinPoint point) throws Throwable {System.out.println("======log-around開始之前before======");HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 獲得詳細時間SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Log sysLog = new Log();// 開始時間sysLog.setStartTime(df.format(new Date()));// 擷取ip地址String ip = TCPIPUtil.getIpAddr(request);String loginName;String name;String methodRemark = getMthodRemark(point);String methodName = point.getSignature().getName();String packages = point.getThis().getClass().getName();if (packages.indexOf("$$EnhancerByCGLIB$$") > -1) { // 如果是CGLIB動態產生的類try {packages = packages.substring(0, packages.indexOf("$$"));} catch (Exception ex) {ex.printStackTrace();}}// String operatingcontent = "";// Object[] method_param = null;Object object;try {// method_param = point.getArgs(); //擷取方法參數// String param=(String) point.proceed(point.getArgs());object = point.proceed();} catch (Exception e) {// 異常處理記錄日誌..log.error(e);throw e;}sysLog.setIp(ip);sysLog.setClazz(packages);// 包名.+方法名// packages + "." + methodNamesysLog.setMethod(methodName);sysLog.setMessage(methodRemark);// 結束時間sysLog.setEndTime(df.format(new Date()));// 返回結果if (object == null) {sysLog.setResult("無傳回值");} else {sysLog.setResult(object.toString());}logManager.addLog(sysLog);System.out.println("======log-around開始之前after======");return object;}@AfterReturning("execution(* com.test.web.*.*(..))")public void x(){System.out.println("-------log-afterreturning-------");}// 擷取方法的中文備忘____用於記錄使用者的動作記錄描述public static String getMthodRemark(ProceedingJoinPoint joinPoint)throws Exception {String targetName = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();Object[] arguments = joinPoint.getArgs();Class targetClass = Class.forName(targetName);Method[] method = targetClass.getMethods();String methode = "";for (Method m : method) {if (m.getName().equals(methodName)) {Class[] tmpCs = m.getParameterTypes();if (tmpCs.length == arguments.length) {Logger methodCache = m.getAnnotation(Logger.class);// 獲得標記,為空白時沒有標記if (methodCache != null) {methode = methodCache.remark();}break;}}}return methode;}}
再接下來是快取服務類;
package com.test.util;public class LogService {@Autowiredprivate CacheManager logManager;public void pointcut() {}public void logAll(JoinPoint point) throws Throwable {System.out.println("======cache-before======");}public void after() {System.out.println("=========cache-after=========");}// 方法執行的前後調用public Object around(ProceedingJoinPoint point) throws Throwable {System.out.println("======cache-around開始之前before======");HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 獲得詳細時間SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// Calendar ca = Calendar.getInstance();// String operDate = df.format(ca.getTime());Log sysLog = new Log();// 開始時間sysLog.setStartTime(df.format(new Date()));// 擷取ip地址String ip = TCPIPUtil.getIpAddr(request);String loginName;String name;String methodRemark = getMthodRemark(point);String methodName = point.getSignature().getName();String packages = point.getThis().getClass().getName();if (packages.indexOf("$$EnhancerByCGLIB$$") > -1) { // 如果是CGLIB動態產生的類try {packages = packages.substring(0, packages.indexOf("$$"));} catch (Exception ex) {ex.printStackTrace();}}Object object;try {// method_param = point.getArgs(); //擷取方法參數// String param=(String) point.proceed(point.getArgs());object = point.proceed();} catch (Exception e) {// 異常處理記錄日誌..log.error(e);throw e;}sysLog.setIp(ip);sysLog.setClazz(packages);// 包名.+方法名// packages + "." + methodNamesysLog.setMethod(methodName);sysLog.setMessage(methodRemark);// 結束時間sysLog.setEndTime(df.format(new Date()));// 返回結果if (object == null) {sysLog.setResult("無傳回值");} else {sysLog.setResult(object.toString());}logManager.addLog(sysLog);System.out.println("======cache-around開始之前after======");return object;}public void x(){System.out.println("-------cache-afterreturning-------");}}
Log Service類和快取服務類只在具體處理過程中不一樣,本文在Log Service的基礎上稍加改動簡化處理,大家可以在使用時根據自身情況進行緩衝處理和Tlog。在具體運行過程中細心的朋友可能會發現在設定檔中不同服務的出現順序決定了兩個服務的執行順序,也就是說在spring aop執行切入動作是通過裝飾者模式來完成,採用類似棧的先進後出方式來具體執行服務這一點大家一定要注意啊。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Spring容器裝飾者模式應用之實現業務類與服務類自由組合的解決方案