Spring 例項化方式有幾種?為什麼會用到 Cglib?

小傅哥發表於2021-05-31


作者:小傅哥
部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!?

《Spring 手擼專欄》目錄

一、前言

技術成長,是對場景設計細節不斷的雕刻!

你覺得自己的技術什麼時候得到了快速的提高,是CRUD寫的多了以後嗎?想都不要想,絕對不可能!CRUD寫的再多也只是能滿足你作為一個搬磚工具人,敲擊少邏輯流水程式碼的速度而已,而程式設計能力這一塊,除了最開始的從不熟練到熟練以外,就很少再有其他提升了。

那你可能會想什麼才是程式設計能力提升?其實更多的程式設計能力的提升是你對複雜場景的架構把控以及對每一個技術實現細節點的不斷用具有規模體量的流量衝擊驗證時,是否能保證系統穩定執行從而決定你見識了多少、學到了多少、提升了多少!

最終當你在接一個產品需求時,開始思考程式資料結構的設計核心功能的演算法邏輯實現整體服務的設計模式使用系統架構的搭建方式應用叢集的部署結構,那麼也就是的程式設計能力真正提升的時候!

二、目標

這一章節的目標主要是為了解決上一章節我們埋下的坑,那是什麼坑呢?其實就是一個關於 Bean 物件在含有建構函式進行例項化的坑。

在上一章節我們擴充了 Bean 容器的功能,把例項化物件交給容器來統一處理,但在我們例項化物件的程式碼裡並沒有考慮物件類是否含建構函式,也就是說如果我們去例項化一個含有建構函式的物件那麼就要拋異常了。

怎麼驗證?其實就是把 UserService 新增一個含入參資訊的建構函式就可以,如下:

public class UserService {

    private String name;

    public UserService(String name) {
        this.name = name;
    }  

    // ...
}

報錯如下:

java.lang.InstantiationException: cn.bugstack.springframework.test.bean.UserService

	at java.lang.Class.newInstance(Class.java:427)
	at cn.bugstack.springframework.test.ApiTest.test_newInstance(ApiTest.java:51)
	...

發生這一現象的主要原因就是因為 beanDefinition.getBeanClass().newInstance(); 例項化方式並沒有考慮建構函式的入參,所以就這個坑就在這等著你了!那麼我們的目標就很明顯了,來把這個坑填平!

三、設計

填平這個坑的技術設計主要考慮兩部分,一個是串流程從哪合理的把建構函式的入參資訊傳遞到例項化操作裡,另外一個是怎麼去例項化含有建構函式的物件。

圖 4-1

  • 參考 Spring Bean 容器原始碼的實現方式,在 BeanFactory 中新增 Object getBean(String name, Object... args) 介面,這樣就可以在獲取 Bean 時把建構函式的入參資訊傳遞進去了。
  • 另外一個核心的內容是使用什麼方式來建立含有建構函式的 Bean 物件呢?這裡有兩種方式可以選擇,一個是基於 Java 本身自帶的方法 DeclaredConstructor,另外一個是使用 Cglib 來動態建立 Bean 物件。Cglib 是基於位元組碼框架 ASM 實現,所以你也可以直接通過 ASM 操作指令碼來建立物件

四、實現

1. 工程結構

small-spring-step-03
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework.beans
    │           ├── factory
    │           │   ├── factory
    │           │   │   ├── BeanDefinition.java
    │           │   │   └── SingletonBeanRegistry.java
    │           │   ├── support
    │           │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   ├── AbstractBeanFactory.java
    │           │   │   ├── BeanDefinitionRegistry.java
    │           │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   ├── DefaultListableBeanFactory.java
    │           │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   ├── InstantiationStrategy.java
    │           │   │   └── SimpleInstantiationStrategy.java
    │           │   └── BeanFactory.java
    │           └── BeansException.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   └── UserService.java
                └── ApiTest.java

工程原始碼公眾號「bugstack蟲洞棧」,回覆:Spring 專欄,獲取完整原始碼

Spring Bean 容器類關係,如圖 4-2

圖 4-2

本章節“填坑”主要是在現有工程中新增 InstantiationStrategy 例項化策略介面,以及補充相應的 getBean 入參資訊,讓外部呼叫時可以傳遞建構函式的入參並順利例項化。

2. 新增 getBean 介面

cn.bugstack.springframework.beans.factory.BeanFactory

public interface BeanFactory {

    Object getBean(String name) throws BeansException;

    Object getBean(String name, Object... args) throws BeansException;

}
  • BeanFactory 中我們過載了一個含有入參資訊 args 的 getBean 方法,這樣就可以方便的傳遞入參給建構函式例項化了。

3. 定義例項化策略介面

cn.bugstack.springframework.beans.factory.support.InstantiationStrategy

public interface InstantiationStrategy {

    Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException;

}
  • 在例項化介面 instantiate 方法中新增必要的入參資訊,包括:beanDefinition、 beanName、ctor、args
  • 其中 Constructor 你可能會有一點陌生,它是 java.lang.reflect 包下的 Constructor 類,裡面包含了一些必要的類資訊,有這個引數的目的就是為了拿到符合入參資訊相對應的建構函式。
  • 而 args 就是一個具體的入參資訊了,最終例項化時候會用到。

4. JDK 例項化

cn.bugstack.springframework.beans.factory.support.SimpleInstantiationStrategy

public class SimpleInstantiationStrategy implements InstantiationStrategy {

    @Override
    public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
        Class clazz = beanDefinition.getBeanClass();
        try {
            if (null != ctor) {
                return clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args);
            } else {
                return clazz.getDeclaredConstructor().newInstance();
            }
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new BeansException("Failed to instantiate [" + clazz.getName() + "]", e);
        }
    }

}
  • 首先通過 beanDefinition 獲取 Class 資訊,這個 Class 資訊是在 Bean 定義的時候傳遞進去的。
  • 接下來判斷 ctor 是否為空,如果為空則是無建構函式例項化,否則就是需要有建構函式的例項化。
  • 這裡我們重點關注有建構函式的例項化,例項化方式為 clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args);,把入參資訊傳遞給 newInstance 進行例項化。

5. Cglib 例項化

cn.bugstack.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy

public class CglibSubclassingInstantiationStrategy implements InstantiationStrategy {

    @Override
    public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(beanDefinition.getBeanClass());
        enhancer.setCallback(new NoOp() {
            @Override
            public int hashCode() {
                return super.hashCode();
            }
        });
        if (null == ctor) return enhancer.create();
        return enhancer.create(ctor.getParameterTypes(), args);
    }

}
  • 其實 Cglib 建立有建構函式的 Bean 也非常方便,在這裡我們更加簡化的處理了,如果你閱讀 Spring 原始碼還會看到 CallbackFilter 等實現,不過我們目前的方式並不會影響建立。

6. 建立策略呼叫

cn.bugstack.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory {

    private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
        } catch (Exception e) {
            throw new BeansException("Instantiation of bean failed", e);
        }

        addSingleton(beanName, bean);
        return bean;
    }

    protected Object createBeanInstance(BeanDefinition beanDefinition, String beanName, Object[] args) {
        Constructor constructorToUse = null;
        Class<?> beanClass = beanDefinition.getBeanClass();
        Constructor<?>[] declaredConstructors = beanClass.getDeclaredConstructors();
        for (Constructor ctor : declaredConstructors) {
            if (null != args && ctor.getParameterTypes().length == args.length) {
                constructorToUse = ctor;
                break;
            }
        }
        return getInstantiationStrategy().instantiate(beanDefinition, beanName, constructorToUse, args);
    }

}
  • 首先在 AbstractAutowireCapableBeanFactory 抽象類中定義了一個建立物件的例項化策略屬性類 InstantiationStrategy instantiationStrategy,這裡我們選擇了 Cglib 的實現類。
  • 接下來抽取 createBeanInstance 方法,在這個方法中需要注意 Constructor 代表了你有多少個建構函式,通過 beanClass.getDeclaredConstructors() 方式可以獲取到你所有的建構函式,是一個集合。
  • 接下來就需要迴圈比對出建構函式集合與入參資訊 args 的匹配情況,這裡我們對比的方式比較簡單,只是一個數量對比,而實際 Spring
    原始碼中還需要比對入參型別,否則相同數量不同入參型別的情況,就會拋異常了。

五、測試

1. 事先準備

cn.bugstack.springframework.test.bean.UserService

public class UserService {

    private String name;

    public UserService(String name) {
        this.name = name;
    }

    public void queryUserInfo() {
        System.out.println("查詢使用者資訊:" + name);
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("");
        sb.append("").append(name);
        return sb.toString();
    }
}
  • 這裡唯一多在 UserService 中新增的就是一個有 name 入參的建構函式,方便我們驗證這樣的物件是否能被例項化。

2. 測試用例

cn.bugstack.springframework.test.ApiTest

@Test
public void test_BeanFactory() {
    // 1.初始化 BeanFactory
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

    // 2. 注入bean
    BeanDefinition beanDefinition = new BeanDefinition(UserService.class);
    beanFactory.registerBeanDefinition("userService", beanDefinition);

    // 3.獲取bean
    UserService userService = (UserService) beanFactory.getBean("userService", "小傅哥");
    userService.queryUserInfo();
}
  • 在此次的單元測試中除了包括;Bean 工廠、註冊 Bean、獲取 Bean,三個步驟,還額外增加了一次物件的獲取和呼叫。這裡主要測試驗證單例物件的是否正確的存放到了快取中。
  • 此外與上一章節測試過程中不同的是,我們把 UserService.class 傳遞給了 BeanDefinition 而不是像上一章節那樣直接 new UserService() 操作。

3. 測試結果

查詢使用者資訊:小傅哥

Process finished with exit code 0
  • 從測試結果來看,最大的變化就是可以滿足帶有建構函式的物件,可以被例項化了。
  • 你可以嘗試分別使用兩種不同的例項化策略,來進行例項化。SimpleInstantiationStrategyCglibSubclassingInstantiationStrategy

4. 操作案例

這裡我們再把幾種不同方式的例項化操作,放到單元測試中,方便大家比對學習。

4.1 無建構函式

@Test
public void test_newInstance() throws IllegalAccessException, InstantiationException {
    UserService userService = UserService.class.newInstance();
    System.out.println(userService);
}
  • 這種方式的例項化也是我們在上一章節實現 Spring Bean 容器時直接使用的方式

4.2 驗證有建構函式例項化

@Test
public void test_constructor() throws Exception {
    Class<UserService> userServiceClass = UserService.class;
    Constructor<UserService> declaredConstructor = userServiceClass.getDeclaredConstructor(String.class);
    UserService userService = declaredConstructor.newInstance("小傅哥");
    System.out.println(userService);
}
  • 從最簡單的操作來看,如果有建構函式的類需要例項化時,則需要使用 getDeclaredConstructor 獲取建構函式,之後在通過傳遞引數進行例項化。

4.3 獲取建構函式資訊

@Test
public void test_parameterTypes() throws Exception {
    Class<UserService> beanClass = UserService.class;
    Constructor<?>[] declaredConstructors = beanClass.getDeclaredConstructors();
    Constructor<?> constructor = declaredConstructors[0];
    Constructor<UserService> declaredConstructor = beanClass.getDeclaredConstructor(constructor.getParameterTypes());
    UserService userService = declaredConstructor.newInstance("小傅哥");
    System.out.println(userService);
  • 這個案例中其實最核心的點在於獲取一個類中所有的建構函式,其實也就是這個方法的使用 beanClass.getDeclaredConstructors()

4.4 Cglib 例項化

@Test
public void test_cglib() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(UserService.class);
    enhancer.setCallback(new NoOp() {
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    });
    Object obj = enhancer.create(new Class[]{String.class}, new Object[]{"小傅哥"});
    System.out.println(obj);
}
  • 此案例演示使用非常簡單,但關於 Cglib 在 Spring 容器中的使用非常多,也可以深入的學習一下 Cglib 的擴充套件知識。

六、總結

  • 本章節的主要以完善例項化操作,增加 InstantiationStrategy 例項化策略介面,並新增了兩個例項化類。這部分類的名稱與實現方式基本是 Spring 框架的一個縮小版,大家在學習過程中也可以從 Spring 原始碼找到對應的程式碼。
  • 從我們不斷的完善增加需求可以看到的,當你的程式碼結構設計的較為合理的時候,就可以非常容易且方便的進行擴充套件不同屬性的類職責,而不會因為需求的增加導致類結構混亂。所以在我們自己業務需求實現的過程中,也要儘可能的去考慮一個良好的擴充套件性以及拆分好類的職責。
  • 動手是學習起來最快的方式,不要讓眼睛是感覺看會了,但上手操作就廢了。也希望有需要的讀者可以親手操作一下,把你的想法也融入到可落地實現的程式碼裡,看看想的和做的是否一致。

七、系列推薦

相關文章