Spring AOP面向切面編程,可以用來配置事務、做日志、權限驗證、在用戶請求時做一些處理等等。用@Aspect做一個切面,就可以直接實現。
1.首先定義一個切面類,加上@Component @Aspect這兩個注解
@Component @Aspect public class LogAspect { private static final Logger logger = LoggerFactory.getLogger(LogAspect.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); }
2.定義切點
1 private final String POINT_CUT = "execution(public * com.xhx.springboot.controller.*.*(..))"; 2 3 @Pointcut(POINT_CUT) 4 public void pointCut(){}
切點表達式中,..兩個點表明多個,*代表一個, 上面表達式代表切入com.xhx.springboot.controller包下的所有類的所有方法,方法參數不限,返回類型不限。 其中訪問修飾符可以不寫,不能用*,,第一個*代表返回類型不限,第二個*表示所有類,第三個*表示所有方法,..兩個點表示方法里的參數不限。 然后用@Pointcut切點注解,想在一個空方法上面,一會兒在Advice通知中,直接調用這個空方法就行了,也可以把切點表達式卸載Advice通知中的,單獨定義出來主要是為了好管理。
3.Advice,通知增強,主要包括五個注解Before,After,AfterReturning,AfterThrowing,Around,下面代碼中關鍵地方都有注釋,我都列出來了。
@Before 在切點方法之前執行
@After 在切點方法之后執行
@AfterReturning 切點方法返回后執行
@AfterThrowing 切點方法拋異常執行
@Around 屬於環繞增強,能控制切點執行前,執行后,,用這個注解后,程序拋異常,會影響@AfterThrowing這個注解
1 package com.xhx.springboot.config; 2 3 import com.fasterxml.jackson.core.JsonProcessingException; 4 import com.fasterxml.jackson.databind.ObjectMapper; 5 import org.aspectj.lang.JoinPoint; 6 import org.aspectj.lang.ProceedingJoinPoint; 7 import org.aspectj.lang.Signature; 8 import org.aspectj.lang.annotation.*; 9 import org.aspectj.lang.reflect.SourceLocation; 10 import org.slf4j.Logger; 11 import org.slf4j.LoggerFactory; 12 import org.springframework.stereotype.Component; 13 import org.springframework.web.context.request.RequestContextHolder; 14 import org.springframework.web.context.request.ServletRequestAttributes; 15 16 import javax.servlet.http.HttpServletRequest; 17 import java.util.Arrays; 18 19 @Component 20 @Aspect 21 public class LogAspect { 22 23 private static final Logger logger = LoggerFactory.getLogger(LogAspect.class); 24 private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 25 26 private final String POINT_CUT = "execution(public * com.xhx.springboot.controller.*.*(..))"; 27 28 @Pointcut(POINT_CUT) 29 public void pointCut(){} 30 31 @Before(value = "pointCut()") 32 public void before(JoinPoint joinPoint){ 33 logger.info("@Before通知執行"); 34 //獲取目標方法參數信息 35 Object[] args = joinPoint.getArgs(); 36 Arrays.stream(args).forEach(arg->{ // 大大 37 try { 38 logger.info(OBJECT_MAPPER.writeValueAsString(arg)); 39 } catch (JsonProcessingException e) { 40 logger.info(arg.toString()); 41 } 42 }); 43 44 45 //aop代理對象 46 Object aThis = joinPoint.getThis(); 47 logger.info(aThis.toString()); //com.xhx.springboot.controller.HelloController@69fbbcdd 48 49 //被代理對象 50 Object target = joinPoint.getTarget(); 51 logger.info(target.toString()); //com.xhx.springboot.controller.HelloController@69fbbcdd 52 53 //獲取連接點的方法簽名對象 54 Signature signature = joinPoint.getSignature(); 55 logger.info(signature.toLongString()); //public java.lang.String com.xhx.springboot.controller.HelloController.getName(java.lang.String) 56 logger.info(signature.toShortString()); //HelloController.getName(..) 57 logger.info(signature.toString()); //String com.xhx.springboot.controller.HelloController.getName(String) 58 //獲取方法名 59 logger.info(signature.getName()); //getName 60 //獲取聲明類型名 61 logger.info(signature.getDeclaringTypeName()); //com.xhx.springboot.controller.HelloController 62 //獲取聲明類型 方法所在類的class對象 63 logger.info(signature.getDeclaringType().toString()); //class com.xhx.springboot.controller.HelloController 64 //和getDeclaringTypeName()一樣 65 logger.info(signature.getDeclaringType().getName());//com.xhx.springboot.controller.HelloController 66 67 //連接點類型 68 String kind = joinPoint.getKind(); 69 logger.info(kind);//method-execution 70 71 //返回連接點方法所在類文件中的位置 打印報異常 72 SourceLocation sourceLocation = joinPoint.getSourceLocation(); 73 logger.info(sourceLocation.toString()); 74 //logger.info(sourceLocation.getFileName()); 75 //logger.info(sourceLocation.getLine()+""); 76 //logger.info(sourceLocation.getWithinType().toString()); //class com.xhx.springboot.controller.HelloController 77 78 ///返回連接點靜態部分 79 JoinPoint.StaticPart staticPart = joinPoint.getStaticPart(); 80 logger.info(staticPart.toLongString()); //execution(public java.lang.String com.xhx.springboot.controller.HelloController.getName(java.lang.String)) 81 82 83 //attributes可以獲取request信息 session信息等 84 ServletRequestAttributes attributes = 85 (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 86 HttpServletRequest request = attributes.getRequest(); 87 logger.info(request.getRequestURL().toString()); //http://127.0.0.1:8080/hello/getName 88 logger.info(request.getRemoteAddr()); //127.0.0.1 89 logger.info(request.getMethod()); //GET 90 91 logger.info("before通知執行結束"); 92 } 93 94 95 /** 96 * 后置返回 97 * 如果第一個參數為JoinPoint,則第二個參數為返回值的信息 98 * 如果第一個參數不為JoinPoint,則第一個參數為returning中對應的參數 99 * returning:限定了只有目標方法返回值與通知方法參數類型匹配時才能執行后置返回通知,否則不執行, 100 * 參數為Object類型將匹配任何目標返回值 101 */ 102 @AfterReturning(value = POINT_CUT,returning = "result") 103 public void doAfterReturningAdvice1(JoinPoint joinPoint,Object result){ 104 logger.info("第一個后置返回通知的返回值:"+result); 105 } 106 107 @AfterReturning(value = POINT_CUT,returning = "result",argNames = "result") 108 public void doAfterReturningAdvice2(String result){ 109 logger.info("第二個后置返回通知的返回值:"+result); 110 } 111 //第一個后置返回通知的返回值:姓名是大大 112 //第二個后置返回通知的返回值:姓名是大大 113 //第一個后置返回通知的返回值:{name=小小, id=1} 114 115 116 /** 117 * 后置異常通知 118 * 定義一個名字,該名字用於匹配通知實現方法的一個參數名,當目標方法拋出異常返回后,將把目標方法拋出的異常傳給通知方法; 119 * throwing:限定了只有目標方法拋出的異常與通知方法相應參數異常類型時才能執行后置異常通知,否則不執行, 120 * 對於throwing對應的通知方法參數為Throwable類型將匹配任何異常。 121 * @param joinPoint 122 * @param exception 123 */ 124 @AfterThrowing(value = POINT_CUT,throwing = "exception") 125 public void doAfterThrowingAdvice(JoinPoint joinPoint,Throwable exception){ 126 logger.info(joinPoint.getSignature().getName()); 127 if(exception instanceof NullPointerException){ 128 logger.info("發生了空指針異常!!!!!"); 129 } 130 } 131 132 @After(value = POINT_CUT) 133 public void doAfterAdvice(JoinPoint joinPoint){ 134 logger.info("后置通知執行了!"); 135 } 136 137 /** 138 * 環繞通知: 139 * 注意:Spring AOP的環繞通知會影響到AfterThrowing通知的運行,不要同時使用 140 * 141 * 環繞通知非常強大,可以決定目標方法是否執行,什么時候執行,執行時是否需要替換方法參數,執行完畢是否需要替換返回值。 142 * 環繞通知第一個參數必須是org.aspectj.lang.ProceedingJoinPoint類型 143 */ 144 @Around(value = POINT_CUT) 145 public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){ 146 logger.info("@Around環繞通知:"+proceedingJoinPoint.getSignature().toString()); 147 Object obj = null; 148 try { 149 obj = proceedingJoinPoint.proceed(); //可以加參數 150 logger.info(obj.toString()); 151 } catch (Throwable throwable) { 152 throwable.printStackTrace(); 153 } 154 logger.info("@Around環繞通知執行結束"); 155 return obj; 156 } 157 }
執行前后順序是這樣
1 : @Around環繞通知 2 : @Before通知執行 3 : @Before通知執行結束 4 : @Around環繞通知執行結束 5 : @After后置通知執行了! 6 : @AfterReturning第一個后置返回通知的返回值:18
org.aspectj.lang.JoinPoint : 方法中的參數JoinPoint為連接點對象,它可以獲取當前切入的方法的參數、代理類等信息,因此可以記錄一些信息,驗證一些信息等。
org.aspectj.lang.ProceedingJoinPoint: 為JoinPoint的子類,多了兩個方法
public Object proceed() throws Throwable; //調用下一個advice或者執行目標方法,返回值為目標方法返回值,因此可以通過更改返回值,修改方法的返回值
public Object proceed(Object[] args) throws Throwable; //參數為目標方法的參數 因此可以通過修改參數改變方法入參
可以用下面的方式獲取request、session等對象
1 //attributes可以獲取request信息 session信息等 2 ServletRequestAttributes attributes = 3 (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 4 HttpServletRequest request = attributes.getRequest();
下面介紹一下切點表達式:
1.execution(方法修飾符 返回類型 方法全限定名(參數)) 主要用來匹配整個方法簽名和返回值的
"execution(public * com.xhx.springboot.controller.*.*(..))"
*只能匹配一級路徑
..可以匹配多級,可以是包路徑,也可以匹配多個參數
+ 只能放在類后面,表明本類及所有子類
還可以按下面這么玩,所有get開頭的,第一個參數是Long類型的
@Pointcut("execution(* *..get*(Long,..))")
2. within(類路徑) 用來限定類,同樣可以使用匹配符
下面用來表示com.xhx.springboot包及其子包下的所有類方法
"within(com.xhx.springboot..*)"
3. this與target
this與target在用法上有些重合,理解上有對比性。
this表示當前切入點表達式所指代的方法的對象的實例,即代理對象是否滿足this類型
target表示當前切入點表達式所指代的方法的目標對象的實例 即是否是為target類做的代理
如果當前對象生成的代理對象符合this指定的類型,則進行切面,target是匹配業務對象為指定類型的類,則進行切面。
生成代理對象時會有兩種方法,一個是CGLIB一個是jdk動態代理。
用下面三個例子進行說明:
this(SomeInterface)或target(SomeInterface):這種情況下,無論是對於Jdk代理還是Cglib代理,其目標對象和代理對象都是實現SomeInterface接口的(Cglib生成的目標對象的子類也是實現了SomeInterface接口的),因而this和target語義都是符合的,此時這兩個表達式的效果一樣;
this(SomeObject)或target(SomeObject),這里SomeObject沒實現任何接口:這種情況下,Spring會使用Cglib代理生成SomeObject的代理類對象,由於代理類是SomeObject的子類,子類的對象也是符合SomeObject類型的,因而this將會被匹配,而對於target,由於目標對象本身就是SomeObject類型,因而這兩個表達式的效果一樣;
this(SomeObject)或target(SomeObject),這里SomeObject實現了某個接口:對於這種情況,雖然表達式中指定的是一種具體的對象類型,但由於其實現了某個接口,因而Spring默認會使用Jdk代理為其生成代理對象,Jdk代理生成的代理對象與目標對象實現的是同一個接口,但代理對象與目標對象還是不同的對象,由於代理對象不是SomeObject類型的,因而此時是不符合this語義的,而由於目標對象就是SomeObject類型,因而target語義是符合的,此時this和target的效果就產生了區別;這里如果強制Spring使用Cglib代理,因而生成的代理對象都是SomeObject子類的對象,其是SomeObject類型的,因而this和target的語義都符合,其效果就是一致的。
4.args(paramType)
args無論其類路徑或者是方法名是什么,表達式的作用是匹配指定參數類型和指定參數數量的方法,類型用全路徑
args(java.lang.String,..,java.lang.Integer)
5.@within(annotationType) 匹配帶有指定注解的類,,within為配置指定類型的類實例
下面匹配含有 @Component注解的類
"@within(org.springframework.stereotype.Component)"
6.@annotation(annotationType) 匹配帶有指定注解的方法
7.@args(annotationType)
@args表示使用指定注解標注的類作為某個方法的參數時該方法將會被匹配
可以使用&&、||、!、三種運算符來組合切點表達式,表示與或非的關系。
@Around(value = "pointcut1() || pointcut2()")
原文鏈接:https://blog.csdn.net/u012326462/article/details/82529835