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>
動態數據庫類如下結構(這裏借鑑了網上的實現):
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.htmlHttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1];
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();