SpringAOP詳解(第二次學習Spring,主要是JDK動態代理)
第一次學習Spring的時候只是覺得很好用,而且也只是停留在會用的階段,現在在公司實習,又叫看Spring,於是就有了這篇文章,這時才體會到Spring確實優秀。(博主只是一個自學一年不到的小白,如果有錯還望各位大佬批評指正)
一.裝飾類
例子:在我們實現具體的業務需求操作的時候,我們為了保證業務安全,需要在業務方法前面開啟事務,方法完成之後再提交事務,如果業務出錯還需要回滾事務。如果有大量的業務方法,我們就會發現業務程式碼變得臃腫,而且全是重複事務的操作,這時候我們就會想要重構。因此首先我們是採用裝飾類來加強業務程式碼。(說明一下,為了直觀一點,我所有的bean都用spring的xml裝配,不採用@autowired自動裝配)
//這是業務方法service,只是模擬了儲存,和會出錯的更新 package com.swust.service.impl; import com.swust.dao.IEmployeeDAO; import com.swust.dao.impl.EmployeeDAOImpl; import com.swust.domain.Employee; import com.swust.service.IEmployeeService; import lombok.Setter; public class EmployeeServiceImpl implements IEmployeeService { @Setter private IEmployeeDAO dao; public void save(Employee employee) { dao.save(employee); } public void update(Employee employee) { dao.update(employee); throw new RuntimeException("故意出錯============="); } }
package com.swust.service.impl; import com.swust.dao.IEmployeeDAO; import com.swust.dao.impl.EmployeeDAOImpl; import com.swust.domain.Employee; import com.swust.service.IEmployeeService; import lombok.Setter; public class EmployeeServiceImpl implements IEmployeeService { @Setter private IEmployeeDAO dao; public void save(Employee employee) { dao.save(employee); } public void update(Employee employee) { dao.update(employee); throw new RuntimeException("故意出錯============="); } }
//這是包裝類
package com.swust.test.wapper;
import com.swust.TranscantionManager;
import com.swust.domain.Employee;
import com.swust.service.IEmployeeService;
/**
* @author Mr
* 模仿包裝類,用事務包裝EmployeeServiceImpl,達到增強的目的
*/
public class EmployeeServiceWapper implements IEmployeeService{
private IEmployeeService serviceImpl;
private TranscantionManager tx;
public EmployeeServiceWapper(IEmployeeService serviceImpl, TranscantionManager tx) {
this.serviceImpl = serviceImpl;
this.tx = tx;
}
public void save(Employee e) {
try {
tx.begin();
serviceImpl.save(e);
tx.commit();
} catch (Exception e2) {
e2.printStackTrace();
tx.rollback();
}
}
public void update(Employee e) {
try {
tx.begin();
serviceImpl.update(e);
tx.commit();
} catch (Exception e2) {
e2.printStackTrace();
tx.rollback();
}
}
}
package com.swust.test.wapper;
import com.swust.TranscantionManager;
import com.swust.domain.Employee;
import com.swust.service.IEmployeeService;
/**
* @author Mr
* 模仿包裝類,用事務包裝EmployeeServiceImpl,達到增強的目的
*/
public class EmployeeServiceWapper implements IEmployeeService{
private IEmployeeService serviceImpl;
private TranscantionManager tx;
public EmployeeServiceWapper(IEmployeeService serviceImpl, TranscantionManager tx) {
this.serviceImpl = serviceImpl;
this.tx = tx;
}
public void save(Employee e) {
try {
tx.begin();
serviceImpl.save(e);
tx.commit();
} catch (Exception e2) {
e2.printStackTrace();
tx.rollback();
}
}
public void update(Employee e) {
try {
tx.begin();
serviceImpl.update(e);
tx.commit();
} catch (Exception e2) {
e2.printStackTrace();
tx.rollback();
}
}
}
package com.swust;
//需要增強的操作,可以是事務,log等
public class TranscantionManager {
public void begin(){
System.out.println("***********開啟事務***********");
}
public void commit(){
System.out.println("***********提交事務***********");
}
public void rollback(){
System.out.println("***********回滾事務***********");
}
}
//需要增強的操作,可以是事務,log等
public class TranscantionManager {
public void begin(){
System.out.println("***********開啟事務***********");
}
public void commit(){
System.out.println("***********提交事務***********");
}
public void rollback(){
System.out.println("***********回滾事務***********");
}
}
我們可以看見,在包裝類中需要實現真實物件的介面,需要引入真實物件並通過構造器注入真實物件,接下來看一看測試類。
package com.swust.test.wapper;
import org.junit.Test;
import com.swust.TranscantionManager;
import com.swust.domain.Employee;
import com.swust.service.impl.EmployeeServiceImpl;
public class TestWapper {
EmployeeServiceWapper wapper=new EmployeeServiceWapper(new EmployeeServiceImpl(), new TranscantionManager());
Employee e=new Employee();
@Test
public void testSave() throws Exception {
wapper.save(e);
}
@Test
public void testUpdate() throws Exception {
wapper.update(e);
}
}
可以看見這種方法確實可以增強業務方法,但是它向外暴露的真實物件,不符合封裝,並且這個類不可複用,一個真實類需要一個包裝類。所以改進一下引出靜態代理。同樣是那些業務,來看看靜態代理類。
二.靜態代理
package com.swust.test.proxy.staticProxy;
import com.swust.TranscantionManager;
import com.swust.domain.Employee;
import com.swust.service.IEmployeeService;
import lombok.Setter;
/**
* @author Mr
* 模仿靜態代理類,使用spring容易管理bean,靜態代理沒有暴露真實物件,而包裝類會暴露
*/
public class EmployeeServiceStaticProxy implements IEmployeeService{
@Setter
private IEmployeeService target;
@Setter
private TranscantionManager tx;
public void save(Employee e) {
try {
tx.begin();
target.save(e);
tx.commit();
} catch (Exception e2) {
e2.printStackTrace();
tx.rollback();
}
}
}
<!-- 配置事務管理 -->
<bean id="txManager" class="com.swust.TranscantionManager"></bean>
<!-- 配置DAO -->
<bean id="employeeDAO" class="com.swust.dao.impl.EmployeeDAOImpl"></bean>
<!-- 配置代理 -->
<bean id="employeeServiceProxy" class="com.swust.test.proxy.staticProxy.EmployeeServiceStaticProxy">
<property name="tx" ref="txManager"></property>
<property name="target">
<!-- 配置真實物件 -->
<bean class="com.swust.service.impl.EmployeeServiceImpl">
<property name="dao" ref="employeeDAO"></property>
</bean>
</property>
</bean>
<bean class="com.swust.service.impl.EmployeeServiceImpl">
<property name="dao" ref="employeeDAO"></property>
</bean>
</property>
</bean>
可以看出靜態代理,並沒有向外暴露真實物件,因為他是無引數的構造器,在配置檔案中也可以隱藏起來,其他和裝飾類基本一樣。
三.JDK動態代理
為了解決類的不可複用性,就引入了動態代理。下面來模仿一下JDK的動態代理(AOP基於動態代理)。
package com.swust.test.proxy.jdkProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.swust.TranscantionManager;
import lombok.Setter;
public class TranscantionManagerHandler implements InvocationHandler{
@Setter
private Object target;//真實物件,可以通過spring注入
@Setter
private TranscantionManager tx;
/**
* @return 代理物件
* 生成的代理類會繼承Proxy類
*/
@SuppressWarnings("unchecked")
public <T>T getProxyObj(){
Object object = Proxy.newProxyInstance(target.getClass().getClassLoader()//通過真實物件得到類載入器
, target.getClass().getInterfaces()//真實物件的介面
, this);//InvocationHandler h:這裡表示當前類物件,即代理輔助類物件,最終會呼叫加強後的invoke方法
return (T) object;
}
/**
* 在真實物件上增強
*
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("輔助類中的method是:"+method+"===args:"+args);
System.out.println("輔助類中的args是:"+args);
System.out.println("========前面做======增強");
Object object = method.invoke(target, args);
System.out.println("========後面做======增強");
return object;
}
}
假設還是需要事務增強,那麼這個類就必須實現InvocationHandler介面,這個介面中只有一個方法
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
然後實現介面的invoke方法。隨後還需要在類中寫一個獲取代理物件的方法(不在這兒寫也可以,獲取的方法一樣)。有兩種方法可以獲取代理物件,這裡使用Proxy.newProxyInstance來獲取。這裡需要傳入三個引數,一個是類載入器,一個是真實物件類實現的介面,一個就是代理輔助類的物件h。我們先看看Proxy的原始碼
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
Class<?> cl = getProxyClass0(loader, intfs);
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
}
}
protected InvocationHandler h;
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
Class<?> cl = getProxyClass0(loader, intfs);
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
}
}
主要看顏色加深部分,可以看見原始碼中的Proxy維護了一個InvocationHandler 物件h,呼叫newProxyInstance方法的時候會先檢查輔助類TranscantionManagerHandler物件h是否為空,如果空,則丟擲空指標。之後利用傳進來的介面和類載入器獲取代理類(代理物件會實現該介面,注意代理類只存在記憶體中,如需存入硬碟需要改變JVM啟動引數,方法自行百度)。隨後根據代理類的含有InvocationHandler 物件h的構造器建立一個代理類的物件返回。之後我們看看測試方法。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class JDKProxyTest {
@Autowired
private TranscantionManagerHandler handle;
@Test
public void testSave() throws Exception {
IEmployeeService service = handle.getProxyObj();//得到的service為代理物件,代理類存在記憶體中
service.save(new Employee());
System.out.println("service物件的類是=======》"+service.getClass());
}
}
通過注入代理輔助類物件呼叫 得到代理物件的方法getProxyObj() 得到代理物件。之後代理物件呼叫自身的業務方法(這裡是save),很多人看到這裡就很好奇,輔助類的TranscantionManagerHandler增強的invoke方法什麼時候呼叫的呢?先看看測試結果
service確實是代理類的物件,業務方法也得到了增強,而在代理輔助類invoke方法中的引數method可以看見是介面中的save方法。
需要知道什麼時候呼叫的invoke就需要反編譯生成的代理類位元組碼(調整虛擬機器引數將代理類位元組碼儲存在硬碟),在生成代理類中$Proxy11繼承了Proxy類,實現了需要被代理類的介面,也就是IEmployeeService,其構造器是如下,還含有三個Object的方法和真實物件的業務方法。
public $Proxy11(InvocationHandler invocationhandler) {
super(invocationhandler);
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals",
new Class[] { Class.forName("java.lang.Object") });
m0 = Class.forName("java.lang.Object").getMethod("hashCode",
new Class[0]);
m3 = Class.forName("***.RealSubject").getMethod("save", //代理方法名稱
new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString",
new Class[0]);
} //static
public final void save() { //代理方法
try {
super.h.invoke(this, m3, null);
return;
} catch (Error e) {
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
super.h.invoke(this, m3, null);
return;
} catch (Error e) {
} catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
在這兒就可以看出了,當代理物件呼叫save方法時,會呼叫
super.h.invoke(this, m3, null);
而在父類Proxy中之前我們講過它維護了一個InvocationHandler的物件h,因此由於代理輔助類實現了一個InvocationHandler的介面,所以最終h會呼叫其實現類TranscantionManagerHandler的invoke方法,因此在
Object object = method.invoke(target, args);
前後就可以做想要的增強了。
總結一下
JDK動態代理,含有增強方法的類需要實現InvocationHandler介面,重寫invoke方法。獲取代理物件的時候需要傳入介面,類載入器,因此需要代理的類必須實現介面。在記憶體中建立代理類的時候,通過傳的介面,建立一個代理類並實現該傳入的介面,繼承Proxy,在生成的代理類的業務方法中呼叫父類維護的InvocationHandler的h物件,並呼叫h.invoke()方法,而由於含有增強方法的代理輔助類實現了InvocationHandler介面,因此實際就會呼叫代理輔助類實現的invoke方法,最後通過代理輔助類裡面注入的真實物件使用method.invoke(target,args)就可以呼叫真實的業務方法了,在在方法前後就可以做增強。
注意:jdk動態代理必須要實現介面
四.CGlib動態代理
原理大部分和jdk動態代理一樣,只是獲取代理物件的方法不同,其餘都一樣。
public class TranscantionManagerHandlerCallbck implements org.springframework.cglib.proxy.InvocationHandler {
@Setter
private Object target;// 真實物件
@SuppressWarnings("unchecked")
public <T> T getProxyObj() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());//代理類需要繼承的父類
enhancer.setCallback(this);//需要增強的物件,即該類的物件
return (T) enhancer.create();
}
/**
* 在真實物件上增強
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("輔助類中的method是:" + method + "===args:" + args);
System.out.println("輔助類中的args是:" + args);
System.out.println("========前面做======增強");
Object object = method.invoke(target, args);
System.out.println("========後面做======增強");
return object;
}
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());//代理類需要繼承的父類
enhancer.setCallback(this);//需要增強的物件,即該類的物件
return (T) enhancer.create();
}
/**
* 在真實物件上增強
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("輔助類中的method是:" + method + "===args:" + args);
System.out.println("輔助類中的args是:" + args);
System.out.println("========前面做======增強");
Object object = method.invoke(target, args);
System.out.println("========後面做======增強");
return object;
}
}
這裡實現的類是spring的cglib包裡的。另外需要cglib代理的類必須是可擴充套件的,因為cglib代理不需要實現介面,只需要繼承父類就可以(繼承需要被代理的類)。
五.AOP
先貼一段aop的配置
<!-- 配置aop:切面what,切入位置where,切入時機when -->
<aop:config>
<!-- what:做什麼增強 -->
<aop:aspect ref="txManager">
<!--where: 在哪些包下的哪些類和介面的哪些方法上做增強 -->
<aop:pointcut expression="execution(* com.swust.common.service.*Service.*(..))" id="txPoint" />
<!-- when:在什麼時候做增強(業務方法前/後/前後)第一個表示在之前做txManager中begin方法的增強 -->
<aop:before method="begin" pointcut-ref="txPoint"/>
<aop:after method="commit" pointcut-ref="txPoint"/>
<aop:after-throwing method="rollback" pointcut-ref="txPoint"/>
</aop:aspect>
</aop:config>
<!-- 配置事務管理 -->
<bean id="txManager" class="com.swust.common.TranscantionManager"></bean>
(有很多配法)在這兒<aspect>裡面配的就相當於需要增強的類,相當於jdk代理中的代理輔助類。而pointcut表示切入點,意思就是我們需要在哪些類的哪些方法上做增強,相當於前面的save業務方法,在這兒指的是service包下的所以帶Service結尾的方法,execution是切的語法,需要引入相關aop和aspectj的包。而再下面就是增強的時機,需要在業務方法之前,之後還是環繞增強,並配置增強的方法。由於所有的bean都交給spring管理,spring會幫我們生成相關的類。關於sop還有其他的一些配置只有瞭解到了aop原理那麼配置那些都是很簡單的,需要了解可以去官方看看。另外,除了xml配置spring還支援註解配置。
注:Spring中AOP代理由Spring的IOC容器負責生成、管理,其依賴關係也由IOC容器負責管理。因此,AOP代理可以直接使用容器中的其它bean例項作為目標,這種關係可由IOC容器的依賴注入提供。Spring建立代理的規則為:
1、預設使用Java動態代理來建立AOP代理,這樣就可以為任何介面例項建立代理了
2、當需要代理的類不是代理介面的時候,Spring會切換為使用CGLIB代理,也可強制使用CGLIB