通过 Spring 5 中 Supplier 来获取 Bean
今天遇到了一个面试题:Spring 中 Bean 的实例化有哪些方式?
大家知道,Spring 中 Bean 的配置方式有很多种,但是正常来说,无论你是 XML 文件配置,还是用类似 @Service
注解这种配置,本质上最终都是通过反射去完成 Bean 的初始化的;@Bean
注解则稍微特殊一点,往往我们在 @Bean
注解中是自己 new
出来目标 Bean,但是 @Bean
注解所标记的方法也是通过反射调用的。
似乎 Bean 的实例化离不开反射。
那么除了上面这些方案,还有没有其他方案呢?本文和大家探讨一下这个问题。
以下内容基于 Spring6.0.4。
总所周知,当使用 Spring 容器的时候,如果遇到一些特殊的 Bean,一般来说可以通过如下三种方式进行配置:
- 静态工厂方法
- 实例工厂方法
FactoryBean
不过从 Spring5 开始,在 AbstractBeandefinition
类中多了一个属性,对于特殊的 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;
}
1、传统解决方案
1.1、问题
大家应该有用过 OkHttp,这是一个用于 HTTP 请求的工具,在微服务的 HTTP 调用组件中,我们可以配置底层使用 OkHttp 这个工具作为 HTTP 客户端。
一般来说,如果我们想直接使用 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) {
.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());
}
});
先通过建造者模式创建出来一个 OkHttpClient
对象,然后还是建造者模式创建出来 Request
对象,接下来去发送请求就可以了。那么对于这样的代码,我们可以将 OkHttpClient
对象交由 Spring 容器统一管理,那么该如何将 OkHttpClient
注册到 Spring 容器中呢?
1.2、静态工厂方法
首先可以采用静态工厂方法,也就是工厂方法是一个静态方法,如下:
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 配置文件中进行注入:
<bean class="org.javaboy.bean.OkHttpStaticFactory" factory-method="getOkHttpClient" id="httpClient"/>
静态工厂的特点是静态方法可以直接调用,并不必要获取到工厂类的实例,所以上面配置的时候只需要指定 factory-method
就可以了。
这就可以了,将来我们去 Spring 容器中查找一个名为 httpClient
的对象,拿到手的就是 OkHttpClient
了。
1.3、实例工厂方法
实例工厂方法意思就是说工厂方法是一个实例方法。如下:
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;
}
}
这是一个简单的单例模式。但是这里的工厂方法是一个实例方法,实例方法的调用必须得先获取到对象然后才能调用实例方法,因此配置方式如下:
<bean class="org.javaboy.bean.OkHttpInstanceFactory" id="httpInstanceFactory"/>
<bean factory-bean="httpInstanceFactory" factory-method="getInstance" id="httpClient"/>
好了,接下来我们就可以去 Spring 容器中获取一个名为 httpClient
的对象了,最终得到的就是 OkHttpClient
实例。
1.4、FactoryBean
当然,也可以通过 FactoryBean
来解决上述问题:
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"/>
这个就不做过多解释了,感兴趣可以参阅 官方文档。
上面这三种方案都是传统方案。
特别是前两种,其实我们用的比较少,前两种有一个缺陷,就是我们配置的的 factory-method
都是通过反射来调用的,通过反射调用的话,多多少少性能受点影响。
这种 factory-method
在 Spring 中处理的源码执行时序图如下:
所以最终反射是在 SimpleInstantiationStrategy#instantiate
方法中执行的,就是大家非常熟悉的反射代码了:
@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();
}
}
}
好了,这是传统的解决方案。
2、Spring 5 的解决方案
Spring5 中开始提供了 Supplier
,可以通过接口回调获取到一个 Bean 的实例,这种方式显然性能更好一些。
如下:
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("江南一点雨");
return book;
});
ctx.registerBeanDefinition("b1", definition);
ctx.refresh();
Book b = ctx.getBean("b1", Book.class);
System.out.println("b = " + b);
关键就是通过调用 BeanDefinition
的 setInstanceSupplier
方法去设置回调。当然,上面这段代码还可以通过 Lambda 进一步简化:
public class BookSupplier {
public Book getBook() {
Book book = new Book();
book.setName("深入浅出 Spring Security");
book.setAuthor("江南一点雨");
return book;
}
}
然后调用这个方法即可:
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);
这是不是更有一点 Lambda 的感觉了?
在 Spring 源码中,处理获取 Bean 实例的时候,有如下一个分支,就是处理 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
这个方法,最终会调用到第二个方法。第二个方法中的 supplier.get();
其实最终就调用到我们自己写的 getBook
方法了。
如上,这是从 Spring5 开始结合 Lamdba 的一种 Bean 注入方式。
Ref:https://mp.weixin.qq.com/s/yKFqDTLwOSIQ2OOzbgSO6w