這章我們接着講 Spring 的核心概念---AOP,這也是 Spring 框架中最爲核心的一個概念。
PS:本篇博客源碼下載鏈接:http://pan.baidu.com/s/1skZjg7r 密碼:dn42
1、AOP 什麼?
AOP(Aspect Oriented Programming),通常稱爲面向切面編程。它利用一種稱爲"橫切"的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其命名爲"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。
什麼是切面,什麼是公共模塊,那麼我們概念少說,直接通過一個實例來看看 AOP 到底是什麼。
2、需求
現在有一張表 User,然後我們要在程序中實現對 User 表的增加和刪除操作。
要求:增加和刪除操作都必須要開啓事務,操作完成之後要提交事務。
User.java
package com.ys.aop.one;
public class User {
private int uid;
private String uname;
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getUname() {
return uname;
}
public void setUname(String uname) {
this.uname = uname;
}
}
3、解決辦法1:使用靜態代理
第一步:創建 UserService 接口
package com.ys.aop.one;
public interface UserService {
//添加 user
public void addUser(User user);
//刪除 user
public void deleteUser(int uid);
}
第二步:創建 UserService的實現類
package com.ys.aop.one;
public class UserServiceImpl implements UserService{
@Override
public void addUser(User user) {
System.out.println("增加 User");
}
@Override
public void deleteUser(int uid) {
System.out.println("刪除 User");
}
}
第三步:創建事務類 MyTransaction
package com.ys.aop.one;
public class MyTransaction {
//開啓事務
public void before(){
System.out.println("開啓事務");
}
//提交事務
public void after(){
System.out.println("提交事務");
}
}
第四步:創建代理類 ProxyUser.java
package com.ys.aop.one;
public class ProxyUser implements UserService{
//真實類
private UserService userService;
//事務類
private MyTransaction transaction;
//使用構造函數實例化
public ProxyUser(UserService userService,MyTransaction transaction){
this.userService = userService;
this.transaction = transaction;
}
@Override
public void addUser(User user) {
transaction.before();
userService.addUser(user);
transaction.after();
}
@Override
public void deleteUser(int uid) {
transaction.before();
userService.deleteUser(uid);
transaction.after();
}
}
測試:
@Test
public void testOne(){
MyTransaction transaction = new MyTransaction();
UserService userService = new UserServiceImpl();
//產生靜態代理對象
ProxyUser proxy = new ProxyUser(userService, transaction);
proxy.addUser(null);
proxy.deleteUser(0);
}
結果:
這是一個很基礎的靜態代理,業務類UserServiceImpl 只需要關注業務邏輯本身,保證了業務的重用性,這也是代理類的優點,沒什麼好說的。我們主要說說這樣寫的缺點:
①、代理對象的一個接口只服務於一種類型的對象,如果要代理的方法很多,勢必要爲每一種方法都進行代理,靜態代理在程序規模稍大時就無法勝任了。
②、如果接口增加一個方法,比如 UserService 增加修改 updateUser()方法,則除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了代碼維護的複雜度。
4、解決辦法2:使用JDK動態代理
動態代理就不要自己手動生成代理類了,我們去掉 ProxyUser.java 類,增加一個 ObjectInterceptor.java 類
package com.ys.aop.two;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.ys.aop.one.MyTransaction;
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(this.target, args);
//提交事務
this.transaction.after();
return null;
}
}
測試:
@Test
public void testOne(){
//目標類
Object target = new UserServiceImpl();
//事務類
MyTransaction transaction = new MyTransaction();
ObjectInterceptor proxyObject = new ObjectInterceptor(target, transaction);
/**
* 三個參數的含義:
* 1、目標類的類加載器
* 2、目標類所有實現的接口
* 3、攔截器
*/
UserService userService = (UserService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), proxyObject);
userService.addUser(null);
}
結果:
那麼使用動態代理來完成這個需求就很好了,後期在 UserService 中增加業務方法,都不用更改代碼就能自動給我們生成代理對象。而且將 UserService 換成別的類也是可以的。
也就是做到了代理對象能夠代理多個目標類,多個目標方法。
注意:我們這裏使用的是 JDK 動態代理,要求是必須要實現接口。與之對應的另外一種動態代理實現模式 Cglib,則不需要,我們這裏就不講解 cglib 的實現方式了。
不管是哪種方式實現動態代理。本章的主角:AOP 實現原理也是動態代理
5、AOP 關鍵術語
1.target:目標類,需要被代理的類。例如:UserService
2.Joinpoint(連接點):所謂連接點是指那些可能被攔截到的方法。例如:所有的方法
3.PointCut 切入點:已經被增強的連接點。例如:addUser()
4.advice 通知/增強,增強代碼。例如:after、before
5. Weaving(織入):是指把增強advice應用到目標對象target來創建新的代理對象proxy的過程.
6.proxy 代理類:通知+切入點
7. Aspect(切面): 是切入點pointcut和通知advice的結合
具體可以根據下面這張圖來理解:
6、AOP 的通知類型
Spring按照通知Advice在目標類方法的連接點位置,可以分爲5類
- 前置通知 org.springframework.aop.MethodBeforeAdvice
- 在目標方法執行前實施增強,比如上面例子的 before()方法
- 後置通知 org.springframework.aop.AfterReturningAdvice
- 在目標方法執行後實施增強,比如上面例子的 after()方法
- 環繞通知 org.aopalliance.intercept.MethodInterceptor
- 在目標方法執行前後實施增強
- 異常拋出通知 org.springframework.aop.ThrowsAdvice
- 在方法拋出異常後實施增強
- 引介通知 org.springframework.aop.IntroductionInterceptor
在目標類中添加一些新的方法和屬性
7、使用 Spring AOP 解決上面的需求
我們只需要在Spring 的配置文件 applicationContext.xml 進行如下配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--1、 創建目標類 -->
<bean class="com.ys.aop.UserServiceImpl"></bean>
<!--2、創建切面類(通知) -->
<bean class="com.ys.aop.one.MyTransaction"></bean>
<!--3、aop編程
3.1 導入命名空間
3.2 使用 <aop:config>進行配置
proxy-target-class="true" 聲明時使用cglib代理
如果不聲明,Spring 會自動選擇cglib代理還是JDK動態代理
<aop:pointcut> 切入點 ,從目標對象獲得具體方法
<aop:advisor> 特殊的切面,只有一個通知 和 一個切入點
advice-ref 通知引用
pointcut-ref 切入點引用
3.3 切入點表達式
execution(* com.ys.aop.*.*(..))
選擇方法 返回值任意 包 類名任意 方法名任意 參數任意
-->
<aop:config>
<!-- 切入點表達式 -->
<aop:pointcut expression="execution(* com.ys.aop.*.*(..))" />
<aop:aspect ref="transaction">
<!-- 配置前置通知,注意 method 的值要和 對應切面的類方法名稱相同 -->
<aop:before method="before" pointcut-ref="myPointCut"></aop:before>
<aop:after-returning method="after" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
測試:
@Test
public void testAop(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService useService = (UserService) context.getBean("userService");
useService.addUser(null);
}
結果:
上面的配置我們在註釋中寫的很清楚了。這裏我們重點講解一下:
①、 切入點表達式,一個完整的方法表示如下:
execution(modifiers-pattern? ref-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
類修飾符 返回值 方法所在的包 方法名 方法拋出的異常
那麼根據上面的對比,我們就很好理解:
execution(* com.ys.aop.*.*(..))
選擇方法 返回值任意 包 類名任意 方法名任意 參數任意
②、springAOP 的具體加載步驟:
1、當 spring 容器啓動的時候,加載了 spring 的配置文件
2、爲配置文件中的所有 bean 創建對象
3、spring 容器會解析 aop:config 的配置
1、解析切入點表達式,用切入點表達式和納入 spring 容器中的 bean 做匹配
如果匹配成功,則會爲該 bean 創建代理對象,代理對象的方法=目標方法+通知
如果匹配不成功,不會創建代理對象
4、在客戶端利用 context.getBean() 獲取對象時,如果該對象有代理對象,則返回代理對象;如果沒有,則返回目標對象
說明:如果目標類沒有實現接口,則 spring 容器會採用 cglib 的方式產生代理對象,如果實現了接口,則會採用 jdk 的方式