spring之旅第五篇-AOP詳解
一、
Aspect oritention programming(面向切面編程),AOP是一種思想,高度概括的話是“橫向重復,縱向抽取”,如何理解呢?舉個例子:訪問頁面時需要權限認證,如果每個頁面都去實現方法顯然是不合適的,這個時候我們就可以利用切面編程。
每個頁面都去實現這個方法就是橫向的重復,我們直接從中切入,封裝一個與主業務無關的權限驗證的公共方法,這樣可以減少系統的重復代碼,降低模塊之間的耦合度,簡單的示意圖如下:
二、應用場景
1.連接點(Joinpoint) 所謂連接點是指那些可能被攔截到的方法。例如:所有可以增加的方法
2.切點(Pointcut) 已經被增強的連接點
3.增強(Advice) 增強的代碼
4.目標對象(Target) 目標類,需要被代理的類
5.織入(Weaving) 是指把增強advice應用到目標對象target來創建新的代理對象proxy的過程
6.代理(Proxy) 一個類被AOP織入增強後,就產生出了一個結果類,它是融合了原類和增強邏輯的代理類。
7.切面(Aspect)切入點+通知
通知類型:Spring按照通知Advice在目標類方法的連接點位置,可以分為5類
-
-
後置通知(在目標方法執行後實施增強)
-
環繞通知(在目標方法執行前後實施增加)
-
異常拋出通知(在方法跑出異常時通知)
-
引介通知(在目標類中添加一些新的方法和屬性)
四、實現原理
AOP的實現關鍵在於AOP框架自動創建的AOP代理。AOP代理主要分為兩大類:
靜態代理:使用AOP框架提供的命令進行編譯,從而在編譯階段就可以生成AOP代理類,因此也稱為編譯時增強;靜態代理一Aspectj為代表。
動態代理:在運行時借助於JDK動態代理,CGLIB等在內存中臨時生成AOP動態代理類,因此也被稱為運行時增強,Spring AOP用的就是動態代理。
4.1
//員工類 public class Employee { private Integer uid; public void setUid(Integer uid) { this.uid = uid; } public Integer getUid() { return uid; } private Integer age; private String name; public Integer getAge() { return age; } public String getName() { return name; } public void setAge(Integer age) { this.age = age; } public void setName(String name) { this.name = name; } }
員工接口:
//員工接口 public interface EmployeeService { //新增方法 void addEmployee(Employee employee); //刪除方法 void deleteEmployee(Integer uid); }
員工實現:
//員工方法實現 public class EmployeeServiceImpl implements EmployeeService { @Override public void addEmployee(Employee employee) { System.out.println("新增員工"); } @Override public void deleteEmployee(Integer uid) { System.out.println("刪除員工"); } }
事務類:
//事務類 public class MyTransaction { //開啟事務 public void before(){ System.out.println("開啟事務"); } //提交事務 public void after(){ System.out.println("提交事務"); } }
代理類:
//代理類 public class ProxyEmployee implements EmployeeService { // private EmployeeService employeeService; private MyTransaction myTransaction; public ProxyEmployee(EmployeeService employeeService,MyTransaction myTransaction) { this.employeeService=employeeService; this.myTransaction=myTransaction; } @Override public void addEmployee(Employee employee) { myTransaction.before(); employeeService.addEmployee(employee); myTransaction.after(); } @Override public void deleteEmployee(Integer uid) { myTransaction.before(); employeeService.deleteEmployee(uid); myTransaction.after(); } }
測試:
@Test public void fun1(){ MyTransaction transaction = new MyTransaction(); EmployeeService EmployeeService = new EmployeeServiceImpl(); //產生靜態代理對象 ProxyEmployee proxy = new ProxyEmployee(EmployeeService, transaction); proxy.addEmployee(null); proxy.deleteEmployee(0); }
結果:
這是靜態代理的實現方式,靜態代理有明顯的缺點:
1、代理對象的一個接口只服務於一種類型的對象,如果要代理的方法很多,勢必要為每一種方法都進行代理,靜態代理在程序規模稍大時就無法勝任了。
2、如果接口增加一個方法,比如 EmployeeService增加修改 updateEmployee()方法,則除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了代碼維護的復雜度。
4.2
public class ObjectInterceptor implements InvocationHandler { //目標類 private Object target; //切面類(這裏指事務類) private MyTransaction transaction; //通過構造器賦值 public ObjectInterceptor(Object target,MyTransaction transaction){ this.target = target; this.transaction = transaction; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { this.transaction.before(); method.invoke(target,args); this.transaction.after(); return null; } }
測試:
@Test public void fun2(){ //目標類 Object target = new EmployeeServiceImpl (); //事務類 MyTransaction transaction = new MyTransaction(); ObjectInterceptor proxyObject = new ObjectInterceptor(target, transaction); /** * 三個參數的含義: * 1、目標類的類加載器 * 2、目標類所有實現的接口 * 3、攔截器 */ EmployeeService employeeService = (EmployeeService) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), proxyObject); employeeService.addEmployee(null); employeeService.deleteEmployee(0); }
結果:
五、
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--目標類--> <bean name="employeeService" class="com.yuanqinnan.aop.EmployeeServiceImpl"></bean> <bean name="transaction" class="com.yuanqinnan.aop.MyTransaction"></bean> <aop:config> <aop:aspect ref="transaction"> <aop:pointcut id="pointcut" expression="execution(* com.yuanqinnan.aop.EmployeeServiceImpl..*(..))"/> <!-- 配置前置通知,註意 method 的值要和 對應切面的類方法名稱相同 --> <aop:before method="before" pointcut-ref="pointcut"></aop:before> <!--配置後置通知,註意 method 的值要和 對應切面的類方法名稱相同--> <aop:after-returning method="after" pointcut-ref="pointcut"/> </aop:aspect> </aop:config> </beans>
測試:
@Test public void fun3(){ ApplicationContext context = new ClassPathXmlApplicationContext("META-INF/applicationContext.xml"); EmployeeService employeeService = (EmployeeService) context.getBean("employeeService"); employeeService.addEmployee(null); }
結果:
補充:
1.aop:pointcut如果位於aop:aspect元素中,則命名切點只能被當前aop:aspect內定義的元素訪問到,為了能被整個aop:config元素中定義的所有增強訪問,則必須在aop:config下定義切點。
2.如果在aop:config元素下直接定義aop:pointcut,必須保證aop:pointcut在aop:aspect之前定義。aop:config下還可以定義aop:advisor,三者在aop:config中的配置有先後順序的要求:首先必須是aop:pointcut,然後是aop:advisor,最後是aop:aspect。而在aop:aspect中定義的aop:pointcut則沒有先後順序的要求,可以在任何位置定義。
aop:pointcut:用來定義切入點,該切入點可以重用;
aop:advisor:用來定義只有一個通知和一個切入點的切面;
aop:aspect:用來定義切面,該切面可以包含多個切入點和通知,而且標簽內部的通知和切入點定義是無序的;和advisor的區別就在此,advisor只包含一個通知和一個切入點。
3.在使用spring框架配置AOP的時候,不管是通過XML配置文件還是註解的方式都需要定義pointcut"切入點" 例如定義切入點表達式 execution(* com.sample.service.impl...(..)) execution()是最常用的切點函數,其語法如下所示:
整個表達式可以分為五個部分: (1)、execution(): 表達式主體。
(2)、第一個號:表示返回類型,
5.2
@Component @Aspect public class AopAspectJ { /** * 必須為final String類型的,註解裏要使用的變量只能是靜態常量類型的 */ public static final String EDP="execution(* com.yuanqinnan.aop.EmployeeServiceImpl..*(..))"; /** * 切面的前置方法 即方法執行前攔截到的方法 * 在目標方法執行之前的通知 * @param jp */ @Before(EDP) public void doBefore(JoinPoint jp){ System.out.println("=========執行前置通知=========="); } /** * 在方法正常執行通過之後執行的通知叫做返回通知 * 可以返回到方法的返回值 在註解後加入returning * @param jp * @param result */ @AfterReturning(value=EDP,returning="result") public void doAfterReturning(JoinPoint jp,String result){ System.out.println("===========執行後置通知============"); } /** * 最終通知:目標方法調用之後執行的通知(無論目標方法是否出現異常均執行) * @param jp */ @After(value=EDP) public void doAfter(JoinPoint jp){ System.out.println("===========執行最終通知============"); } /** * 環繞通知:目標方法調用前後執行的通知,可以在方法調用前後完成自定義的行為。 * @param pjp * @return * @throws Throwable */ @Around(EDP) public Object doAround(ProceedingJoinPoint pjp) throws Throwable{ System.out.println("======執行環繞通知開始========="); // 調用方法的參數 Object[] args = pjp.getArgs(); // 調用的方法名 String method = pjp.getSignature().getName(); // 獲取目標對象 Object target = pjp.getTarget(); // 執行完方法的返回值 // 調用proceed()方法,就會觸發切入點方法執行 Object result=pjp.proceed(); System.out.println("輸出,方法名:" + method + ";目標對象:" + target + ";返回值:" + result); System.out.println("======執行環繞通知結束========="); return result; } /** * 在目標方法非正常執行完成, 拋出異常的時候會走此方法 * @param jp * @param ex */ @AfterThrowing(value=EDP,throwing="ex") public void doAfterThrowing(JoinPoint jp,Exception ex) { System.out.println("===========執行異常通知============"); } }
xml配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.yuanqinnan.aop" ></context:component-scan> <!-- 聲明spring對@AspectJ的支持 --> <aop:aspectj-autoproxy/> <!--目標類--> <bean name="employeeService" class="com.yuanqinnan.aop.EmployeeServiceImpl"></bean> </beans>
測試:
@Test public void fun4(){ ApplicationContext act = new ClassPathXmlApplicationContext("META-INF/applicationContext.xml"); EmployeeService employeeService = (EmployeeService) act.getBean("employeeService"); employeeService.addEmployee(null); }
結果:
pringAOP的知識就總結到這裏
spring之旅第五篇-AOP詳解