簡單六步上手spring aop,通過各種類型通知,面向切面編程,實現代碼解耦(超詳細)

aop(面向切面編程)是一種重要的編程思想,是對面向對象編程的完善和補充。我們都很熟悉“高內聚,低耦合”,這是評判代碼是否優質的標準之一,而aop思想,就是對這一標準的具體實現。

簡單地,我們可以從“日誌模塊”角度來理解aop如何實現解耦。以往的方式,我們會在業務代碼中嵌套許多瞭如System.out.print("xxx")logger.error("xxxx")等方式,實現日誌的記錄,打印,日誌的輸出其實完全不影響核心業務功能的運行,特別是開發階段留下的零散的日誌代碼,往往無法統一管理。這就使得業務代碼和日誌輸出代碼,高度的耦合在一起。當我某天想調整日誌輸出的格式時,往往也會涉及到業務代碼類的更新。
aop思想很好的解決了這一點,可以實現核心業務代碼完全獨立,日誌輸出代碼完全獨立,後者以“切面類的形式切入到業務代碼的具體位置處”。聽起來似乎很抽象,簡言之:將日誌輸出的代碼動態的切入到核心業務代碼中去。
要實現這一點,就不得不提java動態代理。jdk有自己的一套動態代理組件,被代理對象必須實現接口才能生成代理對象,並且操作難度不算簡單。spring推薦使用cglib來實現動態代理,結合相關增強包,可以讓沒有實現任何接口的類,也生成代理對象。
概念性的東西,需要慢慢去理解,代碼對我們來說纔是最直觀的。我們先快速着手一個demo上手spring aop的使用。

  1. 第一步,肯定是導包,或者添加相關的依賴。
    處理spring核心的五個jar+common-logging以外,這裏需要添加aspects、aspectjweaver、cglib、aopalliance的支持,其中後面三個屬於增強型依賴,可以讓沒有實現任何接口的類,也能生成代理對象。
 <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.4.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.5</version>
    </dependency>
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>3.3.0</version>
    </dependency>
    <dependency>
      <groupId>aopalliance</groupId>
      <artifactId>aopalliance</artifactId>
      <version>1.0</version>
    </dependency>
  1. 編寫spring配置applicationContext.xml
    這裏提到兩個,一個是aop自動代理,另一個是我們都非常熟悉的包掃描,掃描時排除掉控制器層的組件。
<!--    開啓aop自動代理-->
    <aop:aspectj-autoproxy/>
<!--    開啓註解掃描,希望處理service和dao,controller不需要,交給springMVC處理-->
    <context:component-scan base-package="com.wuwl">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
  1. 編寫業務層測試代碼
    業務接口類StudentService
public interface StudentService {

    /**
     * 添加學生信息
     */
    public void addStudent();

    /**
     * 刪除學生信息
     */
    public void deleteStudent();

}

接口實現類StudentServiceImpl ,此處添加註解,讓spring加載該組件

@Service("studentService")
public class StudentServiceImpl implements StudentService {
    @Override
    public void addStudent() throws Exception {
            System.out.println("addStudent...業務層代碼執行...");
    }
    @Override
    public void deleteStudent() throws Exception{
            System.out.println("deleteStudent...業務層代碼執行...");
            int i = 1/0;
    }
}


  1. 編寫切面類

該類添加了四個方法,四個方法對應了不同的四個註解,這四個註解來源於org.aspectj.lang.annotation.*用以告訴spring在何時切入當前方法。@Before標註的方法會在切入點方法執行前執行;@After標註的方法會在切入點方法執行後執行;@AfterReturning標註的方法會在切入點方法返回時執行;@AfterThrowing標註的方法會在切入點方法拋出異常時執行。
在註解內部添加的屬性爲切入點表達式,該表達式用以告訴spring這何地切入當前方法。
表達式格式相對固定execution([修飾符] 返回值類型 包名.類名.方法名(參數))

@Aspect
@Component
public class StudentServiceLogger {

    /**
     * 告訴spring在目標方法執行前運行
     * 切入點表達式告訴方法在何地執行,註解類型約束方法在何時執行
     */
    @Before("execution(public void com.wuwl.service.impl.StudentServiceImpl.*() )")
    public void doBefore(){
        System.out.println("before......");
    }

    @After("execution(public void com.wuwl.service.impl.StudentServiceImpl.*() )")
    public void doAfter(){
        System.out.println("after......");
    }

    @AfterReturning("execution(public void com.wuwl.service.impl.StudentServiceImpl.*() )")
    public void doReturn(){
        System.out.println("return......");
    }

    @AfterThrowing("execution(public void com.wuwl.service.impl.StudentServiceImpl.*() )")
    public void doThrow(){
        System.out.println("throw......");
    }

}
  1. 編寫測試類
    先加載IOC容器,再從容器中獲取到代理對象,執行代理對象的方法。
public class AopTest {
    ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
    @Test
    public void test1(){
        StudentService studentService = ac.getBean("studentService",StudentService.class);
        System.out.println(studentService.getClass());
        System.out.println(Arrays.toString(studentService.getClass().getGenericInterfaces()));
        System.out.println("==================割===============");
        studentService.addStudent();
        System.out.println("==================割===============");
        studentService.deleteStudent();
    }
}
  1. 分析控制檯打印日誌記錄
class com.sun.proxy.$Proxy29
[interface com.wuwl.service.StudentService, interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.core.DecoratingProxy]
==================割===============
before......
addStudent...業務層代碼執行...
after......
return......
==================割===============
before......
deleteStudent...業務層代碼執行...
after......
throw......

java.lang.ArithmeticException: / by zero

根據控制檯打印的記錄,我們可以直觀的看到很多問題。
①ioc容器中的bean實際爲代理對象,我們打印了studentService 對象的Class屬性,輸出值爲class com.sun.proxy.$Proxy29,以此可見,我們通過@Service註解標誌後,ioc容器初始化,並未直接將StudentService 類直接實例化後加入ioc容器,而是生成代理類,加入ioc容器的爲代理類的實例化對象。
②被代理類有實現其它接口時,代理類實現了所有被代理類所實現的接口。這種方式的動態代理,使用的是JDK提供的代理功能。但是當被代理類沒有實現任何接口時,通過cglib動態代理,可實現代理。打印一下測試用的PersonService類所實現的所有接口進行查看:

class com.wuwl.service.PersonService$$EnhancerBySpringCGLIB$$70c26fac
[interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.cglib.proxy.Factory]

首先,代理類的命名規範就與上面的有所不同,並且後者是帶有CGLIB字樣的。其次,該代理類的所有接口均爲springframework包下所提供的接口,沒有程序員自定義的接口,也就是說被代理類沒有實現任何接口。
③對比兩個方法的輸出日誌,我們接着分析各類通知執行的順序。

	try{
		@Before
		method.invoke(obj,args);
		@AfterReturning
	}catch(){
		@AfterThrowing
	}finally{
		@After
	}

各通知方法,在代理類中與切入點的相對位置可參考以上示例。當程序正常,執行時,按照我們的理解,應該是:@Before>@AfterReturning>@After,而事實上的順序爲:@Before>@After>@AfterReturning;當程序出現異常時,通知方法的執行順序爲:@Before>@After>@AfterThrowing
感覺是有點怪,不過,約定大於配置嘛,習慣就好。
最後,分享一張AOP概念圖,還是非常形象的。
圖片來自於網絡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章