In the spring-Boot project’s most elegant HTTP client tool, it’s enough, it smells good! In this article, we learned how retrofit-spring-boot-starter can be used. This article continues with the implementation of Retrofit-Spring-boot-starter, showing you how to implement your own lightweight HTTP calling tool based on Retrofit in a Spring-boot project from scratch.

Retrofit-spring-boot-starter

Determine the implementation idea

Let’s start by looking directly at how to initiate an HTTP request using the Retrofit raw API.

  1. Defines the interface

    public interface GitHubService {
    @GET("users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);
    }
    Copy the code
  2. Create an interface proxy object

    Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .build();
    
    // Building Retrofit in a real business scenario is much more complex than that, and this is the easiest way to deal with it
    
    GitHubService service = retrofit.create(GitHubService.class);
    Copy the code
  3. The initiating

    Call<List<Repo>> repos = service.listRepos("octocat");
    Copy the code

As you can see, Retrofit itself already does a good job of supporting HTP requests through interfaces. However, if every business code of our project has to write the above boilerplate code, it will be very tedious. Is there a way for users to just focus on the interface definition and let the framework handle everything else automatically? At this time, we may think of using Mybatis under the Spring-boot project, the user only needs to define Mapper interface and write SQL, not care about the details of JDBC interaction. Similarly, we will eventually implement an implementation where the user only needs to define the HttpService interface, regardless of other low-level implementation details.

Introduction to relevant knowledge

In order to facilitate the following introduction, we first have to understand a few relevant knowledge points.

The Spring container is initialized

We’ll start with a brief look at Spring container initialization. In brief, spring container initialization consists of the following two steps:

  1. Register Bean definitions: Scanning and parsingThe configuration fileorSome annotationsGet the Bean properties (includingbeanName,beanClassName,scope,isSingletonEtc.) and then based on thisbeanAttributes to createBeanDefinitionObject, and finally registers it toBeanDefinitionRegistryIn the.
  2. Creating a Bean instance: according to theBeanDefinitionRegistryThe inside of theBeanDefinitionInformation to create the Bean instance and save the instance object tospringIn the container, the creation methods include reflection creation, factory method creation, and factory beans (FactoryBean) create and so on.

Of course, the actual Initialization of the Spring container is much more complicated than this, and since this is not the focus of this article, it will suffice for now.

RetrofitObject description

We already know how to useRetrofitObject to create interface proxy objects, see nextRetrofitUML class diagram (listing only the dependencies we are concerned with) :By analyzing UML class diagrams, we can discover, buildRetrofitObject, you can inject the following four properties:

  1. HttpUrl:httpThe request ofbaseUrl.
  2. CallAdapterWill:Call<T>Fit to the interface method return value type.
  3. ConverterWill:@BodyThe method parameters of the tag are serialized into the request body data; Deserialize the response body data into the response object.
  4. OkHttpClient: Bottom sendinghttpThe requested client object.

When you build the OkHttpClient object, you inject the Interceptor and ConnectionPool properties.

So to build a Retrofit object, we first create HttpUrl, CallAdapter, Converter, and OkHttpClient; To build the OkHttpClient object, you need to create an Interceptor and a ConnectionPool.

Implementation,

Register Bean definitions

To fully commit the HttpService interface proxy object to the Spring container, the HttpService interface must first be scanned and registered with BeanDefinitionRegistry. Spring provides ImportBeanDefinitionRegistrar interface, the custom register BeanDefinition support functions. So let’s define the RetrofitClientRegistrar class to implement the above functionality. The concrete implementation is as follows:

  1. RetrofitClientRegistrar

    RetrofitClientRegistrar extracted from @ RetrofitScan annotations to scan after package path, the basis of the specific scan registration logic to ClassPathRetrofitClientScanner processing.

    public class RetrofitClientRegistrar implements ImportBeanDefinitionRegistrar.ResourceLoaderAware.BeanClassLoaderAware {
    
        // Omit other code
    
        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AnnotationAttributes attributes = AnnotationAttributes
                    .fromMap(metadata.getAnnotationAttributes(RetrofitScan.class.getName()));
            // Scan the @RetrofitClient annotated interface in the specified path and register with BeanDefinitionRegistry
            / / registered real scan logic to ClassPathRetrofitClientScanner execution
            ClassPathRetrofitClientScanner scanner = new ClassPathRetrofitClientScanner(registry, classLoader);
            if(resourceLoader ! =null) {
                scanner.setResourceLoader(resourceLoader);
            }
            // Specify the base package to scan
            String[] basePackages = getPackagesToScan(attributes);
            scanner.registerFilters();
            // Scan and register to BeanDefinitionscanner.doScan(basePackages); }}Copy the code
  2. ClassPathRetrofitClientScanner

    ClassPathRetrofitClientScanner inherited ClassPathBeanDefinitionScanner, this is Spring provides classpath BeanDefinition scanner. One thing to note: All BeanDefinition beanClass attribute set to RetrofitFactoryBean. Class, and transfer the type of the interface itself to RetrofitFactoryBean retrofitInterface attributes. This shows that the final creation of the Bean instance is done through RetrofitFactoryBean.

    public class ClassPathRetrofitClientScanner extends ClassPathBeanDefinitionScanner {
    
        // Omit other code
    
        private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
            GenericBeanDefinition definition;
            for (BeanDefinitionHolder holder : beanDefinitions) {
                definition = (GenericBeanDefinition) holder.getBeanDefinition();
                if (logger.isDebugEnabled()) {
                    logger.debug("Creating RetrofitClientBean with name '" + holder.getBeanName()
                            + "' and '" + definition.getBeanClassName() + "' Interface"); } definition.getConstructorArgumentValues().addGenericArgumentValue(Objects.requireNonNull(definition.getBeanClassName())) ;// beanClass is all set to RetrofitFactoryBeandefinition.setBeanClass(RetrofitFactoryBean.class); }}}Copy the code

This completes our ability to scan the @RetrofitClient annotated interface under the specified path and register it with BeanDefinitionRegistry.

The @RetrofitClient annotation must be identified on the HttpService interface! @retrofitScan specifies the packet path to scan. Specific can look at the source code.

Creating a Bean instance

As stated above, creating Bean instances is actually done through RetrofitFactoryBean. Implement the FactoryBean

interface, and then override the getObject() method to complete the logic of creating an instance of the interface Bean. Also, we already know that interface proxy objects can be generated with Retrofit objects. So the core of the getObject() method is to build a Retrofit object and generate an HTTP interface proxy object based on it.

  1. Configuration items and @RetroFITClient To make building Retrofit objects more flexible, we can pass dynamic parameter information through configuration items and the @RetrofitClient attribute. @RetrofitClient contains the following attributes:

    1. baseUrl: used to createRetrofittheHttpUrlIs applicable to all requests on the interfaceBased on the url.
    2. poolName: Specifies the name of the connection pool to be used for requests under this interfaceConnectionPoolObject value.
    3. connectTimeoutMs/readTimeoutMs/writeTimeoutMs: for buildOkHttpClienT Specifies the timeout period for the object.
    4. logLevel/logStrategy: Sets the log printing level and log printing policy for requests on this interface, which can be used to create a log printing interceptorInterceptor.
  2. RetrofitFactoryBean RetrofitFactoryBean implementation logic is very complex, which can be summarized as follows:

    1. By configuring item data as well@RetrofitClientThe annotation data is completeRetrofitObject construction.
    2. eachHttpServiceThe interface will build oneRetrofitObject, every one of themRetrofitObject will build the correspondingOkHttpClientObject.
    3. Extensible annotated interceptor is adoptedInterceptMarkThe annotation tag is implemented and the path intercept match is passedBasePathMatchInterceptorThe implementation.
    public class RetrofitFactoryBean<T> implements FactoryBean<T>, EnvironmentAware.ApplicationContextAware {
    
        // Omit other code
    
        public RetrofitFactoryBean(Class<T> retrofitInterface) {
            this.retrofitInterface = retrofitInterface;
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public T getObject(a) throws Exception {
            // Interface verification
            checkRetrofitInterface(retrofitInterface);
            // Build Retrofit objects
            Retrofit retrofit = getRetrofit(retrofitInterface);
            // Create interface proxy objects based on Retrofit
            return retrofit.create(retrofitInterface);
        }
    
        /** * Get the OkHttpClient instance, one interface corresponds to one OkHttpClient **@paramRetrofitClientInterfaceClass retrofitClient interface class *@return* / OkHttpClient instance
        private synchronized OkHttpClient getOkHttpClient(Class
              retrofitClientInterfaceClass) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
            // Build OkHttpClient based on various conditions
        }
    
        /** * Get a Retrofit instance. One retrofitClient interface corresponds to one Retrofit instance **@paramRetrofitClientInterfaceClass retrofitClient interface class *@return* / Retrofit instance
        private synchronized Retrofit getRetrofit(Class
              retrofitClientInterfaceClass) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
            / / build retrofit
        }
    Copy the code

This completes our ability to create an instance of HttpServiceBean. When used, inject HttpService directly and then call its methods to send the corresponding HTTP request.

conclusion

In summary, the core of implementing your own lightweight HTTP calling tool based on Retrofit in the Spring-Boot project is just two things: The first is to register the BeanDefinition of the HttpService interface, and the second is to build Retrofit to create a proxy object for HttpService. For more details, it is recommended to look directly at the retrofit-spring-boot-Starter source code.

Original is not easy, feel that the article is written well small partners, a praise 👍 to encourage it ~

Welcome to my open source project: a lightweight HTTP invocation framework for SpringBoot