一、AOP術語
描述切面的常用術語有通知(advice)、切點(pointcut)和連接點(joinpoint)通知(Advice):
Spring切面可以應用5種類型的通知:
1、前置通知(Before):在目標方法被調用之前調用通知功能;
2、後置通知(After):在目標方法完成之後調用通知,此時不會關心方法的輸出是什麼;
3、返回通知(Afert-returning):在目標方法成功執行之後調用通知;
4、異常通知(After-throwing):在目標方法拋出異常後調用通知;
5、環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行爲;
連接點(joinpoint):
連接點是在應用在執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程中,並添加新的行爲。
切點(pointcut):我們通常使用明確的類名和方法名稱,或是利用正則表達式定義所匹配的類和方法名稱來指定切點。
二、Spring對AOP的支持
Spring提供了4種類型的AOP支持
1、基於代理的經典SpringAOP;
2、純pojo切面
3、@AspectJ註解驅動的切面
4、注入式AspectJ切面
三、通過切點來選擇連接點
切點用於準確定位應該在什麼地方應用切面的通知。通知和切點是切面的最基本元素。
Spring藉助AspectJ的切點表達式語言來定義Spring切面
四、編寫切點
1、execution
execution(<修飾符模式>? <返回類型模式> <方法名模式>(<參數模式>) <異常模式>?) 除了返回類型模式、方法名模式和參數模式外,其它項都是可選的;
2、args()和@args()
args()函數的入參是類名,@args()函數的入參必須是註解類的類名。雖然args()允許在類名後使用+通配符後綴,但該通配符在此處沒有意義:添加和不添加效果都一樣;
3、@annotation、@within()和@target()
@annotation、@within()和@target()函數的入參必須都是註解類的類名。@annotation
是應用於java類中的某方法上的註解而@within()和@target()應用於java類上的註解;
爲了闡述Spring中的切面,我們需要有個主題來定義切面的切點。爲此我們定義一個IPersonBeanService接口
public interface IPersonBeanService {
void save();
void testDependency();
}
假設我們想編寫IPersonBeanService的save()方法觸發通知
execution1(*2 com.cn.service.IPersonBeanService3. save4.(..)5)
1-execution:定義方法切點的指示器
2-*:任意返回值
3-com.cn.service.IPersonBeanService:方法所在類
4-save:方法名稱
5-(..):使用任意參數
我們可以使用“&&”操作符把多個指示器連接在一起形成and關係(切點必須匹配所有的指示器),使用“||”操作符把指示器連接在一起形成or關係,而是用“!”操作符來辨識非操作
在xml中則使用and代替“&&”,or代替“||”,not代替“!”。
在切點中選擇bean
除了上述表中所有的指示器外,Spring還引入了一個新的bean()指示器,他允許我們在切點表達式中使用bean的ID來標識bean。Bean()使用bean ID或bean名稱作爲參數來限定切點只能匹配特定的bean。
Execution定義詳解:
execution(public * *(..))
匹配所有目標類的public方法,第一個代表返回類型,第二個代表方法名,而..代表任意入參的方法;
execution(* *To(..))l
匹配目標類所有以To爲後綴的方法。第一個*代表返回類型,而*To代表任意以To爲後綴的方法;
execution(* com.cn.Waiter.*(..))l
匹配Waiter接口的所有方法,第一個代表返回任意類型,com.cn.Waiter.代表Waiter接口中的所有方法;
execution(* com.cn.Waiter+.*(..))
匹配Waiter接口及其所有實現類的方法
在類名模式串中,“.”表示包下的所有類,而“..”表示包、子孫包下的所有類。execution(* com.cn.*(..))匹配com.cn包下所有類的所有方法;
execution(* com.cn..*(..))
匹配com.cn包、子孫包下所有類的所有方法,如com.cn.dao,com.cn.servier以及 com.cn.dao.user包下的所有類的所有方法都匹配。“..”出現在類名中時,後面必須跟“*”,表示包、子孫包下的所有類;
execution(* com...*Dao.find(..))
匹配包名前綴爲com的任何包下類名後綴爲Dao的方法,方法名必須以find爲前綴。如com.cn.UserDao#findByUserId()、com.cn.dao.ForumDao#findById()的方法都匹配切點。
通過方法入參定義切點
切點表達式中方法入參部分比較複雜,可以使用“”和“..”通配符,其中“”表示任意類型的參數,而“..”表示任意類型參數且參數個數不限。
execution(* joke(String,int)))
匹配joke(String,int)方法,且joke()方法的第一個入參是String,第二個入參是int。它匹配 NaughtyWaiter#joke(String,int)方法。如果方法中的入參類型是java.lang包下的類,可以直接使用類名,否則必須使用全限定類名,如joke(java.util.List,int);
execution(* joke(String,*)))l
匹配目標類中的joke()方法,該方法第一個入參爲String,第二個入參可以是任意類型,如joke(String s1,String s2)和joke(String s1,double d2)都匹配,但joke(String s1,double d2,String s3)則不匹配;
execution(* joke(String,..)))
匹配目標類中的joke()方法,該方法第 一個入參爲String,後面可以有任意個入參且入參類型不限,如joke(String s1)、joke(String s1,String s2)和joke(String s1,double d2,String s3)都匹配。
execution(* joke(Object+)))
匹配目標類中的joke()方法,方法擁有一個入參,且入參是Object類型或該類的子類。它匹配joke(String s1)和joke(Client c)。如果我們定義的切點是execution(* joke(Object)),則只匹配joke(Object object)而不匹配joke(String cc)或joke(Client c)。
五、使用註解定義切面
1、Spring使用AspecJ註解來聲明通知方法
@After:通知方法會在目標方法返回或拋出異常後調用
@AfterReturning:通知方法會在目標方法成功返回後調用
@AfterThrowing:通知方法會在目標方法拋出異常後調用
@Around:通知方法會將目標方法封鎖起來
@Before:通知方法會在目標方法調用之前調用
@Pointcut:定義切點。
用法示例:
在AspectLog中,excudeService()方法使用了@Pointcut註解。爲@Pointcut註解設置的值是一個切點表達式。
excudeService()方法的內容並不重要,在這裏他實際上應該是空的。其實該方法本身只是一個標識,供@Pointcut註解依附。
簡單的小示例:
@Aspect
@Configuration
public class AspectLog {
@Pointcut("execution(* service.IPerformanceService.*(..))")
public void excudeService(){
}
@Before("excudeService()")
public void doBeforeInServiceLayer(){
System.out.println("before.....");
}
@Around("excudeService()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("before proceed");
Object obj = pjp.proceed();
System.out.println("after proceed");
return obj;
}
@After("excudeService()")
public void doAfterInServiceLayer(JoinPoint joinPoint) {
System.out.println("after.....");
}
}
public interface IPerformanceService {
public String testPerformence();
}
@Service("performenceService")
public class PerformenceServiceImpl implements IPerformanceService{
@cold
@Override
public String testPerformence(){
return "performence";
}
}
@Configuration
@ComponentScan(basePackageClasses=IPerformanceService.class)
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class AppConfig {
}
@EnableAspectJAutoProxy(proxyTargetClass=true):啓用AspectJ自動代理
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={AppConfig.class,AspectLog.class})
public class TestSpringBean_Aspect {
@Autowired
private IPerformanceService performanceService;
@Test
public void test() {
System.out.println(performanceService.testPerformence()+"~~~");
}
}
執行打印結果
before proceed
before…..
after proceed
after…..
performence~~~
由打印結果可得出執行順序:Around方法的pjp.proceed()執行之前 —> before方法 —> Around方法的pjp.proceed()執行之後 —> after方法
在XML中聲明切面
1、在xml頭部聲明AOP的命名空間
<?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:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
</beans>
AOP配置元素:用途
<aop:advisor>:
定義AOP通知器
<aop:after>:
定義AOP後置通知(不管被通知的方法是否執行成功)
<aop:after-returning>:
定義AOP返回通知
<aop:after-throwing>:
定義AOP異常通知
<aop:around>:
定義AOP環繞通知
<aop:aspect>:
定義一個切面
<aop:aspectj-autoproxy>:
啓用@AspectJ註解驅動的切面
<aop:before>:
定義一個AOP前置通知
<aop:config>:
頂層的AOP配置元素。大多數的<aop:*>
元素必須包含在<aop:config>
元素內
<aop:declare-parents>:
以透明方式爲被通知的對象引入額外的接口
<aop:pointcut>:
定義一個切點
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:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<bean id="performanceService" class="service.PerformenceServiceImpl"></bean>
<bean id="aspectLogBean" class="Config.AspectLogBean"></bean>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<aop:config>
<aop:aspect ref="aspectLogBean">
<aop:pointcut expression="execution(* service.IPerformanceService.*(..))" id="excudeService"/>
<aop:before method="doBeforeInServiceLayer" pointcut-ref="excudeService"/>
<aop:after method="doAfterInServiceLayer" pointcut-ref="excudeService"/>
<aop:around method="doAround" pointcut-ref="excudeService"/>
</aop:aspect>
</aop:config>
</beans>
關於SpringAOP配置元素,第一個需要注意的事項是大多數的AOP配置元素必須在<aop:config>
元素的上下文內使用。這條規則有幾種意外場景,但是把bean聲明爲一個切面時,
我們總是從<aop:config>
元素開始配置的。
在<aop:config>
元素內,我們可以聲明一個或多個通知器、切面或者切點。
上述,我們使用<aop:aspect>
元素生命了一個簡單的切面,ref元素應用了一個POJO Bean,該bean實現了切面的功能。
<aop:pointcut>
定義了一個切點,它被多個通知通過pointcut-ref引用
jar包:
aopalliance.jar 、 aspectjweaver-1.8.9.jar、spring-aop-4.1.6.RELEASE.jar、spring-aspects-4.1.6.RELEASE.jar
項目中實際應用示例:
一、定義切面:用於做頁面商品訪問量統計
@Aspect
@Component
public class StatisticsAspect {
@Autowired
private IISpringRedis springRedisService;
private Logger logger = LoggerFactory.getLogger(StatisticsAspect.class);
private Object[] paramsArray = null;
@Around("@annotation(com.isgo.gallerydao.core.support.annotation.Statistics)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
paramsArray = joinPoint.getArgs();
Object result = joinPoint.proceed();
return result;
}
/**
* @Title: doAfterInServiceLayer
* @Description: 將點擊量存於緩存
* @param statistics 參數
* @return void 返回類型
* @throws
*/
@After(value = "@annotation(statistics)", argNames = "statistics")
public void doAfterInServiceLayer(Statistics statistics) {
try {
if(null!=paramsArray && paramsArray.length>=statistics.paramterPlace()){
String foreignKey = paramsArray[statistics.paramterPlace()-1].toString();
String key = new StringBuffer(RedisKeyPrefixEnum.ART_EXT.getValue()).append(foreignKey).append("_")
.append(statistics.type().getValue()).toString();
ArtExt artExt = springRedisService.get(key, ArtExt.class);
if(null==artExt){
artExt = new ArtExt();
}
Integer count = artExt.getExt01()==null?0:artExt.getExt01();
artExt.setForeignKey(foreignKey);
artExt.setType(statistics.type().getValue());
artExt.setExt01(count+1);
springRedisService.saveOrUpdate(key, artExt);
ArtExt artExt_new = springRedisService.get(key, ArtExt.class);
logger.info("foreignKey:{},type:{},hits:{}",artExt_new.getForeignKey(),
artExt_new.getType(),artExt_new.getExt01());
}
} catch (Exception e) {
StackTraceElement[] trace = new StackTraceElement[1];
trace[0] = e.getStackTrace()[0];
e.setStackTrace(trace);
StringBuffer sb = new StringBuffer("exception---");
if(null!=trace[0]){
sb.append("className:{").append(trace[0].getClassName()).append("} ;")
.append("methodName:{").append(trace[0].getMethodName()).append("} ;")
.append("lineNumber:{").append(trace[0].getLineNumber()).append("} ;")
.append("cause:{").append(e).append("}");
}
logger.info("save hits fail:"+sb.toString());
}
}
}
二、定義切面:用於做web請求切面日誌
@Aspect //定義一個切面
@Configuration
public class LogRecordAspect {
private Logger logger = LoggerFactory.getLogger(LogRecordAspect.class);
private String requestPath = null ; // 請求地址
private String encoding=null;
private String httpMethod =null;
private Map<String,String> headerMap = new HashMap<String,String>();
private Map<?, ?> paramter = null ; // 傳入參數
private Map<String, Object> outputParamMap = null; // 存放輸出結果
private long startTimeMillis = 0; // 開始時間
private long endTimeMillis = 0; // 結束時間
// 定義切點Pointcut
@Pointcut("execution(* com.gallery.*.*controller..*(..))")
public void excudeService() {
}
/**
*
* @Title:doBeforeInServiceLayer
* @Description: 方法調用前觸發
* 記錄開始時間
* @author shaojian.yu
* @date 2014年11月2日 下午4:45:53
* @param joinPoint
*/
@Before("excudeService()")
public void doBeforeInServiceLayer(JoinPoint joinPoint) {
startTimeMillis = System.currentTimeMillis(); // 記錄方法開始執行的時間
}
@Around("excudeService()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// 獲取輸入字符編碼
encoding = request.getCharacterEncoding();
// 獲取請求地址
requestPath = request.getRequestURL().toString();
httpMethod =request.getMethod();
paramter = request.getParameterMap();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
headerMap.put(key, value);
}
if ("POST".equals(httpMethod)) {
Object[] paramsArray = pjp.getArgs();
try {
paramter = argsArrayToString(paramsArray);
} catch (Exception e) {
}
}
// 執行完方法的返回值:調用proceed()方法,就會觸發切入點方法執行
outputParamMap = new HashMap<String, Object>();
Object result = null;
try {
result = pjp.proceed();
outputParamMap.put("result", result);
} catch (Exception e) {
StackTraceElement[] trace = new StackTraceElement[1];
trace[0] = e.getStackTrace()[0];
e.setStackTrace(trace);
StringBuffer sb = new StringBuffer("exception---");
if(null!=trace[0]){
sb.append("className:{").append(trace[0].getClassName()).append("} ;")
.append("methodName:{").append(trace[0].getMethodName()).append("} ;")
.append("lineNumber:{").append(trace[0].getLineNumber()).append("} ;")
.append("cause:{").append(e).append("}");
}
outputParamMap.put("result", sb.toString());
throw e;
}
return result;
}
/**
*
* @Title:doAfterInServiceLayer
* @Description: 方法調用後觸發
* 記錄結束時間
* @author shaojian.yu
* @date 2014年11月2日 下午4:46:21
* @param joinPoint
*/
@After("excudeService()")
public void doAfterInServiceLayer(JoinPoint joinPoint) {
endTimeMillis = System.currentTimeMillis(); // 記錄方法執行完成的時間
this.printOptLog();
}
/**
*
* @Title:printOptLog
* @Description: 輸出日誌
* @author shaojian.yu
* @date 2014年11月2日 下午4:47:09
*/
private void printOptLog() {
String optTime = DateUtil.getSysDateTimeString();
logger.info("\nAddress: "+ requestPath+"\n"
+"Encoding: "+encoding+"\n"
+"Http-Method: "+httpMethod+"\n"
+"Op_time: "+optTime+"\n"
+"Pro_time: "+(endTimeMillis - startTimeMillis) +"ms\n"
+"Paramter: "+JSON.toJSONString(paramter) +"\n"
+"Header: "+JSON.toJSONString(headerMap)+"\n"
+"result: "+JSON.toJSONString(outputParamMap) +"\n"
+"-------------------------------------------------------------------------------------------------");
}
/**
* 請求參數拼裝
*
* @param paramsArray
* @return
*/
@SuppressWarnings("rawtypes")
private Map<?,?> argsArrayToString(Object[] paramsArray) {
Object jsonObj=null;
Map params = new HashMap();
if (paramsArray != null && paramsArray.length > 0) {
for (int i = 0; i < paramsArray.length; i++) {
jsonObj = JSON.toJSON(paramsArray[i]);
params = JSON.parseObject(JSON.toJSONString(jsonObj));
}
}
return params;
}
}
三、切面日誌:service中方法調用切面日誌
@Aspect
@Component
public class SystemLogAspect {
private Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
private String method =null;
private String paramter = null ; // 傳入參數
private Map<String, Object> outputParamMap = null; // 存放輸出結果
private long startTimeMillis = 0; // 開始時間
private long endTimeMillis = 0; // 結束時間
private String className = null;
// 定義切點Pointcut
@Pointcut("@within(com.isgo.gallerydao.core.support.annotation.Log)")
public void excudeService() {
}
/**
*
* @Title:doBeforeInServiceLayer
* @Description: 方法調用前觸發
* 記錄開始時間
* @author chen.danwei
* @date 2014年11月2日 下午4:45:53
* @param joinPoint
*/
@Before("excudeService()")
public void doBeforeInServiceLayer(JoinPoint joinPoint) {
startTimeMillis = System.currentTimeMillis(); // 記錄方法開始執行的時間
}
@Around("excudeService()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
className = joinPoint.getSignature().toLongString();
method = joinPoint.getSignature().getName();
Object[] paramsArray = joinPoint.getArgs();
paramter = argsArrayToString(paramsArray);
outputParamMap = new HashMap<String, Object>();
Object result =null;
try {
result = joinPoint.proceed();
outputParamMap.put("result", result);
} catch (Throwable e) {
StackTraceElement[] trace = new StackTraceElement[1];
trace[0] = e.getStackTrace()[0];
e.setStackTrace(trace);
StringBuffer sb = new StringBuffer("exception---");
if(null!=trace[0]){
sb.append("className:{").append(trace[0].getClassName()).append("} ;")
.append("methodName:{").append(trace[0].getMethodName()).append("} ;")
.append("lineNumber:{").append(trace[0].getLineNumber()).append("} ;")
.append("cause:{").append(e).append("}");
}
outputParamMap.put("result", sb.toString());
throw e;
}
return result;
}
@After("excudeService()")
public void doAfterInServiceLayer(JoinPoint joinPoint) {
endTimeMillis = System.currentTimeMillis(); // 記錄方法執行完成的時間
this.printOptLog();
}
/**
* @Title:printOptLog
* @Description: 輸出日誌
* @author chen.danwei
* @date 2014年11月2日 下午4:47:09
*/
private void printOptLog() {
String optTime = DateTimeUtil.getYYYYMMddHHmmss(new Date());
try {
logger.info("\nClass: "+className+"\n"
+"Method: "+method+"\n"
+"Op_time: "+optTime+"\n"
+"Pro_time: "+(endTimeMillis - startTimeMillis) +"ms\n"
+"Paramter: "+paramter+"\n"
+"result: "+JSON.json(outputParamMap) +"\n"
+"-------------------------------------------------------------------------------------------------");
} catch (IOException e) {
logger.info("\nClass: "+className+"\n"
+"Method: "+method+"\n"
+"Op_time: "+optTime+"\n"
+"Pro_time: "+(endTimeMillis - startTimeMillis) +"ms\n"
+"paramter or outputParamMap turn to json String exception \n"
+"-------------------------------------------------------------------------------------------------");
}
}
/**
* 請求參數拼裝
*
* @param paramsArray
* @return
*/
private String argsArrayToString(Object[] paramsArray) {
StringBuffer sb=new StringBuffer();
if (paramsArray != null && paramsArray.length > 0) {
for (int i = 0; i < paramsArray.length; i++) {
try {
sb.append(JSON.json(paramsArray[i])+";");
} catch (IOException e) {
logger.info("argsArrayToString method exception");
}
}
}
return sb.toString();
}
}