More elegant third-party bean injection in Spring5

Friends know that when we use the Spring container, if we encounter some special beans, generally speaking, they can be configured in the following three ways:

  • static factory method
  • instance factory method
  • FactoryBean

However, starting from Spring 5, there is an additional attribute in the AbstractBeandefinition class, and we have more choices for special beans:

/**
 * 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;
}

Next, Brother Song will come and briefly talk with you about this topic.

1. Traditional solutions

1.1 Question

I don’t know if any of you have ever used OkHttp. This is a tool specialized in making network requests. In the HTTP calling component of the microservice, we can configure the underlying tool to use OkHttp.

Generally speaking, if we want to use OkHttp directly, the code is as follows:

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());
    }
});

First create an OkHttpClient object through the builder mode, then create the Request object through the builder mode, and then send the request. So for such code, we can hand over the OkHttpClient object to the Spring container for unified management. So how to register the OkHttpClient into the Spring container?

1.2 Static factory method

First, you can use a static factory method, that is, the factory method is a static method, as follows:

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;
    }
}

Then inject it in the Spring configuration file:

< bean  class = "org.javaboy.bean.OkHttpStaticFactory"  factory-method = "getOkHttpClient"  id = "httpClient" />

The characteristic of static factory is that static methods can be called directly and an instance of the factory class must be obtained, so you only need to specify it when configuring above factory-method.

That’s it. In the future, we will search for an object named httpClient in the Spring container, and the one we get is OkHttpClient.

1.3 Instance factory method

Instance factory method means that the factory method is an instance method. as follows:

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;
    }
}

This is a simple singleton pattern. But the factory method here is an instance method. The call to the instance method must first obtain the object and then call the instance method. Therefore, the configuration is as follows:

< bean  class = "org.javaboy.bean.OkHttpInstanceFactory"  id = "httpInstanceFactory" /> 
< bean  factory-bean = "httpInstanceFactory"  factory-method = "getInstance"  id = "httpClient" />

Okay, next we can go to the Spring container to get an object named httpClient, and what we get is the OkHttpClient instance.

1.4 FactoryBean

Of course, you can also solve the above problems through FactoryBean. FactoryBean Song Ge just introduced it to you in the previous article. Let’s take a look:

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 ;
    }
}

Finally, configure it in Spring:

< bean  class = "org.javaboy.bean.OkHttpClientFactoryBean"  id = "httpClient" />

I won’t explain too much about this. Friends who are not familiar with it can read the previous articles.

The above three solutions are all traditional solutions.

Especially the first two, we actually use them less. The first two have a flaw, that is, the factory-methods we configure are all called through reflection. If called through reflection, the performance will be affected to some extent.

The source code execution sequence diagram of this factory-method processed in Spring is as follows:

So the final reflection is SimpleInstantiationStrategy#instantiateexecuted in the method, which is the reflection code that everyone is very familiar with:

@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();
            }
        }
}

Okay, here’s the traditional solution.

2. Spring5 solution

Supplier has been provided in Spring 5, and a Bean instance can be obtained through interface callback. This method obviously has better performance.

as follows:

AnnotationConfigApplicationContext  ctx  =  new  AnnotationConfigApplicationContext ();
 GenericBeanDefinition  definition  =  new  GenericBeanDefinition ();
definition.setBeanClass(Book.class);
definition.setInstanceSupplier((Supplier<Book>) () -> {
    Book  book  =  new  Book ();
    book.setName( "Spring Security in simple terms" );
    book.setAuthor( "A little rain in Jiangnan" );
     return book;
});
ctx.registerBeanDefinition( "b1" , definition);
ctx.refresh();
Book  b  = ctx.getBean( "b1" , Book.class);
System.out.println( "b = " + b);

The key is to set the callback by calling the setInstanceSupplier method of BeanDefinition. Of course, the above code can be further simplified through Lambda:

public  class  BookSupplier {
     public Book getBook () {
         Book  book  =  new  Book ();
        book.setName( "Spring Security in simple terms" );
        book.setAuthor( "A little rain in Jiangnan" );
         return book;
    }
}

Then just call this method:

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);

Doesn’t this feel more like Lambda?

In the Spring source code, when processing the acquisition of Bean instances, there is the following branch, which handles the situation of 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();
    }
}

The obtainFromSupplier method above will eventually call the second method. In the second method, supplier.get();we actually end up calling the getBook method we wrote ourselves.

Okay, this is a Bean injection method combined with Lamdba starting from Spring 5. Interested friends can try it~