1. 程式人生 > 實用技巧 >【SpringBoot】SpingBoot整合AOP

【SpringBoot】SpingBoot整合AOP

說起spring,我們知道其最核心的兩個功能就是AOP(面向切面)和IOC(控制反轉),這邊文章來總結一下SpringBoot如何整合使用AOP。

一、示例應用場景:對所有的web請求做切面來記錄日誌。

1、pom中引入SpringBoot的web模組和使用AOP相關的依賴:

其中:
cglib包是用來動態代理用的,基於類的代理;
aspectjrt和aspectjweaver是與aspectj相關的包,用來支援切面程式設計的;
aspectjrt包是aspectj的runtime包;
aspectjweaver是aspectj的織入包;

2、實現一個簡單的web請求入口(實現傳入name引數,返回“hello xxx”的功能):

注意:在完成了引入AOP依賴包後,一般來說並不需要去做其他配置。使用過Spring註解配置方式的人會問是否需要在程式主類中增加@EnableAspectJAutoProxy來啟用,實際並不需要。

因為在AOP的預設配置屬性中,spring.aop.auto屬性預設是開啟的,也就是說只要引入了AOP依賴後,預設已經增加了@EnableAspectJAutoProxy。

3、定義切面類,實現web層的日誌切面

要想把一個類變成切面類,需要兩步,
① 在類上使用 @Component 註解 把切面類加入到IOC容器中
② 在類上使用 @Aspect 註解 使之成為切面類

package com.example.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * Created by lmb on 2018/9/5.
 */
@Aspect
@Component
public class WebLogAcpect {

    private Logger logger = LoggerFactory.getLogger(WebLogAcpect.class);

    /**
     * 定義切入點,切入點為com.example.aop下的所有函式
     */
    @Pointcut("execution(public * com.example.aop..*.*(..))")
    public void webLog(){}

    /**
     * 前置通知:在連線點之前執行的通知
     * @param joinPoint
     * @throws Throwable
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到請求,記錄請求內容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 記錄下請求內容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
    }

    @AfterReturning(returning = "ret",pointcut = "webLog()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 處理完請求,返回內容
        logger.info("RESPONSE : " + ret);
    }
}

以上的切面類通過 @Pointcut定義的切入點為com.example.aop包下的所有函式做切人,通過 @Before實現切入點的前置通知,通過 @AfterReturning記錄請求返回的物件。

訪問http://localhost:8004/hello?name=lmb得到控制檯輸出如下:

詳細程式碼參見本人的Github:SpringBoot整合AOP

二、AOP支援的通知
1、前置通知@Before:在某連線點之前執行的通知,除非丟擲一個異常,否則這個通知不能阻止連線點之前的執行流程。

/** 
 * 前置通知,方法呼叫前被呼叫 
 * @param joinPoint/null
 */  
@Before(value = POINT_CUT)
public void before(JoinPoint joinPoint){
    logger.info("前置通知");
    //獲取目標方法的引數資訊  
    Object[] obj = joinPoint.getArgs();  
    //AOP代理類的資訊  
    joinPoint.getThis();  
    //代理的目標物件  
    joinPoint.getTarget();  
    //用的最多 通知的簽名  
    Signature signature = joinPoint.getSignature();  
    //代理的是哪一個方法  
    logger.info("代理的是哪一個方法"+signature.getName());  
    //AOP代理類的名字  
    logger.info("AOP代理類的名字"+signature.getDeclaringTypeName());  
    //AOP代理類的類(class)資訊  
    signature.getDeclaringType();  
    //獲取RequestAttributes  
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();  
    //從獲取RequestAttributes中獲取HttpServletRequest的資訊  
    HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);  
    //如果要獲取Session資訊的話,可以這樣寫:  
    //HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);  
    //獲取請求引數
    Enumeration<String> enumeration = request.getParameterNames();  
    Map<String,String> parameterMap = Maps.newHashMap();  
    while (enumeration.hasMoreElements()){  
        String parameter = enumeration.nextElement();  
        parameterMap.put(parameter,request.getParameter(parameter));  
    }  
    String str = JSON.toJSONString(parameterMap);  
    if(obj.length > 0) {  
        logger.info("請求的引數資訊為:"+str);
    }  
}

注意:這裡用到了JoinPoint和RequestContextHolder。
1)、通過JoinPoint可以獲得通知的簽名信息,如目標方法名、目標方法引數資訊等;
2)、通過RequestContextHolder來獲取請求資訊,Session資訊;

2、後置通知@AfterReturning:在某連線點之後執行的通知,通常在一個匹配的方法返回的時候執行(可以在後置通知中繫結返回值)。

/** 
 * 後置返回通知 
 * 這裡需要注意的是: 
 *      如果引數中的第一個引數為JoinPoint,則第二個引數為返回值的資訊 
 *      如果引數中的第一個引數不為JoinPoint,則第一個引數為returning中對應的引數 
 *       returning:限定了只有目標方法返回值與通知方法相應引數型別時才能執行後置返回通知,否則不執行,
 *       對於returning對應的通知方法引數為Object型別將匹配任何目標返回值 
 * @param joinPoint 
 * @param keys 
 */  
@AfterReturning(value = POINT_CUT,returning = "keys")  
public void doAfterReturningAdvice1(JoinPoint joinPoint,Object keys){  
    logger.info("第一個後置返回通知的返回值:"+keys);  
}  

@AfterReturning(value = POINT_CUT,returning = "keys",argNames = "keys")  
public void doAfterReturningAdvice2(String keys){  
    logger.info("第二個後置返回通知的返回值:"+keys);  
}

3、後置異常通知@AfterThrowing:在方法丟擲異常退出時執行的通知。

/** 
 * 後置異常通知 
 *  定義一個名字,該名字用於匹配通知實現方法的一個引數名,當目標方法丟擲異常返回後,將把目標方法丟擲的異常傳給通知方法; 
 *  throwing:限定了只有目標方法丟擲的異常與通知方法相應引數異常型別時才能執行後置異常通知,否則不執行, 
 *           對於throwing對應的通知方法引數為Throwable型別將匹配任何異常。 
 * @param joinPoint 
 * @param exception 
 */  
@AfterThrowing(value = POINT_CUT,throwing = "exception")  
public void doAfterThrowingAdvice(JoinPoint joinPoint,Throwable exception){  
    //目標方法名:  
    logger.info(joinPoint.getSignature().getName());  
    if(exception instanceof NullPointerException){  
        logger.info("發生了空指標異常!!!!!");  
    }  
}

4、後置最終通知@After:當某連線點退出時執行的通知(不論是正常返回還是異常退出)。

/** 
 * 後置最終通知(目標方法只要執行完了就會執行後置通知方法) 
 * @param joinPoint 
 */  
@After(value = POINT_CUT)  
public void doAfterAdvice(JoinPoint joinPoint){ 
    logger.info("後置最終通知執行了!!!!");  
} 

5、環繞通知@Around:包圍一個連線點的通知,如方法呼叫等。這是最強大的一種通知型別。環繞通知可以在方法呼叫前後完成自定義的行為,它也會選擇是否繼續執行連線點或者直接返回它自己的返回值或丟擲異常來結束執行。

環繞通知最強大,也最麻煩,是一個對方法的環繞,具體方法會通過代理傳遞到切面中去,切面中可選擇執行方法與否,執行幾次方法等。環繞通知使用一個代理ProceedingJoinPoint型別的物件來管理目標物件,所以此通知的第一個引數必須是ProceedingJoinPoint型別。在通知體內呼叫ProceedingJoinPoint的proceed()方法會導致後臺的連線點方法執行。proceed()方法也可能會被呼叫並且傳入一個Object[]物件,該陣列中的值將被作為方法執行時的入參。

/** 
 * 環繞通知: 
 *   環繞通知非常強大,可以決定目標方法是否執行,什麼時候執行,執行時是否需要替換方法引數,執行完畢是否需要替換返回值。 
 *   環繞通知第一個引數必須是org.aspectj.lang.ProceedingJoinPoint型別 
 */  
@Around(value = POINT_CUT)  
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){  
    logger.info("環繞通知的目標方法名:"+proceedingJoinPoint.getSignature().getName());  
    try {  
        Object obj = proceedingJoinPoint.proceed();  
        return obj;  
    } catch (Throwable throwable) {  
        throwable.printStackTrace();  
    }  
    return null;  
} 

6、有時候我們定義切面的時候,切面中需要使用到目標物件的某個引數,如何使切面能得到目標物件的引數呢?可以使用args來繫結。如果在一個args表示式中應該使用型別名字的地方使用一個引數名字,那麼當通知執行的時候物件的引數值將會被傳遞進來。

@Before("execution(* findById*(..)) &&" + "args(id,..)")
    public void twiceAsOld1(Long id){
        System.err.println ("切面before執行了。。。。id==" + id);

    }

注意:任何通知方法都可以將第一個引數定義為org.aspectj.lang.JoinPoint型別(環繞通知需要定義第一個引數為ProceedingJoinPoint型別,它是 JoinPoint 的一個子類)。JoinPoint介面提供了一系列有用的方法,比如 getArgs()(返回方法引數)、getThis()(返回代理物件)、getTarget()(返回目標)、getSignature()(返回正在被通知的方法相關資訊)和 toString()(打印出正在被通知的方法的有用資訊)。

三、切入點表示式
定義切入點的時候需要一個包含名字和任意引數的簽名,還有一個切入點表示式,如execution(public * com.example.aop...(..))

切入點表示式的格式:execution([可見性]返回型別[宣告型別].方法名(引數)[異常])
其中[]內的是可選的,其它的還支援萬用字元的使用:
1) *:匹配所有字元
2) ..:一般用於匹配多個包,多個引數
3) +:表示類及其子類
4)運算子有:&&,||,!

切入點表示式關鍵詞用例:
1)execution:用於匹配子表示式。
//匹配com.cjm.model包及其子包中所有類中的所有方法,返回型別任意,方法引數任意
@Pointcut(“execution(* com.cjm.model...(..))”)
public void before(){}

2)within:用於匹配連線點所在的Java類或者包。
//匹配Person類中的所有方法
@Pointcut(“within(com.cjm.model.Person)”)
public void before(){}
//匹配com.cjm包及其子包中所有類中的所有方法
@Pointcut(“within(com.cjm..*)”)
public void before(){}

3) this:用於向通知方法中傳入代理物件的引用。
@Before(“before() && this(proxy)”)
public void beforeAdvide(JoinPoint point, Object proxy){
//處理邏輯
}

4)target:用於向通知方法中傳入目標物件的引用。
@Before(“before() && target(target)
public void beforeAdvide(JoinPoint point, Object proxy){
//處理邏輯
}

5)args:用於將引數傳入到通知方法中。
@Before(“before() && args(age,username)”)
public void beforeAdvide(JoinPoint point, int age, String username){
//處理邏輯
}

6)@within :用於匹配在類一級使用了引數確定的註解的類,其所有方法都將被匹配。
@Pointcut(“@within(com.cjm.annotation.AdviceAnnotation)”)
- 所有被@AdviceAnnotation標註的類都將匹配
public void before(){}

7)@target :和@within的功能類似,但必須要指定註解介面的保留策略為RUNTIME。
@Pointcut(“@target(com.cjm.annotation.AdviceAnnotation)”)
public void before(){}

8)@args :傳入連線點的物件對應的Java類必須被@args指定的Annotation註解標註。
@Before(“@args(com.cjm.annotation.AdviceAnnotation)”)
public void beforeAdvide(JoinPoint point){
//處理邏輯
}

9)@annotation :匹配連線點被它引數指定的Annotation註解的方法。也就是說,所有被指定註解標註的方法都將匹配。
@Pointcut(“@annotation(com.cjm.annotation.AdviceAnnotation)”)
public void before(){}

10)bean:通過受管Bean的名字來限定連線點所在的Bean。該關鍵詞是Spring2.5新增的。
@Pointcut(“bean(person)”)
public void before(){}

參考資料:https://www.cnblogs.com/lic309/p/4079194.html

轉 :https://blog.csdn.net/lmb55/article/details/82470388