Spring5 中更優(yōu)雅的第三方 Bean 注入
小伙伴們知道,當(dāng)我們使用 Spring 容器的時(shí)候,如果遇到一些特殊的 Bean,一般來說可以通過如下三種方式進(jìn)行配置:
- 靜態(tài)工廠方法
- 實(shí)例工廠方法
- FactoryBean
不過從 Spring5 開始,在 AbstractBeandefinition 類中多了一個(gè)屬性,對于特殊的 Bean 我們有了更多的選擇:
/**
* Specify a callback for creating an instance of the bean,
* as an alternative to a declaratively specified factory method.
* <p>If such a callback is set, it will override any other constructor
* or factory method metadata. However, bean property population and
* potential annotation-driven injection will still apply as usual.
* @since 5.0
* @see #setConstructorArgumentValues(ConstructorArgumentValues)
* @see #setPropertyValues(MutablePropertyValues)
*/
public void setInstanceSupplier(@Nullable Supplier<?> instanceSupplier) {
this.instanceSupplier = instanceSupplier;
}
/**
* Return a callback for creating an instance of the bean, if any.
* @since 5.0
*/
@Nullable
public Supplier<?> getInstanceSupplier() {
return this.instanceSupplier;
}
接下來松哥就來和大家簡單聊一聊這個(gè)話題。
1. 傳統(tǒng)解決方案
1.1 問題
不知道各位小伙伴們有沒有用過 OkHttp,這是一個(gè)專門做網(wǎng)絡(luò)請求的工具,在微服務(wù)的 HTTP 調(diào)用組件中,我們可以配置底層使用 OkHttp 這個(gè)工具。
一般來說,如果我們想直接使用 OkHttp,代碼如下:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build();
Request getReq = new Request.Builder().get().url("http://www.javaboy.org").build();
Call call = client.newCall(getReq);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println("e.getMessage() = " + e.getMessage());
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
System.out.println("response.body().string() = " + response.body().string());
}
});
先通過建造者模式創(chuàng)建出來一個(gè) OkHttpClient 對象,然后還是建造者模式創(chuàng)建出來 Request 對象,接下來去發(fā)送請求就可以了。那么對于這樣的代碼,我們可以將 OkHttpClient 對象交由 Spring 容器統(tǒng)一管理,那么該如何將 OkHttpClient 注冊到 Spring 容器中呢?
1.2 靜態(tài)工廠方法
首先可以采用靜態(tài)工廠方法,也就是工廠方法是一個(gè)靜態(tài)方法,如下:
public class OkHttpStaticFactory {
private static OkHttpClient okHttpClient;
static {
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build();
}
public static OkHttpClient getOkHttpClient() {
return okHttpClient;
}
}
然后在 Spring 配置文件中進(jìn)行注入:
<bean class="org.javaboy.bean.OkHttpStaticFactory" factory-method="getOkHttpClient" id="httpClient"/>
靜態(tài)工廠的特點(diǎn)是靜態(tài)方法可以直接調(diào)用,并必須要獲取到工廠類的實(shí)例,所以上面配置的時(shí)候只需要指定 factory-method 就可以了。
這就可以了,將來我們?nèi)?Spring 容器中查找一個(gè)名為 httpClient 的對象,拿到手的就是 OkHttpClient 了。
1.3 實(shí)例工廠方法
實(shí)例工廠方法意思就是說工廠方法是一個(gè)實(shí)例方法。如下:
public class OkHttpInstanceFactory {
private volatile static OkHttpClient okHttpClient;
public OkHttpClient getInstance() {
if (okHttpClient == null) {
synchronized (OkHttpInstanceFactory.class) {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build();
}
}
}
return okHttpClient;
}
}
這是一個(gè)簡單的單例模式。但是這里的工廠方法是一個(gè)實(shí)例方法,實(shí)例方法的調(diào)用必須得先獲取到對象然后才能調(diào)用實(shí)例方法,因此配置方式如下:
<bean class="org.javaboy.bean.OkHttpInstanceFactory" id="httpInstanceFactory"/>
<bean factory-bean="httpInstanceFactory" factory-method="getInstance" id="httpClient"/>
好啦,接下來我們就可以去 Spring 容器中獲取一個(gè)名為 httpClient 的對象了,拿到手的就是 OkHttpClient 實(shí)例。
1.4 FactoryBean
當(dāng)然,也可以通過 FactoryBean 來解決上述問題,F(xiàn)actoryBean 松哥在之前的文章中剛剛和大家介紹過,我們來看下:
public class OkHttpClientFactoryBean implements FactoryBean<OkHttpClient> {
@Override
public OkHttpClient getObject() throws Exception {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build();
}
@Override
public Class<?> getObjectType() {
return OkHttpClient.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
最后在 Spring 中配置即可:
<bean class="org.javaboy.bean.OkHttpClientFactoryBean" id="httpClient"/>
這個(gè)就不做過多解釋了,不熟悉的小伙伴可以翻看前面的文章。
上面這三種方案都是傳統(tǒng)方案。
特別是前兩種,其實(shí)我們用的比較少,前兩種有一個(gè)缺陷,就是我們配置的的 factory-method 都是通過反射來調(diào)用的,通過反射調(diào)用的話,多多少少性能受點(diǎn)影響。
這種 factory-method 在 Spring 中處理的源碼執(zhí)行時(shí)序圖如下:
圖片
所以最終反射是在 SimpleInstantiationStrategy#instantiate 方法中執(zhí)行的,就是大家非常熟悉的反射代碼了:
@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
@Nullable Object factoryBean, final Method factoryMethod, Object... args) {
ReflectionUtils.makeAccessible(factoryMethod);
Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get();
try {
currentlyInvokedFactoryMethod.set(factoryMethod);
Object result = factoryMethod.invoke(factoryBean, args);
if (result == null) {
result = new NullBean();
}
return result;
}
finally {
if (priorInvokedFactoryMethod != null) {
currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod);
}
else {
currentlyInvokedFactoryMethod.remove();
}
}
}
好了,這是傳統(tǒng)的解決方案。
2. Spring5 解決方案
Spring5 中開始提供了 Supplier,可以通過接口回調(diào)獲取到一個(gè) Bean 的實(shí)例,這種方式顯然性能更好一些。
如下:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(Book.class);
definition.setInstanceSupplier((Supplier<Book>) () -> {
Book book = new Book();
book.setName("深入淺出 Spring Security");
book.setAuthor("江南一點(diǎn)雨");
return book;
});
ctx.registerBeanDefinition("b1", definition);
ctx.refresh();
Book b = ctx.getBean("b1", Book.class);
System.out.println("b = " + b);
關(guān)鍵就是通過調(diào)用 BeanDefinition 的 setInstanceSupplier 方法去設(shè)置回調(diào)。當(dāng)然,上面這段代碼還可以通過 Lambda 進(jìn)一步簡化:
public class BookSupplier {
public Book getBook() {
Book book = new Book();
book.setName("深入淺出 Spring Security");
book.setAuthor("江南一點(diǎn)雨");
return book;
}
}
然后調(diào)用這個(gè)方法即可:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(Book.class);
BookSupplier bookSupplier = new BookSupplier();
definition.setInstanceSupplier(bookSupplier::getBook);
ctx.registerBeanDefinition("b1", definition);
ctx.refresh();
Book b = ctx.getBean("b1", Book.class);
System.out.println("b = " + b);
這是不是更有一點(diǎn) Lambda 的感覺了~
在 Spring 源碼中,處理獲取 Bean 實(shí)例的時(shí)候,有如下一個(gè)分支,就是處理 Supplier 這種情況的:
AbstractAutowireCapableBeanFactory#createBeanInstance
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
// Make sure bean class is actually resolved at this point.
Class<?> beanClass = resolveBeanClass(mbd, beanName);
if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
}
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
if (instanceSupplier != null) {
return obtainFromSupplier(instanceSupplier, beanName);
}
if (mbd.getFactoryMethodName() != null) {
return instantiateUsingFactoryMethod(beanName, mbd, args);
}
//...
return instantiateBean(beanName, mbd);
}
@Nullable
private Object obtainInstanceFromSupplier(Supplier<?> supplier, String beanName) {
String outerBean = this.currentlyCreatedBean.get();
this.currentlyCreatedBean.set(beanName);
try {
if (supplier instanceof InstanceSupplier<?> instanceSupplier) {
return instanceSupplier.get(RegisteredBean.of((ConfigurableListableBeanFactory) this, beanName));
}
if (supplier instanceof ThrowingSupplier<?> throwableSupplier) {
return throwableSupplier.getWithException();
}
return supplier.get();
}
}
上面 obtainFromSupplier 這個(gè)方法,最終會(huì)調(diào)用到第二個(gè)方法。第二個(gè)方法中的 supplier.get(); 其實(shí)最終就調(diào)用到我們自己寫的 getBook 方法了。