aop(面向切面編程)是一種重要的編程思想,是對面向對象編程的完善和補充。我們都很熟悉“高內聚,低耦合”,這是評判代碼是否優質的標準之一,而aop思想,就是對這一標準的具體實現。
簡單地,我們可以從“日誌模塊”角度來理解aop如何實現解耦。以往的方式,我們會在業務代碼中嵌套許多瞭如System.out.print("xxx")
或logger.error("xxxx")
等方式,實現日誌的記錄,打印,日誌的輸出其實完全不影響核心業務功能的運行,特別是開發階段留下的零散的日誌代碼,往往無法統一管理。這就使得業務代碼和日誌輸出代碼,高度的耦合在一起。當我某天想調整日誌輸出的格式時,往往也會涉及到業務代碼類的更新。
aop思想很好的解決了這一點,可以實現核心業務代碼完全獨立,日誌輸出代碼完全獨立,後者以“切面類的形式切入到業務代碼的具體位置處”。聽起來似乎很抽象,簡言之:將日誌輸出的代碼動態的切入到核心業務代碼中去。
要實現這一點,就不得不提java動態代理。jdk有自己的一套動態代理組件,被代理對象必須實現接口才能生成代理對象,並且操作難度不算簡單。spring推薦使用cglib來實現動態代理,結合相關增強包,可以讓沒有實現任何接口的類,也生成代理對象。
概念性的東西,需要慢慢去理解,代碼對我們來說纔是最直觀的。我們先快速着手一個demo上手spring aop的使用。
- 第一步,肯定是導包,或者添加相關的依賴。
處理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>
- 編寫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>
- 編寫業務層測試代碼
業務接口類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;
}
}
- 編寫切面類
該類添加了四個方法,四個方法對應了不同的四個註解,這四個註解來源於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......");
}
}
- 編寫測試類
先加載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();
}
}
- 分析控制檯打印日誌記錄
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概念圖,還是非常形象的。