聊一聊 AOP :表現形式與基礎概念

glmapper發表於2019-02-28

aop 終於提上日程來寫一寫了。

系列目錄

本系列分為 上、中、下三篇。上篇主要是介紹如果使用 AOP ,提供了demo和配置方式說明;中篇來對實現 AOP 的技術原理進行分析;下篇主要針對Spring中對於AOP的實現進行原始碼分析。

專案地址

專案地址:glmapper-ssm-parent

這個專案裡面包含了下面幾種 AOP 實現方式的所有程式碼,有興趣的同學可以fork跑一下。這個demo中列舉了4中方式的實現:

  • 基於程式碼的方式
  • 基於純POJO類的方式
  • 基於Aspect註解的方式
  • 基於注入式Aspect的方式

目前我們經常用到的是基於Aspect註解的方式的方式。下面來一個個瞭解下不同方式的表現形式。

基於代理的方式

這種方式看起來很好理解,但是配置起來相當麻煩;小夥伴們可以參考專案來看,這裡只貼出比較關鍵的流程程式碼。

1、首先定義一個介面:GoodsService

public interface GoodsService {
	/**
	 * 查詢所有商品資訊
	 * 
	 * @param offset 查詢起始位置
	 * @param limit 查詢條數
	 * @return
	 */
	List<Goods> queryAll(int offset,int limit);
}
複製程式碼

2、GoodsService 實現類

@Service
@Qualifier("goodsService")
public class GoodsServiceImpl implements GoodsService {
	@Autowired 
	private GoodsDao goodsDao;
	
	public List<Goods> queryAll(int offset, int limit) {
		System.out.println("執行了queryAll方法");
		List<Goods> list = new ArrayList<Goods>();
		return list;
	}
}
複製程式碼

3、定義一個通知類 LoggerHelper,該類繼承 MethodBeforeAdvice和 AfterReturningAdvice。

//通知類 LoggerHelper
public class LoggerHelper implements MethodBeforeAdvice,
AfterReturningAdvice {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoggerHelper.class);
    //MethodBeforeAdvice的before方法實現
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        LOGGER.info("before current time:"+System.currentTimeMillis());
    }
    //AfterReturningAdvice的afterReturning方法實現
    public void afterReturning(Object o, Method method,
    Object[] objects, Object o1) throws Throwable {
        LOGGER.info("afterReturning current time:"+System.currentTimeMillis());
    }
}
複製程式碼

4、重點,這個配置需要關注下。這個專案裡面我是配置在applicationContext.xml檔案中的。

<!-- 定義被代理者 -->
<bean id="goodsServiceImpl" class="com.glmapper.framerwork.service.impl.GoodsServiceImpl"></bean>

<!-- 定義通知內容,也就是切入點執行前後需要做的事情 -->
<bean id="loggerHelper" class="com.glmapper.framerwork.aspect.LoggerHelper"></bean>

<!-- 定義切入點位置 -->
<bean id="loggerPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
	<property name="pattern" value=".*query.*"></property>
</bean>

<!-- 使切入點與通知相關聯,完成切面配置 -->
<!-- 從這裡可以幫助我們理解Advisor,advice和pointcut之間的關係-->
<!--adivce和pointcut是Advisor的兩個屬性-->
<bean id="loggerHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
	<property name="advice" ref="loggerHelper"></property>
	<property name="pointcut" ref="loggerPointcut"></property>
</bean>

<!-- 設定代理 -->
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
	<!-- 代理的物件 ,也就是目標類-->
	<property name="target" ref="goodsServiceImpl"></property>
	<!-- 使用切面 -->
	<property name="interceptorNames" value="loggerHelperAdvisor"></property>
	<!-- 代理介面,商品介面 -->
	<property name="proxyInterfaces" value="com.glmapper.framerwork.service.GoodsService"></property>
</bean>
複製程式碼

5、使用:註解注入方式

@Controller
@RequestMapping("/buy")
public class BuyController {
    @Autowired
    private OrderService orderService;
    //因為我們已經在配置檔案中配置了proxy,
    //所以這裡可以直接注入拿到我們的代理類
    @Autowired
    private GoodsService proxy;
    
    @RequestMapping("/initPage")
    public ModelAndView initPage(HttpServletRequest request,
    	HttpServletResponse response, ModelAndView view) {
    //這裡使用proxy執行了*query*,
    List<Goods> goods = proxy.queryAll(10,10);
    view.addObject("goodsList", goods);
    view.setViewName("goodslist");
    return view;
    }
}
複製程式碼

6、使用:工具類方式手動獲取bean

這個方式是通過一個SpringContextUtil工具類來獲取代理物件的。

@RequestMapping("/initPage")
public ModelAndView initPage(HttpServletRequest request,
	HttpServletResponse response, ModelAndView view) {
    //這裡通過工具類來拿,效果一樣的。
    GoodsService proxy= (GoodsService) SpringContextUtil.getBean("proxy");
    List<Goods> goods = proxy.queryAll(10,10);
    view.addObject("goodsList", goods);
    view.setViewName("goodslist");
    return view;
}
複製程式碼

7、SpringContextUtil 類的定義

這個還是有點坑的,首先SpringContextUtil是繼承ApplicationContextAware這個介面,我們希望能夠SpringContextUtil可以被Spring容器直接管理,所以,需要使用 @Component 標註。標註了之後最關鍵的是它得能夠被我們配置的注入掃描掃到(親自踩的坑,我把它放在一個掃不到的包下面,一直debug都是null;差點砸電腦...)

@Component
public class SpringContextUtil implements ApplicationContextAware {

    // Spring應用上下文環境
    private static ApplicationContext applicationContext;

    /**
     * 實現ApplicationContextAware介面的回撥方法,設定上下文環境
     *
     * @param applicationContext
     */
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * @return ApplicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 獲取物件
     * 這裡重寫了bean方法,起主要作用
     * @param name
     * @return Object 一個以所給名字註冊的bean的例項
     * @throws BeansException
     */
    public static Object getBean(String name) throws BeansException {
        return applicationContext.getBean(name);
    }
}

複製程式碼

8、執行結果

21:04:47.940 [http-nio-8080-exec-7] INFO 
c.g.framerwork.aspect.LoggerHelper - before current
time:1529413487940

執行了queryAll方法

21:04:47.940 [http-nio-8080-exec-7] INFO 
c.g.framerwork.aspect.LoggerHelper - afterReturning current
time:1529413487940
複製程式碼

上面就是最最經典的方式,就是通過代理的方式來實現AOP的過程。

純POJO切面 aop:config

注意這裡和LoggerHelper的區別,這裡的LoggerAspect並沒有繼承任何介面或者抽象類。

1、POJO 類定義

/**
 * @description: [描述文字]
 * @email: <a href="guolei.sgl@antfin.com"></a>
 * @author: guolei.sgl
 * @date: 18/6/20
 */
public class LoggerAspect {
    private static final Logger LOGGER =
    LoggerFactory.getLogger(LoggerHelper.class);

    public void before(){
        LOGGER.info("before current time:"+System.currentTimeMillis());
    }

    public void afterReturning() {
        LOGGER.info("afterReturning current time:"+System.currentTimeMillis());
    }
} 
複製程式碼

2、配置檔案

<!-- 定義通知內容,也就是切入點執行前後需要做的事情 -->
<bean id="loggerAspect"  
    class="com.glmapper.framerwork.aspect.LoggerAspect">
</bean>

<aop:config>
    <!--定義切面-->
    <aop:aspect ref="loggerAspect">
    	<aop:pointcut id="loggerPointCut"  expression=
    	"execution(* com.glmapper.framerwork.service.impl.*.*(..)) " />
    	<!-- 定義 Advice -->
    	<!-- 前置通知 -->
    	<aop:before pointcut-ref="loggerPointCut" method="before" />
    	<!-- 後置通知 -->
    	<aop:after-returning pointcut-ref="loggerPointCut"
    	method="afterReturning"/>
    </aop:aspect>
</aop:config>
複製程式碼

注意這裡LoggerAspect中的before和afterReturning如果有引數,這裡需要處理下,否則會報 0 formal unbound in pointcut 異常。

@AspectJ 註解驅動方式

這種方式是最簡單的一種實現,直接使用 @Aspect 註解標註我們的切面類即可。

1、定義切面類,並使用 @Aspect 進行標註

/**
 * @description: 使用Aspect註解驅動的方式
 * @email: <a href="guolei.sgl@antfin.com"></a>
 * @author: guolei.sgl
 * @date: 18/6/20
 */
@Aspect
public class LoggerAspectInject {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspectInject.class);

    @Pointcut("execution(* com.glmapper.framerwork.service.impl.*.*(..))")
    public void cutIn(){}

    @Before("cutIn()")
    public void before(){
        LOGGER.info("before current time:"+System.currentTimeMillis());
    }

    @AfterReturning("cutIn()")
    public void AfterReturning(){
        LOGGER.info("afterReturning current time:"+System.currentTimeMillis());
    }
}
複製程式碼

2、使用方式1:配置檔案方式宣告 bean

<aop:aspectj-autoproxy />
<!-- 定義通知內容,也就是切入點執行前後需要做的事情 -->
<bean id="loggerAspectInject"
    class="com.glmapper.framerwork.aspect.LoggerAspectInject">
</bean>
<!-- 定義被代理者 -->
<bean id="goodsServiceImpl"
    class="com.glmapper.framerwork.service.impl.GoodsServiceImpl">
</bean>
複製程式碼

3、客戶端使用:

@Controller
@RequestMapping("/buy")
public class BuyController {

    @Autowired
    private OrderService orderService;
    
    @RequestMapping("/initPage")
    public ModelAndView initPage(HttpServletRequest request,
    		HttpServletResponse response, ModelAndView view) {
        //通過SpringContextUtil手動獲取 代理bean
    	GoodsService goodsService=(GoodsService)
    	SpringContextUtil.getBean("goodsServiceImpl");
    
    	List<Goods> goods = goodsService.queryAll(10,10);
    	view.addObject("goodsList", goods);
    	view.setViewName("goodslist");
    	return view;
    }
}
複製程式碼

4、使用方式2:使用@component註解託管給IOC

@Aspect
@Component //這裡加上了Component註解,就不需要在xml中配置了
public class LoggerAspectInject {

    private static final Logger LOGGER =
    LoggerFactory.getLogger(LoggerAspectInject.class);

    @Pointcut("execution(* com.glmapper.framerwork.service.impl.*.*(..))")
    public void cutIn(){}

    @Before("cutIn()")
    public void before(){
        LOGGER.info("before current time:"+System.currentTimeMillis());
    }

    @AfterReturning("cutIn()")
    public void AfterReturning(){
        LOGGER.info("afterReturning current time:"+System.currentTimeMillis());
    }
}
複製程式碼

5、客戶端程式碼:

@Controller
@RequestMapping("/buy")
public class BuyController {

    @Autowired
    private OrderService orderService;
    //直接注入
    @Autowired
    private GoodsService goodsService;
    
    @RequestMapping("/initPage")
    public ModelAndView initPage(HttpServletRequest request,
    		HttpServletResponse response, ModelAndView view) {
    	
    	List<Goods> goods = goodsService.queryAll(10,10);
    	view.addObject("goodsList", goods);
    	view.setViewName("goodslist");
    	return view;
    }
}
複製程式碼

6、比較完整的一個LoggerAspectInject,在實際工程中可以直接參考


/**
 * @description: aop
 * @email: <a href="henugl@1992.163.com"></a>
 * @author: glmapper@磊叔
 * @date: 18/6/4
 */
@Aspect
@Component
public class LoggerAspectInject {
    private static final Logger LOGGER= LoggerFactory.getLogger(LoggerAspectInject.class);
    
    @Pointcut("execution(* com.glmapper.book.web.controller.*.*(..))")
    public void cutIn(){

    }

    @Around("cutIn()")   // 定義Pointcut,名稱即下面的標識"aroundAdvice
    public Object aroundAdvice(ProceedingJoinPoint poin){
        System.out.println("環繞通知");
        Object object = null;
        try{
            object = poin.proceed();
        }catch (Throwable e){
            e.printStackTrace();
        }
        return object;
    }

    // 定義 advise
    //這個方法只是一個標識,相當於在配置檔案中定義了pointcut的id,此方法沒有返回值和引數
    @Before("cutIn()")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    @After("cutIn()")
    public void afterAdvice(){
        System.out.println("後置通知");
    }

    @AfterReturning("cutIn()")
    public void afterReturning(){
        System.out.println("後置返回 ");
    }

    @AfterThrowing("cutIn()")
    public void afterThrowing(){
        System.out.println("後置異常");
    }
}
複製程式碼

關於命名切入點:上面的例子中cutIn方法可以被稱之為命名切入點,命名切入點可以被其他切入點引用,而匿名切入點是不可以的。只有@AspectJ支援命名切入點,而Schema風格不支援命名切入點。 如下所示,@AspectJ使用如下方式引用命名切入點:

@Pointcut("execution(* com.glmapper.book.web.controller.*.*(..))")
public void cutIn(){
}
//引入命名切入點
@Before("cutIn()")
public void beforeAdvice(){
    System.out.println("前置通知");
}
複製程式碼

注入式 AspectJ 切面

這種方式我感覺是第二種和第三種的結合的一種方式。

1、定義切面類

/**
* @description: 注入式 也是一種通過XML方式配置的方式
* @email: <a href="guolei.sgl@antfin.com"></a>
* @author: guolei.sgl
* @date: 18/6/20
*/
public class LoggerAspectHelper {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspectHelper.class);
    
    /**
     * 調動方法前執行
     * @param point
     * @throws Throwable
     */
    public void doBefore(JoinPoint point) throws Throwable {
        LOGGER.info("before current time:"+System.currentTimeMillis());
    }
    
    /**
     * 在呼叫方法前後執行
     * @param point
     * @return
     * @throws Throwable
     */
    public Object doAround(ProceedingJoinPoint point) throws Throwable
    {
        LOGGER.info("around current time:"+System.currentTimeMillis());
        if(point.getArgs().length>0) {
            return point.proceed(point.getArgs());
        }else{
            return point.proceed();
        }
    }
    
    /**
     * 在呼叫方法之後執行
     * @param point
     * @throws Throwable
     */
    public void doAfter(JoinPoint point) throws Throwable
    {
        LOGGER.info("after current time:"+System.currentTimeMillis());
    }
    
    /**
     * 異常通知
     * @param point
     * @param ex
     */
    public void doThrowing(JoinPoint point, Throwable ex)
    {
        LOGGER.info("throwing current time:"+System.currentTimeMillis());
    }

}

複製程式碼

2、XML 配置

<bean id="loggerAspectHelper"    
    class="com.glmapper.framerwork.aspect.LoggerAspectHelper">
</bean>

<aop:config>
    <aop:aspect id="configAspect" ref="loggerAspectHelper">
    	<!--配置com.glmapper.framerwork.service.imp
    	包下所有類或介面的所有方法 -->
    	<aop:pointcut id="cutIn" expression=
    	"execution(* com.glmapper.framerwork.service.impl.*.*(..))" />
    	
    	<aop:before   pointcut-ref="cutIn" method="doBefore" />
    	<aop:after    pointcut-ref="cutIn" method="doAfter" />
    	<aop:around   pointcut-ref="cutIn" method="doAround" />
    	<aop:after-throwing pointcut-ref="cutIn" 
    	    method="doThrowing" throwing="ex" />
    	
    </aop:aspect>
</aop:config>
複製程式碼

3、結果

23:39:48.756 [http-nio-8080-exec-4] INFO  c.g.f.aspect.LoggerAspectHelper
- before current time:1529509188756
23:39:48.757 [http-nio-8080-exec-4] INFO  c.g.f.aspect.LoggerAspectHelper
- around current time:1529509188757
excute queryAll method...
23:39:48.757 [http-nio-8080-exec-4] INFO  c.g.f.aspect.LoggerAspectHelper
- after current time:1529509188757
複製程式碼

表示式


從上面的例子中我們都是使用一些正規表示式來指定我們的切入點的。在實際的使用中,不僅僅是execution,還有其他很多種型別的表示式。下面就列舉一些:

1、execution

用於匹配方法執行的連線點;

execution(* com.glmapper.book.web.controller.*.*(..))
複製程式碼
  • execution()表示式的主體;
  • 第一個 "*" 符號表示返回值的型別任意;
  • com.glmapper.book.web.controller AOP所切的服務的包名,即,我們的業務部分
  • 包名後面的"." 表示當前包及子包
  • 第二個"*" 表示類名,即所有類
  • .*(..) 表示任何方法名,括號表示引數,兩個點表示任何引數型別

2、within

用於匹配指定型別內的方法執行;

//如果在com.glmapper.book.web.controller包或其下的任何子包中
//定義了該型別,則在Web層中有一個連線點。
within(com.glmapper.book.web.controller..*)

@Pointcut("within(com.glmapper.book.web.controller..*)")
public void cutIn(){}
複製程式碼

@within:用於匹配所以持有指定註解型別內的方法;

/**
 * @description: 註解定義
 * @email: <a href="henugl@1992.163.com"></a>
 * @author: glmapper@磊叔
 * @date: 18/6/4
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface AuthAnnotation {
}
複製程式碼

任何目標物件對應的型別持有AuthAnnotation註解的類方法;必須是在目標物件上宣告這個註解,在介面上宣告的對它不起作用。

@within(com.glmapper.book.common.annotaion.AuthAnnotation)

//所有被@AdviceAnnotation標註的類都將匹配
@Pointcut("@within(com.glmapper.book.common.annotaion.AuthAnnotation)") 
public void cutIn(){}
複製程式碼

3、this

用於匹配當前AOP代理物件型別的執行方法;注意是AOP代理物件的型別匹配,這樣就可能包括引入介面也型別匹配;this中使用的表示式必須是型別全限定名,不支援萬用字元;

//當前目標物件(非AOP物件)實現了 UserService 介面的任何方法
this(com.glmapper.book.web.service.UserService)

//用於向通知方法中傳入代理物件的引用。
@Before("cutIn() && this(proxy)")
public void beforeAdvice(ProceedingJoinPoint poin,Object proxy){
    System.out.println("前置通知");
}
複製程式碼

4、target

用於匹配當前目標物件型別的執行方法;注意是目標物件的型別匹配,這樣就不包括引入介面也型別匹配;target中使用的表示式必須是型別全限定名,不支援萬用字元;

//當前目標物件(非AOP物件)實現了 UserService 介面的任何方法
target(com.glmapper.book.web.service.UserService)

//用於向通知方法中傳入代理物件的引用。
@Before("cutIn() && target(proxy)")
public void beforeAdvice(ProceedingJoinPoint poin,Object proxy){
    System.out.println("前置通知");
}
複製程式碼

@target:用於匹配當前目標物件型別的執行方法,其中目標物件持有指定的註解;任何目標物件持有Secure註解的類方法;這個和@within一樣必須是在目標物件上宣告這個註解,在介面上宣告的對它同樣不起作用。

@target(com.glmapper.book.common.annotaion.AuthAnnotation)

@Pointcut("@target(com.glmapper.book.common.annotaion.AuthAnnotation)")
public void cutIn(){}
複製程式碼

5、args

用於匹配當前執行的方法傳入的引數為指定型別的執行方法;引數型別列表中的引數必須是型別全限定名,萬用字元不支援;args屬於動態切入點,這種切入點開銷非常大,非特殊情況最好不要使用;

//任何一個以接受“傳入引數型別為java.io.Serializable”開頭,
//且其後可跟任意個任意型別的引數的方法執行,
//args指定的引數型別是在執行時動態匹配的
args (java.io.Serializable,..)

//用於將引數傳入到通知方法中。
@Before("cutIn() && args(age,username)")
public void beforeAdvide(JoinPoint point, int age, String username){
  //...
}
複製程式碼

@args:用於匹配當前執行的方法傳入的引數持有指定註解的執行;任何一個只接受一個引數的方法,且方法執行時傳入的引數持有註解AuthAnnotation;動態切入點,類似於arg指示符;

@args (com.glmapper.book.common.annotaion.AuthAnnotation)

@Before("@args(com.glmapper.book.common.annotaion.AuthAnnotation)")
public void beforeAdvide(JoinPoint point){
  //...
}
複製程式碼

6、@annotation

使用“@annotation(註解型別)”匹配當前執行方法持有指定註解的方法;註解型別也必須是全限定型別名;

//當前執行方法上持有註解 AuthAnnotation將被匹配
@annotation(com.glmapper.book.common.annotaion.AuthAnnotation)

//匹配連線點被它引數指定的AuthAnnotation註解的方法。
//也就是說,所有被指定註解標註的方法都將匹配。
@Pointcut("@annotation(com.glmapper.book.common.annotaion.AuthAnnotation)")
public void cutIn(){}
複製程式碼

還有一種是bean的方式,沒用過。有興趣可以看看。

例子在下面說到的基礎概念部分對應給出。

基礎概念

基礎概念部分主要將 AOP 中的一些概念點捋一捋,這部分主要參考了官網上的一些解釋。

AOP

AOP(Aspect-Oriented Programming), 即 面向切面程式設計, 它與 OOP( Object-Oriented Programming, 物件導向程式設計) 相輔相成, 提供了與 OOP 不同的抽象軟體結構的視角。在 OOP 中,我們以類(class)作為我們的基本單元, 而 AOP 中的基本單元是 Aspect(切面)

橫切關注點(Cross Cutting Concern):獨立服務,如系統日誌。如果不是獨立服務(就是與業務耦合比較強的服務)就不能橫切了。通常這種獨立服務需要遍佈系統各個角落,遍佈在業務流程之中。

Target Object

目標物件。織入 advice 的目標物件。 目標物件也被稱為 advised object。 因為 Spring AOP 使用執行時代理的方式來實現 aspect, 因此 adviced object 總是一個代理物件(proxied object);注意, adviced object 指的不是原來的類, 而是織入 advice 後所產生的代理類。

織入(Weave)

Advice應用在JoinPoint的過程,這個過程叫織入。從另外一個角度老說就是將 aspect 和其他物件連線起來, 並建立 adviced object 的過程。

根據不同的實現技術, AOP織入有三種方式:

  • 編譯器織入,這要求有特殊的Java編譯器
  • 類裝載期織入, 這需要有特殊的類裝載器
  • 動態代理織入, 在執行期為目標類新增增強( Advice )生成子類的方式。

Spring 採用動態代理織入, 而AspectJ採用編譯器織入和類裝載期

代理

Spring AOP預設使用代理的是標準的JDK動態代理。這使得任何介面(或一組介面)都可以代理。

Spring AOP也可以使用CGLIB代理。如果業務物件不實現介面,則預設使用CGLIB。對介面程式設計而不是對類程式設計是一種很好的做法;業務類通常會實現一個或多個業務介面。在一些特殊的情況下,即需要通知的介面上沒有宣告的方法,或者需要將代理物件傳遞給具體型別的方法,有可能強制使用CGLIB。

Introductions

我們知道Java語言本身並非是動態的,就是我們的類一旦編譯完成,就很難再為他新增新的功能。但是在一開始給出的例子中,雖然我們沒有向物件中新增新的方法,但是已經向其中新增了新的功能。這種屬於向現有的方法新增新的功能,那能不能為一個物件新增新的方法呢?答案肯定是可以的,使用introduction就能夠實現。

introduction:動態為某個類增加或減少方法。為一個型別新增額外的方法或欄位。Spring AOP 允許我們為 目標物件 引入新的介面(和對應的實現)。

Aspect

切面:通知和切入點的結合。

切面實現了cross-cutting(橫切)功能。最常見的是logging模組、方法執行耗時模組,這樣,程式按功能被分為好幾層,如果按傳統的繼承的話,商業模型繼承日誌模組的話需要插入修改的地方太多,而通過建立一個切面就可以使用AOP來實現相同的功能了,我們可以針對不同的需求做出不同的切面。

而將散落於各個業務物件之中的Cross-cutting concerns 收集起來,設計各個獨立可重用的物件,這些物件稱之為Aspect;在上面的例子中我們根據不同的配置方式,定義了四種不同形式的切面。

Joinpoint

Aspect 在應用程式執行時加入業務流程的點或時機稱之為 Joinpoint ,具體來說,就是 Advice 在應用程式中被呼叫執行的時機,這個時機可能是某個方法被呼叫之前或之後(或兩者都有),或是某個異常發生的時候。

Joinpoint & ProceedingJoinPoint

環繞通知 = 前置+目標方法執行+後置通知,proceed方法就是用於啟動目標方法執行的。

環繞通知 ProceedingJoinPoint 執行 proceed 方法 的作用是讓目標方法執行 ,這 也是環繞通知和前置、後置通知方法的一個最大區別。

Proceedingjoinpoint 繼承了 JoinPoint 。是在JoinPoint的基礎上暴露出 proceed 這個方法。proceed很重要,這個是aop代理鏈執行的方法;暴露出這個方法,就能支援aop:around 這種切面(其他的幾種切面只需要用到JoinPoint,這跟切面型別有關), 能決定是否走代理鏈還是走自己攔截的其他邏輯。

在環繞通知的方法中是需要返回一個Object型別物件的,如果把環繞通知的方法返回型別是void,將會導致一些無法預估的情況,比如:404。

Pointcut

匹配 join points的謂詞。Advice與切入點表示式相關聯, 並在切入點匹配的任何連線點上執行。(例如,具有特定名稱的方法的執行)。由切入點表示式匹配的連線點的概念是AOP的核心,Spring預設使用AspectJ切入點表示式語言。

Spring 中, 所有的方法都可以認為是Joinpoint, 但是我們並不希望在所有的方法上都新增 Advice, 而 Pointcut 的作用就是提供一組規則(使用 AspectJ pointcut expression language 來描述) 來匹配Joinpoint, 給滿足規則的Joinpoint 新增 Advice

Pointcut 和 Joinpoint

Spring AOP 中, 所有的方法執行都是 join point。 而 point cut 是一個描述資訊,它修飾的是 join point, 通過 point cut,我們就可以確定哪些 join point 可以被織入Advice。 因此join pointpoint cut本質上就是兩個不同維度上的東西。

advice 是在 join point 上執行的, 而 point cut 規定了哪些 join point 可以執行哪些 advice

Advice

概念

Advice 是我們切面功能的實現,它是切點的真正執行的地方。比如像前面例子中列印時間的幾個方法(被@Before等註解標註的方法都是一個通知);Advice 在 Jointpoint 處插入程式碼到應用程式中。

分類

BeforeAdvice,AfterAdvice,區別在於Advice在目標方法之前呼叫還是之後呼叫,Throw Advice 表示當目標發生異常時呼叫Advice。

  • before advice: 在 join point 前被執行的 advice. 雖然 before advice 是在 join point 前被執行, 但是它並不能夠阻止 join point 的執行, 除非發生了異常(即我們在 before advice 程式碼中, 不能人為地決定是否繼續執行 join point 中的程式碼)
  • after return advice: 在一個 join point 正常返回後執行的 advice
  • after throwing advice: 當一個 join point 丟擲異常後執行的 advice
  • after(final) advice: 無論一個 join point 是正常退出還是發生了異常, 都會被執行的 advice.
  • around advice:在 join point 前和 joint point 退出後都執行的 advice. 這個是最常用的 advice.

Advice、JoinPoint、PointCut 關係

聊一聊 AOP :表現形式與基礎概念

下面這張圖是在網上一位大佬的部落格裡發現的,可以幫助我們更好的理解這些概念之間的關係。

圖片源自網路

上面是對於AOP中涉及到的一些基本概念及它們之間的關係做了簡單的梳理。

一些坑

在除錯程式過程中出現的一些問題記錄

1、使用AOP攔截controller層的服務成功,但是頁面報錯404

@Around("cutIn()")
public void aroundAdvice(ProceedingJoinPoint poin) {
    System.out.println("環繞通知");
}
複製程式碼

這裡需要注意的是再使用環繞通知時,需要給方法一個返回值。

@Around("cutIn()")
public Object aroundAdvice(ProceedingJoinPoint poin) throws Throwable {
    System.out.println("環繞通知");
    return poin.proceed();
}
複製程式碼

2、0 formal unbound in pointcut

在spring 4.x中 提供了aop註解方式 帶引數的方式。看下面例子:

@Pointcut(value = "execution(* com.glmapper.framerwork.service.impl.*(int,int)) && args(i,j)")  
public void cutIn(int i, int j) {}  
  
@Before(value="cutIn(i, j)",argNames = "i,j")  
public void beforeMethod( int i, int j) {  
    System.out.println("---------begins with " + i + "-" +j);  
}  
複製程式碼

比如說這裡,Before中有兩個int型別的引數,如果此時我們在使用時沒有給其指定引數,那麼就會丟擲:Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut 異常資訊。

本來是想放在一篇裡面的,但是實在太長了,就分開吧;週末更新下

相關文章