写点什么

Spring5 中更优雅的第三方 Bean 注入

作者:江南一点雨
  • 2023-07-11
    广东
  • 本文字数:4524 字

    阅读完需:约 15 分钟

Spring5 中更优雅的第三方 Bean 注入

小伙伴们知道,当我们使用 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 */@Nullablepublic Supplier<?> getInstanceSupplier() { return this.instanceSupplier;}
复制代码


接下来松哥就来和大家简单聊一聊这个话题。

1. 传统解决方案

1.1 问题

不知道各位小伙伴们有没有用过 OkHttp,这是一个专门做网络请求的工具,在微服务的 HTTP 调用组件中,我们可以配置底层使用 OkHttp 这个工具。


一般来说,如果我们想直接使用 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());    }});
复制代码


先通过建造者模式创建出来一个 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 来解决上述问题,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 方法中执行的,就是大家非常熟悉的反射代码了:


@Overridepublic 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. Spring5 解决方案

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);}
@Nullableprivate 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 注入方式,感兴趣的小伙伴可以试试哦~

发布于: 刚刚阅读数: 5
用户头像

技术宅 2019-04-09 加入

Java猿

评论

发布
暂无评论
Spring5 中更优雅的第三方 Bean 注入_Java_江南一点雨_InfoQ写作社区