以下内容基于 Spring6.0.4。
小伙伴们知道,当我们使用 Spring 容器的时候,如果遇到一些特殊的 Bean,一般来说可以通过如下三种方式进行配置:
静态工厂方法
实例工厂方法
FactoryBean
不过从 Spring5 开始,在 AbstractBeandefinition 类中多了一个属性,对于特殊的 Bean 我们有了更多的选择:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void setInstanceSupplier (@Nullable Supplier<?> instanceSupplier) { this .instanceSupplier = instanceSupplier; } @Nullable public Supplier<?> getInstanceSupplier() { return this .instanceSupplier; }
接下来松哥就来和大家简单聊一聊这个话题。
1. 传统解决方案 1.1 问题 不知道各位小伙伴们有没有用过 OkHttp,这是一个专门做网络请求的工具,在微服务的 HTTP 调用组件中,我们可以配置底层使用 OkHttp 这个工具。
一般来说,如果我们想直接使用 OkHttp,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 静态工厂方法 首先可以采用静态工厂方法,也就是工厂方法是一个静态方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 配置文件中进行注入:
1 <bean class ="org.javaboy.bean.OkHttpStaticFactory" factory-method ="getOkHttpClient" id ="httpClient" />
静态工厂的特点是静态方法可以直接调用,并不必要获取到工厂类的实例,所以上面配置的时候只需要指定 factory-method
就可以了。
这就可以了,将来我们去 Spring 容器中查找一个名为 httpClient 的对象,拿到手的就是 OkHttpClient 了。
1.3 实例工厂方法 实例工厂方法意思就是说工厂方法是一个实例方法。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; } }
这是一个简单的单例模式。但是这里的工厂方法是一个实例方法,实例方法的调用必须得先获取到对象然后才能调用实例方法,因此配置方式如下:
1 2 <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 松哥在之前的文章中刚刚和大家介绍过,我们来看下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 中配置即可:
1 <bean class ="org.javaboy.bean.OkHttpClientFactoryBean" id ="httpClient" />
这个就不做过多解释了,不熟悉的小伙伴可以翻看前面的文章。
上面这三种方案都是传统方案。
特别是前两种,其实我们用的比较少,前两种有一个缺陷,就是我们配置的的 factory-method 都是通过反射来调用的,通过反射调用的话,多多少少性能受点影响。
这种 factory-method 在 Spring 中处理的源码执行时序图如下:
所以最终反射是在 SimpleInstantiationStrategy#instantiate
方法中执行的,就是大家非常熟悉的反射代码了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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. Spring5 解决方案 Spring5 中开始提供了 Supplier,可以通过接口回调获取到一个 Bean 的实例,这种方式显然性能更好一些。
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 进一步简化:
1 2 3 4 5 6 7 8 public class BookSupplier { public Book getBook () { Book book = new Book (); book.setName("深入浅出 Spring Security" ); book.setAuthor("江南一点雨" ); return book; } }
然后调用这个方法即可:
1 2 3 4 5 6 7 8 9 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 protected BeanWrapper createBeanInstance (String beanName, RootBeanDefinition mbd, @Nullable Object[] args) { 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 注入方式,感兴趣的小伙伴可以试试哦~