代理模式——結合SpringAOP講解

追風少年10發表於2019-03-02

前言

  筆者最近想學學Spring原始碼,各種設計模式在Spring的原始碼中運用得淋漓盡致,筆者也不得不感嘆原來廣大的開發者一直站在牛人的肩膀上進行程式設計。談到Spring,面試問得最多的就是Spring的兩大核心,IOC以及AOP。IOC本質上來說就是對bean反射以及依賴注入,管理bean生命週期的容器。而AOP本質上就是動態代理。今天筆者就來講講動態代理。接下來我將從下面幾個方面闡述動態代理:

  • 靜態代理
  • JDK動態代理
  • CGlib動態代理
  • 談談筆者自己在實際專案使用AOP遇到的坑

靜態代理

  靜態代理很簡單,我們們自己在寫程式碼的時候都會寫到這種類似靜態代理的程式碼。簡單來說,就是把被代理類作為引數傳給代理類的構造方法,讓代理類替被代理類實現更強大的功能。

 1package com.bingo.designPatterns.proxy;
2
3/**
4 * Description:靜態代理
5 * User: bingo
6 */

7
8public class StaticProxyTest {
9
10    public static void main(String[] args) {
11
12        UserService userService = new UserService();
13
14        LogProxy logProxy = new LogProxy(userService);
15        logProxy.addUser();
16        logProxy.deleteUser();
17    }
18}
19
20interface IUserService{
21    void addUser();
22    void deleteUser();
23}
24
25
26class UserService implements IUserService{
27
28    @Override
29    public void addUser() {
30        System.out.println("新增使用者");
31    }
32
33    @Override
34    public void deleteUser() {
35        System.out.println("刪除使用者");
36    }
37}
38
39//日誌代理
40class LogProxy implements IUserService{
41
42    //目標類
43    private UserService target;
44
45    public LogProxy(UserService target){
46        this.target = target;
47    }
48
49    @Override
50    public void addUser() {
51        System.out.println("記錄日誌開始");
52        target.addUser();
53        System.out.println("記錄日誌結束");
54    }
55
56    @Override
57    public void deleteUser() {
58        System.out.println("記錄日誌開始");
59        target.deleteUser();
60        System.out.println("記錄日誌結束");
61    }
62}
複製程式碼

  雖然靜態代理實現比較簡單,但是在實際專案中我們需要為每個類都寫一個代理類,需要寫很多重複冗餘的程式碼,不利於程式碼的解耦與擴充套件。但是動態代理便很好的解決了上述問題,真真正正地實現了業務邏輯程式碼與增強功能程式碼的解耦。

動態代理

  在Spring原始碼中,用到的動態代理主要有兩種,JDK動態代理以及CGLib動態代理。兩者主要區別是:
  • JDK動態代理一般針對實現了介面的類生成代理。(下面講AOP遇到的坑時更能理解這句話的含義)
  • 目標物件沒有實現介面,則預設會採用CGLIB代理。如果目標物件實現了介面,可以強制使用CGLIB實現代理(新增CGLIB庫)
    其實,上面的區別闡述雖然不夠完全,但足以區分二者。
相同點:
  • 兩種動態代理本質上都是:位元組碼組裝
AOP動態代理的應用場景:
  • 日誌
  • 事務
  • 許可權
  • 快取
  • 懶載入

JDK動態代理

JDK動態代理的代理類一般需要實現介面

 1package com.bingo.designPatterns.proxy;
2
3import java.lang.reflect.InvocationHandler;
4import java.lang.reflect.Method;
5import java.lang.reflect.Proxy;
6
7/**
8 * Description: jdk動態代理
9 * User: bingo
10 */

11public class JdkProxyTest {
12
13    public static void main(String[] args) {
14        IPersonService personService = JdkDynamicProxy.getProxy();
15        personService.addPerson();
16        personService.deletePerson();
17    }
18}
19
20interface IPersonService{
21    void addPerson();
22    void deletePerson();
23}
24
25class PersonService implements IPersonService{
26    @Override
27    public void addPerson() {
28        System.out.println("新增人物");
29    }
30
31    @Override
32    public void deletePerson() {
33        System.out.println("刪除人物");
34    }
35}
36
37
38/**
39 * newProxyInstance方法引數說明:
40 *      ClassLoader loader:指定當前目標物件使用的類載入器,獲取載入器的方法是固定的
41 *      Class<?>[] interfaces:指定目標物件實現的介面的型別,使用泛型方式確認型別
42 *      InvocationHandler:指定動態處理器,執行目標物件的方法時,會觸發事件處理器的方法
43 */

44class JdkDynamicProxy{
45
46    public static IPersonService getProxy(){
47
48        IPersonService personService = new PersonService();
49
50        IPersonService proxy = (IPersonService) Proxy.newProxyInstance(IPersonService.class.getClassLoader(), new Class<?>[]{IPersonService.class}, new InvocationHandler() {
51
52            @Override
53            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
54                System.out.println("記錄日誌開始");
55                Object obj = method.invoke(personService, args);
56                System.out.println("記錄日誌結束");
57                return obj;
58            }
59        });
60
61        return proxy;
62    }
63}
複製程式碼

CGLib動態代理

  需要匯入cglib.jar,asm.jar包才能使用此代理

 1package com.bingo.designPatterns.proxy;
2
3import net.sf.cglib.proxy.Enhancer;
4import net.sf.cglib.proxy.MethodInterceptor;
5import net.sf.cglib.proxy.MethodProxy;
6
7import java.lang.reflect.Method;
8
9/**
10 * Description: cglib動態代理
11 * User: bingo
12 */

13
14public class CglibProxyTest {
15    public static void main(String[] args) {
16
17        CglibProxy proxy = new CglibProxy();
18        Train t = (Train)proxy.getProxy(Train.class);
19        t.move();
20    }
21}
22
23class Train {
24
25    public void move(){
26        System.out.println("火車行駛中...");
27    }
28}
29
30class CglibProxy implements MethodInterceptor {
31
32    private Enhancer enhancer = new Enhancer();
33
34    public Object getProxy(Class clazz){
35        //設定建立子類的類
36        enhancer.setSuperclass(clazz);
37        enhancer.setCallback(this);
38
39        return enhancer.create();
40    }
41
42    /**
43     * 攔截所有目標類方法的呼叫
44     * obj  目標類的例項
45     * m   目標方法的反射物件
46     * args  方法的引數
47     * proxy代理類的例項
48     */

49    @Override
50    public Object intercept(Object obj, Method m, Object[] args,
51                            MethodProxy proxy)
 throws Throwable 
{
52        System.out.println("日誌開始...");
53        //代理類呼叫父類的方法
54        proxy.invokeSuper(obj, args);
55        System.out.println("日誌結束...");
56        return null;
57    }
58}
複製程式碼

專案中使用AOP遇到的坑

  筆者之前在開發小程式服務介面時遇到的問題:
1.在Spring配置檔案中配置了事務管理器,如下:

1<!--事務管理器配置,單資料來源事務-->
2<tx:annotation-driven transaction-manager="transactionManager" />  
3<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
4    <property name="dataSource" ref="dataSource" />
5</bean>
複製程式碼

2.配置了事務管理器後,加入了@Transactional註解

1@Service
2@Transactional
3public class AccountService{
4    //to do something
5}
複製程式碼

  乍一看,這個配置與使用都沒什麼毛病,對吧,但是專案一放到Tomcat服務上就報錯,具體報什麼錯,筆者已經記不得了,但是筆者很記得出錯的原因就是transaction註解的問題,於是沒辦法,翻看原來公司的專案程式碼,對比自己的程式碼,發現各種配置也不差,不知道問題出在哪,但是原來的專案啟動正常,我的專案卻報錯。細看發現,公司原來的專案中Service都定義了一個介面作為規範,@Transactional註解都是加在介面實現類上的。於是乎,我半信半疑的為每個Service的類定義了一個介面規範,實現類加上@Transactional註解,如下:

1@Service
2@Transactional
3public class AccountService implements IAccountService {
4    //to do something
5}
複製程式碼

配置沒變,只是簡單地為Service層定義了介面,並實現介面,專案就執行正常了。

問題根源出在哪呢?就在這裡:
1<tx:annotation-driven transaction-manager="txManager"/>
複製程式碼

上面配置預設啟用JDK動態代理,JDK只能代理介面不能代理類。而我的專案中用的是這個配置,卻因為沒有定義Service介面導致專案啟動報錯。

如果需要使用CGLIB動態代理:配置如下:

1<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/> 
複製程式碼

  不知道有沒有讀者遇到過跟我同樣的坑,如果不瞭解動態代理,就有可能出現和我相同的問題。但是寫程式規範很重要,MVC三層結構中,除了Controller層,Service、Dao層都需要定義介面,這是企業開發的規範。那段時間還是個剛畢業不久的實習生,難免有時候會寫出這種不規範的程式碼,但是希望以後能夠越來越規範,多看看《阿里巴巴Java開發手冊》,多看看原始碼,看看大牛們寫的程式碼,相信不久之後自己也可以寫出如此優雅的程式碼,讓程式設計變成一種藝術。

  我是廣州的java程式設計師小彬,剛畢業半年,一直致力於java的學習,對我的文章感興趣可以關注我的微信公眾號(J2彬彬),感謝支援!

相關文章