SpringAOP在項目中的簡單應用




SpringAOP在項目中的簡單應用

一、前言

項目背景

由於公司的機房到期,需要做應用遷移(包括數據庫),割接到雲平臺。但是又不能進行一次性割接,需要分三個階段:第一階段割接2個地市,第二階段再割接9個地市,第三階段做全盤割接。在第一、第二階段是要保證雲平臺與現網平臺的應用能夠正常並行運行。所以,要保證兩邊數據訪問的正常,需要對某些功能進行改造。比如,某個子功能在並行階段是不做割接的,那麼在做用戶登錄驗證的時候,就需要訪問兩邊的數據庫來做驗證。

技術背景

此次改造涉及到應用的改造,按理應該要修改原來的代碼,才能保證在並行階段的功能正常。這樣子的話,就要維護兩套代碼,即雲平臺一套、現網一套。而且到了最後的割接的時候,還需要將代碼進行還原,工作量非常大,且風險也高。後來想到用SpringAOP能夠在不改動原代碼的基礎上加入一些附加的處理,而且只要配置好就行,功能也是支持熱拔插的(將配置去掉即可)。

二、SpringAOP介紹

網上找了不少關於springAOP的相關資料,在這裏做一個整合及小結,當然,只是初級層面的理解,沒有深入研究其代碼實現。

AOP概念

面向切面編程(也叫面向方面編程):Aspect Oriented Programming(AOP),是軟件開發中的一個熱點,也是Spring框架中的一個重要內容。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

AOP是OOP的延續。

主要的功能是:日誌記錄,性能統計,安全控制,事務處理,異常處理等等。

主要的意圖是:將日誌記錄,性能統計,安全控制,事務處理,異常處理等代碼從業務邏輯代碼中劃分出來,通過對這些行爲的分離,我們希望可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行爲的時候不影響業務邏輯的代碼。

可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種技術。AOP實際是GoF設計模式的延續,設計模式孜孜不倦追求的是調用者和被調用者之間的解耦,提高代碼的靈活性和可擴展性,AOP可以說也是這種目標的一種實現。

在Spring中提供了面向切面編程的豐富支持,允許通過分離應用的業務邏輯與系統級服務(例如審計(auditing)和事務(transaction)管理)進行內聚性的開發。應用對象只實現它們應該做的——完成業務邏輯——僅此而已。它們並不負責(甚至是意識)其它的系統級關注點,例如日誌或事務支持。

AspectJ介紹

AspectJ是AOP的一個很悠久的實現,它能夠和 Java 配合起來使用。

         這裏介紹AspectJ 幾個必須要了解的概念

·        切面(Aspect :官方的抽象定義爲“一個關注點的模塊化,這個關注點可能會橫切多個對象”,在本例中,“切面”就是類TestAspect所關注的具體行爲,例如,AServiceImpl.barA()的調用就是切面TestAspect所關注的行爲之一。“切面”在ApplicationContext中<aop:aspect>來配置。

·        連接點(Joinpoint :程序執行過程中的某一行爲,例如,AServiceImpl.barA()的調用或者BServiceImpl.barB(String_msg, int _type)拋出異常等行爲。

·        通知(Advice :“切面”對於某個“連接點”所產生的動作,例如,TestAspect中對com.spring.service包下所有類的方法進行日誌記錄的動作就是一個Advice。其中,一個“切面”可以包含多個“Advice”,例如TestAspect

·        切入點(Pointcut :匹配連接點的斷言,在AOP中通知和一個切入點表達式關聯。例如,TestAspect中的所有通知所關注的連接點,都由切入點表達式execution(*com.spring.service.*.*(..))來決定

·        目標對象(Target Object :被一個或者多個切面所通知的對象。例如,AServcieImpl和BServiceImpl,當然在實際運行時,Spring AOP採用代理實現,實際AOP操作的是TargetObject的代理對象。

·        AOP代理(AOP Proxy 在Spring AOP中有兩種代理方式,JDK動態代理和CGLIB代理。默認情況下,TargetObject實現了接口時,則採用JDK動態代理,例如,AServiceImpl;反之,採用CGLIB代理,例如,BServiceImpl。強制使用CGLIB代理需要將 <aop:config> 的 proxy-target-class 屬性設爲true

   我們在使用該框架進行業務整改,主要的邏輯代碼實就在於通知(Advice),常用有以下幾種類型:

·        前置通知(Before advice :在某連接點(JoinPoint)之前執行的通知,但這個通知不能阻止連接點前的執行。ApplicationContext中在<aop:aspect>裏面使用<aop:before>元素進行聲明。例如,TestAspect中的doBefore方法

·        後通知(After advice :當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。ApplicationContext中在<aop:aspect>裏面使用<aop:after>元素進行聲明。例如,TestAspect中的doAfter方法,所以AOPTest中調用BServiceImpl.barB拋出異常時,doAfter方法仍然執行

·        返回後通知(After return advice :在某連接點正常完成後執行的通知,不包括拋出異常的情況。ApplicationContext中在<aop:aspect>裏面使用<after-returning>元素進行聲明。

·        環繞通知(Around advice :包圍一個連接點的通知,類似Web中Servlet規範中的Filter的doFilter方法。可以在方法的調用前後完成自定義的行爲,也可以選擇不執行。ApplicationContext中在<aop:aspect>裏面使用<aop:around>元素進行聲明。例如,TestAspect中的doAround方法。

拋出異常後通知(After throwing advice :在方法拋出異常退出時執行的通知。 ApplicationContext中在<aop:aspect>裏面使用<aop:after-throwing>元素進行聲明。例如,TestAspect中的doThrowing方法。

二、SpringAOP介紹

AspectJ介紹

需要spring的核心包外,還需要aspectjrt.jar、aspectjweaver.ja、cglib-nodep.jar幾個包。

         在ApplicationContext.xml中import進一個aop.xml配置,如下:

<?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:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:ehcache="http://www.springframework.org/schema/ehcache"
	xsi:schemaLocation="
     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
     http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-2.5.xsd
     http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
     http://www.springframework.org/schema/ehcache  http://www.springframework.org/schema/cache/springmodules-ehcache.xsd">
	
	<description>aop配置</description>

	<!-- 配置一個緩存攔截器對象,處理具體的同步緩存業務 -->
   <aop:aspectj-autoproxy />
   <aop:config proxy-target-class="true" />
</beans>

說明:這裏主要是基於註解方式實現AOP,具體實現類看後面內容。這裏的<aop:config proxy-target-class="true"/>這要是防止拋java.lang.IllegalArgumentException異常,原因是AOP使用的動態代理可以針對接口,也可以針對類。java的動態代理只能針對接口。在用Spring的AOP時,默認動態代理是針對接口的,而我們是針對類的,所以要加上proxy-target-class="true"。

多數據源配置

由於項目需要,需要配置動態數據源,現網的應用有可能要訪問雲平臺數據庫的需要。

         Dao.xml配置如下(這裏用到c3p0數據庫連接池,數據庫操作用springJDBC):

<?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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

	<!-- 數據庫配置(現網) -->
	<bean id="dataSource" destroy-method="close" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="jdbcUrl" value="${db.url}" />
        <property name="user" value="${db.user}" />
        <property name="password" value="${db.password}" />
        <property name="driverClass" value="${db.driver}" />
        <property name="minPoolSize" value="${db.minPoolSize}" />
        <property name="maxPoolSize" value="${db.maxPoolSize}" />
        <property name="maxStatements" value="${db.maxStatement}" />
        <property name="maxIdleTime" value="${db.maxIdleTime}" />
	</bean>
	
	<!-- 數據庫配置(雲平臺)-->
	<bean id="dataSource2" destroy-method="close" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="jdbcUrl" value="${db.url2}" />
        <property name="user" value="${db.user2}" />
        <property name="password" value="${db.password2}" />
        <property name="driverClass" value="${db.driver}" />
        <property name="minPoolSize" value="${db.minPoolSize}" />
        <property name="maxPoolSize" value="${db.maxPoolSize}" />
        <property name="maxStatements" value="${db.maxStatement}" />
        <property name="maxIdleTime" value="${db.maxIdleTime}" />
	</bean>
	
	<!-- 配置動態數據源 -->
	<bean id="dynamicDataSource" class="cn.qtone.xxt.base.aop.adapter.db.dynamic.DynamicDataSource">
		<!-- 通過key-value的形式來關聯數據源 -->
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry value-ref="dataSource" key="dataSource"></entry>
				<entry value-ref="dataSource2" key="dataSource2"></entry>
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="dataSource" />
	</bean>
	
	<!-- 配置數據源切換實現類 -->
	<bean id="dataSourceEntry" class="cn.qtone.xxt.base.aop.adapter.db.dynamic.DataSourceEntryImpl" />
	

    <!-- JdbcTemplate配置 -->
    <bean class="org.springframework.jdbc.core.JdbcTemplate">
		<constructor-arg ref="dynamicDataSource"></constructor-arg>
	</bean>
	<bean class="org.springframework.jdbc.core.simple.SimpleJdbcTemplate">
		<constructor-arg ref="dynamicDataSource"></constructor-arg>
	</bean>
	<bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
		<constructor-arg ref="dynamicDataSource"></constructor-arg>
	</bean>
	<bean class="org.springframework.jdbc.core.simple.SimpleJdbcInsert">
		<constructor-arg ref="dynamicDataSource"></constructor-arg>
	</bean>
</beans>

動態數據庫類如下結構(這裏借鑑了網上的實現):
DataSorceEntry.爲接口:
public interface DataSourceEntry {

	// 雲平臺數據源標誌
	public final static String YUN_SOURCE = "dataSource2";

	// 現網數據源標誌
	public final static String CURR_SOURCE = "dataSource";
	/**
	 * 還原數據源
	 * 
	 */
	public void restore();
	
	/**
	 * 切換數據源
	 */

	public void switchSource();
	/**
	 * 設置數據源
	 * 
	 * @param dataSource
	 */
	public void set(String source);

	/**
	 * 獲取數據源
	 * 
	 * @return String
	 */
	public String get();

	/**
	 * 清空數據源
	 */
	public void clear();
}

DataSourceEntryImpl爲DataSorceEntry的實現類:
public class DataSourceEntryImpl implements DataSourceEntry {
	private final static ThreadLocal<String> local = new ThreadLocal<String>();
	
	public void clear() {
		local.remove();
	}

	public String get() {
		return local.get();
	}

	public void restore() {
		local.set(null); // 設置null數據源
	}

	public void set(String source) {
		local.set(source);
	}

	public void switchSource() {
		if (DataSourceEntry.CURR_SOURCE.equals(get())) {
			set(DataSourceEntry.YUN_SOURCE);
		}else {
			set(DataSourceEntry.CURR_SOURCE);
		}
	}
}

DynamicDataSource則爲繼承AbstractRoutingDataSource(springjdbc的多數據源路由類)類,該類以DataSorceEntry的實例作爲數據源選擇類,以注入方式實現:
public class DynamicDataSource extends AbstractRoutingDataSource {

	@Autowired
	private DataSourceEntry dataSourceEntry;

	@Override
	protected Object determineCurrentLookupKey() {
		return this.dataSourceEntry.get();
	}

	@Resource
	public void setDataSourceEntry(DataSourceEntry dataSourceEntry) {
		this.dataSourceEntry = dataSourceEntry;
	}
}

改造實例

這裏以其中一個改造實進行說明,爲了簡單化,這裏就舉一個某個子系統的登錄驗證功能來說明,因爲該子系統在並行期間是不做割接的,所以用戶驗證需訪問兩個平臺的數據庫。

DaYiAspest.java:

@Component
@Aspect
public class DaYiAspest{
	@Autowired
	private DataSourceEntry dataSourceEntry;//動態數據源


	//配置切入點集合
	@Pointcut("execution(* cn.qtone.xxt.parentnew.kwfd.controller.*.dayi(..)) " +
			"|| execution(* cn.qtone.xxt.schoolnew.kjck.controller.*.dayi(..))" +
			"|| execution(* cn.qtone.xxt.studentnew.kwfd.controller.*.dayi(..))")
	public void pointcuts(){}
	/**
	 * 單點登陸 (切入替換原方法)
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws IOException
	 */
	@Around( value = "pointcuts()")
	public Object dayiLoginInit(ProceedingJoinPoint pjp) throws IOException {
		HttpServletRequest request = (HttpServletRequest) pjp.getArgs()[0];
		HttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1];
		Object obj = null;
		//執行方法前操作
		obj = pjp.proceed();// 執行原操作
		//執行原方法後操作
		return obj;
	}
}

說明:@Pointcut可以定義多個切入點集合,也可以直接@Around( “execution表達式"),這裏介紹一下execution表達式:

Spring AOP 用戶可能會經常使用execution pointcut designator。執行表達式的格式如下:

 

execution(modifiers-pattern?ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)除了返回類型模式(上面代碼片斷中的ret-type-pattern),名字模式和參數模式以外,所有的部分都是可選的。返回類型模式決定了方法的返回類型必須依次匹配一個連接點。你會使用的最頻繁的返回類型模式是 *,它代表了匹配任意的返回類型。一個全稱限定的類型名將只會匹配返回給定類型的方法。名字模式匹配的是方法名。你可以使用 * 通配符作爲所有或者部分命名模式。參數模式稍微有點複雜:() 匹配了一個不接受任何參數的方法,而 (..) 匹配了一個接受任意數量參數的方法(零或者更多)。模式 (*) 匹配了一個接受一個任何類型的參數的方法。模式 (*,String) 匹配了一個接受兩個參數的方法,第一個可以是任意類型,第二個則必須是String類型。請參見AspectJ編程指南的 Language Semantics 部分。

 

下面給出一些常見切入點表達式的例子。

 

任意公共方法的執行:

execution(public **(..))任何一個以“set”開始的方法的執行:

execution(*set*(..))AccountService 接口的任意方法的執行:

execution(*com.xyz.service.AccountService.*(..))定義在service包裏的任意方法的執行:

execution(*com.xyz.service.*.*(..))定義在service包或者子包裏的任意方法的執行:

execution(*com.xyz.service..*.*(..))在service包裏的任意連接點(在Spring AOP中只是方法執行):

within(com.xyz.service.*)在service包或者子包裏的任意連接點(在Spring AOP中只是方法執行):

within(com.xyz.service..*)實現了AccountService 接口的代理對象的任意連接點(在Spring AOP中只是方法執行):

this(com.xyz.service.AccountService)'this'在binding form中用的更多:- 請常見以下討論通知的章節中關於如何使得代理對象可以在通知體內訪問到的部分。

實現了 AccountService 接口的目標對象的任意連接點(在Spring AOP中只是方法執行):

target(com.xyz.service.AccountService)'target'在binding form中用的更多:- 請常見以下討論通知的章節中關於如何使得目標對象可以在通知體內訪問到的部分。

任何一個只接受一個參數,且在運行時傳入的參數實現了 Serializable 接口的連接點(在Spring AOP中只是方法執行)

args(java.io.Serializable)'args'在binding form中用的更多:- 請常見以下討論通知的章節中關於如何使得方法參數可以在通知體內訪問到的部分。

請注意在例子中給出的切入點不同於execution(* *(java.io.Serializable)): args只有在動態運行時候傳入參數是可序列化的(Serializable)才匹配,而execution 在傳入參數的簽名聲明的類型實現了 Serializable 接口時候匹配。

有一個 @Transactional 註解的目標對象中的任意連接點(在Spring AOP中只是方法執行)

@target(org.springframework.transaction.annotation.Transactional)'@target'也可以在binding form中使用:請常見以下討論通知的章節中關於如何使得annotation對象可以在通知體內訪問到的部分。

任何一個目標對象聲明的類型有一個@Transactional 註解的連接點(在Spring AOP中只是方法執行)

@within(org.springframework.transaction.annotation.Transactional)'@within'也可以在bindingform中使用:- 請常見以下討論通知的章節中關於如何使得annotation對象可以在通知體內訪問到的部分。

任何一個執行的方法有一個@Transactional annotation的連接點(在Spring AOP中只是方法執行)

@annotation(org.springframework.transaction.annotation.Transactional)'@annotation'也可以在binding form中使用:- 請常見以下討論通知的章節中關於如何使得annotation對象可以在通知體內訪問到的部分。

任何一個接受一個參數,並且傳入的參數在運行時的類型實現了 @Classified annotation的連接點(在Spring AOP中只是方法執行)

 

@args(com.xyz.security.Classified)'@args'也可以在bindingform中使用:- 請常見以下討論通知的章節中關於如何使得annotation對象可以在通知體內訪問到的部分。

 

還有這裏我主要用了@Around的註解,其實還有好幾種方式,它們的作用各不相同:

@Before:前置通知,在切點方法集合執行前,執行前置通知;

@After:後置通知,在切點方法集合執行後,執行後置通知;

@AfterReturning:後置通知,在切點方法集合執行後返回結果後,執行後置通知;

@Around :環繞通知(##環繞通知的方法中一定要有ProceedingJoinPoint參數,與Filter中的doFilter方法類似)

@AfterThrowing :異常通知,切點方法集合執行拋異常後執行處理;

具體實例可以看:http://blog.sina.com.cn/s/blog_7ffb8dd501014am6.html

在這裏遇到一個問題:因爲原方法是:public ModelAndView dayiLoginInit(HttpServletRequest request)只有一個參數,所以
上面的語句:
HttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1];
是報數據越界的異常的,因爲有些模是需要用到response 參數的,爲了解決這個問題,我在網上找了解決方案,即配置過濾器,並運用線程變量ThreadLocal來實現:
過濾器:
import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @Description: 雲平臺割接過濾器
* @author 柯穎波
* @date 2014-4-1 下午03:04:34 
* @version v1.0
 */
public class GetContextFilter implements Filter {

	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
			ServletException {
		SysContext.setRequest((HttpServletRequest) request);
		SysContext.setResponse((HttpServletResponse) response);
		chain.doFilter(request, response);
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		// TODO Auto-generated method stub
	}
}

SysContext 存儲變量:

package cn.qtone.xxt.base.aop;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description: 系統請求上下文(主要存儲request及response對象)
 * @author 柯穎波
 * @date 2014-4-1 下午02:49:58
 * @version v1.0
 */
public class SysContext {
	private static ThreadLocal<HttpServletRequest> requestLocal = new ThreadLocal<HttpServletRequest>();
	private static ThreadLocal<HttpServletResponse> responseLocal = new ThreadLocal<HttpServletResponse>();

	/**
	* @Description: 獲取HttpServletRequest對象
	* @return    設定文件
	 */
	public static HttpServletRequest getRequest() {
		return (HttpServletRequest) requestLocal.get();
	}

	/**
	* @Description: 設置HttpServletRequest對象
	* @return    設定文件
	 */
	public static void setRequest(HttpServletRequest request) {
		requestLocal.set(request);
	}
	/**
	* @Description: 獲取HttpServletResponse對象
	* @return    設定文件
	 */
	public static HttpServletResponse getResponse() {
		return (HttpServletResponse) responseLocal.get();
	}
	/**
	* @Description: 設置HttpServletResponse對象
	* @return    設定文件
	 */
	public static void setResponse(HttpServletResponse response) {
		responseLocal.set(response);
	}
	
	/**
	* @Description: 清除配置相關變量
	 */
	public static void clear() {
		requestLocal.remove();
		responseLocal.remove();
	}
}
那麼,只要配好過濾器,那麼上面的request,response對象可以這樣獲取:

HttpServletResponse response = SysContext.getResponse();
HttpServletRequest request =  SysContext.getRequest();

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章