Spring 實例化方式有幾種?為什麼會用到 Cglib?
作者:小傅哥
博客://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!😄
《Spring 手擼專欄》目錄
- [x] 第 1 章:開篇介紹,我要帶你擼 Spring 啦!
- [x] 第 2 章:小試牛刀,實現一個簡單的Bean容器
- [x] 第 3 章:初顯身手,運用設計模式,實現 Bean 的定義、註冊、獲取
- [x] 第 4 章:嶄露頭角,基於Cglib實現含構造函數的類實例化策略
- [ ] 第 5 章:給 Bean 對象填充屬性信息
- [ ] 第 6 章:待歸檔…
一、前言
技術成長,是對場景設計細節不斷的雕刻!
你覺得自己的技術什麼時候得到了快速的提高,是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();
實例化方式並沒有考慮構造函數的入參,所以就這個坑就在這等着你了!那麼我們的目標就很明顯了,來把這個坑填平!
三、設計
填平這個坑的技術設計主要考慮兩部分,一個是串流程從哪合理的把構造函數的入參信息傳遞到實例化操作里,另外一個是怎麼去實例化含有構造函數的對象。
- 參考 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
本章節「填坑」
主要是在現有工程中添加 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
- 從測試結果來看,最大的變化就是可以滿足帶有構造函數的對象,可以被實例化了。
- 你可以嘗試分別使用兩種不同的實例化策略,來進行實例化。
SimpleInstantiationStrategy
、CglibSubclassingInstantiationStrategy
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 源碼找到對應的代碼。
- 從我們不斷的完善增加需求可以看到的,當你的代碼結構設計的較為合理的時候,就可以非常容易且方便的進行擴展不同屬性的類職責,而不會因為需求的增加導致類結構混亂。所以在我們自己業務需求實現的過程中,也要儘可能的去考慮一個良好的擴展性以及拆分好類的職責。
- 動手是學習起來最快的方式,不要讓眼睛是感覺看會了,但上手操作就廢了。也希望有需要的讀者可以親手操作一下,把你的想法也融入到可落地實現的代碼里,看看想的和做的是否一致。
七、系列推薦
- 面經手冊 · 第1篇《認知自己的技術棧盲區》
- 剛火了的中台轉頭就拆,一大波公司放不下又拿不起來!
- 工作兩三年了,整不明白架構圖都畫啥?
- 初識領域驅動設計DDD落地
- 面試現場:小夥伴美團一面的分享和分析(含解答)