Retrofit is a type-safe HTTP client for Android and Java, and its biggest feature is that it supports initiating HTTP requests through an interface. Spring-boot is the most widely used Java development framework, but Retrofit officially does not support rapid integration with the spring-boot framework, so we developed retrofit-spring-boot-starter.

retrofit-spring-boot-starter enables rapid integration of retrofit and spring-boot frameworks, and supports many feature enhancements, greatly simplifying development.

Features

< ul class="list-paddingleft-2">

  • custom injection OkHttpClient
  • annotation interceptor
  • connection pool management
  • Log print
  • request retry
  • error decoder
  • global interceptor
  • circuit breaker downgrade
  • HTTP calls between microservices
  • call adapter
  • data converters
  • for quick use

    Introducing dependent

    <dependency> <
    groupId>com.github.lianjiatech</ groupId>
        <artifactId>retrofit-spring-boot-starterartifactId>
        <version> 2.2.2version>
    dependency> copy code

    definition http interface

    Interfaces must be marked with @RetrofitClient annotations! For the http concern, please refer to the official documentation: [retrofit official documentation]

    @RetrofitClient(baseUrl = "${test.baseUrl}")
    public interface HttpApi {

        @GET("person")


        Result getPerson(@Query("id" ) Long id); }Copy code

    injection can be used by

    injecting interfaces into other services!

    @Service
    public class TestService {

        @Autowired


        private HttpApi httpApi;

    public void test() {


    http request via httpAPI }} Copy the code

    HTTP request concern solution

    HTTP request concern solution, all using retrofit native annotations. **For details, please refer to the official documentation: [retrofit official documentation]

    annotation

    parameter

    classification supported annotation
    request methods @GET @HEAD @POST @PUT @DELETE @OPTIONS
    request headers @Header @HeaderMap @Headers
    The Query @Query @Path @QueryMap @QueryName
    path parameter
    The form-coded parameter @Field @FieldMap @FormUrlEncoded
    file upload @Multipart @Part @PartMap
    URL parameter @Url

    description of the configuration item

    Retrofit-spring-boot-starter supports several configurable properties to address different business scenarios. You can modify it as appropriate, as follows:

    Configure the default value to
    enable-log true enable log printing
    logging-interceptor DefaultLoggingInterceptorlogprintinterceptorpool
    connection pool configuration
    disable-void-return-type false disables java.lang.void return type
    retry-interceptor DefaultRetryInterceptor request retry interceptor
    global-converter-factories JacksonConverterFactoryGlobal converter
    factoryglobal-call-adapter-factories BodyCallAdapterFactory, ResponseCallAdapterFactory Global Call Adapter Factory
    enable-degrade false Whether circuit breaker downgrade is enabled degrade-type
    sentinel Circuit breaker downgrade implementation (currently only supported Sentinel)
    resource-name-parser DefaultResourceNameParser fuses the resource name resolver to resolve resource names

    YML configuration method

    :

    retrofit: enable-response-call-adapter:
    true
    # Enable log printing
      enable-log: true
    # Connection pool configuration pool:

    test1:
    max-idle-connections: 3
    keep-alive-second: 100
        test2:
          max-idle-connections: 5
          keep-alive-second: 50 # Disable void return value type
    disable-void-return-type:
    false # Log print interceptor

    logging-interceptor: com.github.lianjiatech.retrofit.spring.boot.interceptor.DefaultLoggingInterceptor # Request retry interceptor
    retry-interceptor
    : com.github.lianjiatech.retrofit.spring.boot.retry.DefaultRetryInterceptor
    # Global converter factory
    global-converter-factories:
    - retrofit2.converter.jackson.JacksonConverterFactory
    # Global call adapter factory
    global-call-adapter-factories:
    - com.github.lianjiatech.retrofit.spring.boot.core.BodyCallAdapterFactory
        -  com.github.lianjiatech.retrofit.spring.boot.core.ResponseCallAdapterFactory
    # Whether circuit breaker downgrade
    is enabled enable-degrade: true
      # Circuit breaker implementation
    degrade-type: sentinel
    # Circuit breaker resource name parser
    resource-name-parser: com.github.lianjiatech.retrofit.spring.boot.degrade.DefaultResourceNameParser
    copies code

    for advanced functionality

    Custom injection of OkHttpClient In general, dynamically creating OkHttpClient

    objects by @RetrofitClient annotation properties can meet most usage scenarios. However, in some cases, users may need to customize OkHttpClient, in this case, you can define the return type on the interface as a static method implementation of OkHttpClient.Builder. The code example is as follows:

    @RetrofitClient(baseUrl = 
    "public interface HttpApi3 {    @OkHttpClientBuilder    static OkHttpClient.Builder okhttpClientBuilder() {        return new OkHttpClient.Builder()                .connectTimeout(1, TimeUnit.SECONDS)                .readTimeout(1, TimeUnit.SECONDS)                .writeTimeout(1, TimeUnit.SECONDS);    } @GET

    Result getPerson(@Url String url, @Query(

    "The id

    method must be marked with @OkHttpClientBuilder annotation!”!

    Annotation interceptors

    Many times, we want certain HTTP requests under a certain interface to perform a unified interception processing logic. To support this feature, Retrofit-Spring-Boot-Starter provides annotated interceptors for URL-path matching. The steps used are mainly divided into 2 steps:

    1. inherit BasePathMatchInterceptor to write interception handlers;
    2. The interface is labeled with @Intercept. If you need to configure multiple interceptors, just mark multiple @Intercept annotations on the interface!

    The following is an example of using annotation interceptors by concatenating timestamps after the URL of a specified request.

    Inherit the BasePathMatchInterceptor to write the interceptor handler

    @Component
    public class TimeStampInterceptor extends BasePathMatchInterceptor {

        @Override


         public Response doIntercept(Chain chain) throws IOException {        Request request = chain.request();        HttpUrl url = request.url();

            long timestamp = System.currentTimeMillis();

            HttpUrl newUrl = url.newBuilder()

                    .addQueryParameter("timestamp", String.valueOf(timestamp))

                    .build();        Request newRequest = request.newBuilder()                .url(newUrl)                .build();

            return chain.proceed(newRequest);

    }}}

    @Intercept annotated @RetrofitClient on the copy code interface

    (baseUrl = "${ test.baseUrl}")
    @Intercept(handler = TimeStampInterceptor. classinclude = {"/api/**"}, exclude = "/api/test/savePerson")
    public interface HttpApi  {

        @GET("person")


        Result getPerson(@Query("id") Long id);

        @POST("savePerson")


        Result savePerson(@Body Person person); The

    @Intercept configuration above the copy code indicates that it intercepts requests under the /api/** path under the HttpApi interface (excluding /api/test/savePerson) and intercepts the processor TimeStampInterceptor

    Extended annotation interceptors

    Sometimes, we need to dynamically pass in some parameters in the interception annotation, and then use this parameter when performing the interception. In this case, we can extend the implementation of custom interception annotations. Custom interception annotations must use @InterceptMark tags and must include information for the include(), exclude(), handler() attributes. The steps used are mainly divided into 3 steps:

      custom interception

    1. annotations inherit BasePathMatchInterceptor to write interception handlers
    2. Use custom interception annotations on the interface;

    For example, we need to dynamically add accessKeyId and accessKeySecret signature information to the request header to initiate http requests normally, and at this time we can customize a signed interceptor annotation @Sign to achieve. The following is an example of a custom @Sign interception annotation.

    Custom @Sign Annotation

    @Retention (RetentionPolicy.RUNTIME)
    @Target( ElementType.TYPE)
    @Documented
    @InterceptMark
    public @interface Sign {
    /** * Key key * supports placeholder configuration.     *

         * @return


         */

        String accessKeyId();

    /** Key supports placeholder configuration.     *

         * @return


         */


        String accessKeySecret();

    /** * interceptor match path *

    * @return


    *

    /
    String[] include() default {"/**"};

    /** * interceptor exclude matching, exclude specified path interception *

    * @return


    */


    String[] exclude() default {};

    /** * Interceptor class that handles the annotation * Get the corresponding bean from the spring container first, if you can't get it, create one with reflection!     *

         * @return


         */


        Class handler() default SignInterceptor. class; }Copy code

    extension custom interception annotations have the following 2 points to note:

      custom interception

    1. annotations must use @InterceptMark markup.
    2. Annotations must include information for the include(), exclude(), handler() attributes.

    Implement SignInterceptor

    @Component
    public class SignInterceptor extends BasePathMatchInterceptor {

        private String accessKeyId;

        private String accessKeySecret;

        public void setAccessKeyId(String accessKeyId) {


            this.accessKeyId = accessKeyId;    }

        public void setAccessKeySecret(String accessKeySecret) {


            this.accessKeySecret = accessKeySecret;    }

        @Override


        public Response doIntercept(Chain chain) throws IOException  {        Request request = chain.request();        Request newReq = request.newBuilder()

                    .addHeader("accessKeyId", accessKeyId)


                    .addHeader("accessKeySecret" , accessKeySecret)                .build();

            return chain.proceed(newReq);

    }} Copy

    the

    above accessKeyId and accessKeySecret field values will be based on the accessKeyId() and accessKeySecret() of the @Sign annotations. Values are injected automatically, and if @Sign specifies a string as a placeholder, the configuration property value is taken for injection. In addition, the accessKeyId and accessKeySecret fields must provide setter methods.

    Use @Sign

    @RetrofitClient on the interface (baseUrl = "${test.baseUrl}"
    ). @Sign(accessKeyId = "${test.accessKeyId}", accessKeySecret = "${test.accessKeySecret}", exclude = {"/api/test/person"})
    public interface HttpApi {

        @GET("person")


        Result getPerson (@Query("id") Long id);

        @POST("savePerson")


        Result savePerson(@Body Person person); Copy the code

    so that the signature information is automatically added to the request for the specified URL.

    Connection Pool Management

    By default, all http requests sent via Retrofit use the default connection pool of max-idle-connections=5 keep-alive-second=300. Of course, we can also configure multiple custom connection pools in the configuration file and then specify the use by @RetrofitClient the poolName property. For example, if we want to let all requests under an interface use the poolName=test1 connection pool, the code implementation is as follows:

    1. configure the connection pool.

      retrofit: 
      # Connection pool configuration pool:

      test1:
      max-idle-connections: 3
              keep-alive-second: 100
              test2:
              max-idle-connections: 5
              keep-alive-second: 50
      The copy code
    2. specifies the connection pool used by @RetrofitClient the poolName property.

      @RetrofitClient(baseUrl = "${test.baseUrl}", poolName="test1")
      public  interface HttpApi {

          @GET("person")


          Result getPerson(@Query("id" ) Long id); }Copy code

    log printing

    In many cases, we want to log down HTTP requests. Through retrofit.enableLog configuration, you can globally control whether the log is enabled or not. For each interface, you can control whether it is enabled through the enableLog @RetrofitClient, and through logLevel and logStrategy, you can specify the log printing level and log printing policy of each interface. retrofit-spring-boot-starter supports 5 log print levels (ERROR, WARN, INFO, DEBUG, TRACE), default INFO; SUPPORTS FOUR LOG PRINTING POLICIES (NONE, BASIC, HEADERS, BODY), AND THE DEFAULT BASIC IS SUPPORTED. The four log printing strategies have the following meanings

    :

    1. NONE:No logs
    2. BASIC:Logs request and response lines.
    3. HEADERS:Logs request and response lines and their respective headers.
    4. BODY:Logs request and response lines and their respective headers and bodies (if present).

    retrofit-spring-boot-starter uses DefaultLoggingInterceptor by default to perform real log printing functions, and its underlying layer is the native HttpLoggingInterceptor of okhttp. Of course, you can also customize the implementation of your own log print interceptor, only need to inherit the BaseLoggingInterceptor (you can refer to the implementation of DefaultLoggingInterceptor for details), and then configure it in the configuration file.

    retrofit: 
    # log print interceptor
    logging-interceptor: com.github.lianjiatech.retrofit.spring.boot.interceptor.DefaultLoggingInterceptor
    Copy code

    request to retry

    retrofit-spring-boot-starter supports request retry by simply adding @Retry annotations to the interface or method. @Retry supports maxRetries, interval Ms, and retryRules configurations. The retry rule supports three configurations

    :

    1. RESPONSE_STATUS_NOT_2XX: retry is performed when the response status code is not 2xx;
    2. OCCUR_IO_EXCEPTION: Retry occurs when an IO exception occurs;
    3. OCCUR_EXCEPTION: Retry occurs when any exception occurs;

    The default response status code is not 2xx or an IO exception is automatically retried. If needed, you can also inherit BaseRetryInterceptor to implement your own request retry interceptor and then configure it.

    retrofit: 
    # Request to retry the interceptor retry-interceptor
    : com.github.lianjiatech.retrofit.spring.boot.retry.DefaultRetryInterceptor
    Copy code

    error decoder

    in When HTTP request errors occur (including exceptions or response data that do not meet expectations), the error decoder can decode HTTP-related information into a custom exception. You can specify the error decoder of the current interface in the errorDecoder() annotation @RetrofitClient, and the custom error decoder needs to implement the ErrorDecoder interface:

    /** * Error decoder. ErrorDecoder. * When an exception occurs in the request or an invalid response result is received, the HTTP-related information is decoded into the exception, and the invalid response is judged by the business itself * * When an exception occurs in the request or an invalid response result is received, the HTTP related information is decoded into the exception, * and the invalid response is determined by the business itself. * *

    @author Chen Tim Ming


    */

    public interface ErrorDecoder {

    /** * When the invalid response is invalid, the HTTP information is decoded into the exception, and the invalid response is determined by the service.     * When the response is invalid, decode the HTTP information into the exception, invalid response is determined by business.     *

         * @param request  request


         * @param response response
         * @return  If it returns null, the processing is ignored and the processing continues with the original response.
         */


        default RuntimeException invalidRespDecode(Request request, Response response) {
            if (! response.isSuccessful()) {
                throw RetrofitException.errorStatus(request, response);        }

            return null;

    }

    /** * When an IO exception occurs in the request, the HTTP information is decoded into the exception.     * When an IO exception occurs in the request, the HTTP information is decoded into the exception.     *

         * @param request request


         * @param cause   IOException
         * @return RuntimeException
         */


        default  RuntimeException ioExceptionDecode(Request request, IOException cause) {
            return RetrofitException.errorExecuting(request, cause);    }

    /** * When an exception other than an IO exception occurs in the request, the HTTP information is decoded into the exception.     * When the request has an exception other than the IO exception, the HTTP information is decoded into the exception.     *

         * @param request request


         * @param cause   Exception
         * @return RuntimeException
         */


        default  RuntimeException exceptionDecode(Request request, Exception cause) {
            return RetrofitException.errorUnknown(request, cause);    }}Copy codeGlobal

    interceptor

    Global application interceptor If we need to perform unified interception processing of http requests for the entire system, we can customize the implementation of the global

    interceptor BaseGlobalInterceptor

    , and configure it as a bean in the spring container! For example, we need to bring the origin information to the HTTP request initiated by the entire system.

    @Component
    public class SourceInterceptor extends  BaseGlobalInterceptor {
        @Override
        public Response doIntercept(Chain chain) throws  IOException {        Request request = chain.request();        Request newReq = request.newBuilder()

                    .addHeader("source""test")

                    .build();

            return chain.proceed(newReq);

    }}} Copy code

    Global network interceptor only needs to implement the NetworkInterceptor

    interface and configure it as a bean in the spring container to support automatic weaving into the global network interceptor.

    In

    a distributed service architecture, circuit breaker degradation of unstable external services is one of the important measures to ensure high service availability. Since the stability of external services is not guaranteed, the response time will become longer when the external service is unstable. Correspondingly, caller response times become longer and threads accumulate, which can eventually exhaust the caller’s thread pool and render the entire service unavailable. Therefore, we need to circuit down unstable weak dependent service calls, temporarily cut off unstable calls, and avoid local instability leading to an avalanche of the overall service.

    retrofit-spring-boot-starter supports the circuit breaker downgrade function, which is based on [Sentinel]

    1. Enable the circuit breaker downgrade function By default, the circuit breaker downgrade function

    is turned off. You need to set the corresponding configuration items to enable the circuit breaker degrading function

    :

    retrofit: # Whether to enable circuit breaker degrade enable-degrade:

    true
    # Circuit breaker downgrade implementation (currently only supported by Sentinel)
    degrade-type: sentinel # resource name
    resolver resource-name-parser
    : com.github.lianjiatech.retrofit.spring.boot.degrade.DefaultResourceNameParser
    replication code

    resource

    name resolver is used to implement user-defined resource names, the default configuration is DefaultResourceNameParser, the corresponding resource name format is ‘HTTP_OUT:GET:In

    addition, since the circuit breaker

    degrade function is optional, enabling circuit breaker downgrade requires users to introduce Sentinel dependencies:

    < by themselves dependency>
        <groupId>com.alibaba.cspgroupId>
        <artifactId> sentinel-coreartifactId>
        <version>1.6.3version>
    dependency > copy code

    2. Configure downgrade rules (optional)

    retrofit-spring-boot-starter supports annotated configuration of downgrade rules via @Degrade annotation to configure the demotion rule. @Degrade annotations can be configured on interfaces or methods, with higher priority over methods.

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Documented
    public @interface Degrade {

        

    /**     * RT threshold or exception ratio threshold count.

         */


        double count();

        

    /**     * Degrade recover timeout (in seconds) when degradation occurs.

         */


        int timeWindow() default 5;

        

    /**     * Degrade strategy (0: average RT, 1: exception ratio).

         */


        DegradeStrategy degradeStrategy() default DegradeStrategy.AVERAGE_RT; }Copy

    the

    codeIf the application project supports configuring downgrade rules through the configuration center, you can ignore the annotation configuration method.

    3. @RetrofitClient set fallback or fallbackFactory (optional)

    if @RetrofitClient do not set fallback or FallbackFactory, when the circuit breaker is triggered, will directly throw the RetrofitBlockException exception. Users can customize the method return value during circuit breaker by setting fallback or fallbackFactory. The fallback class must be the implementation class of the current interface, the fallbackFactory must be the FallbackFactory implementation class, and the generic parameter type is the current interface type. In addition, fallback and fallbackFactory instances must be configured as Beans of the Spring container.

    The main difference between fallbackFactory and fallback is the ability to sense the abnormal cause of each circuit breaker. The reference example is as follows:

    @Slf4j
    @Service
    public class HttpDegradeFallback implements HttpDegradeApi {

        @Override


        public Result test() {
            Result fallback = new Result<>();
            fallback.setCode(100)
                    .setMsg("fallback")
                    .setBody(1000000);
            return fallback;    }}Copy Code

    @Slf4j


    @Service
    public class HttpDegradeFallbackFactory implements FallbackFactory<HttpDegradeApi{

        

    /**     * Returns an instance of the fallback appropriate for the given cause     *

         * @param  Cause fallback cause


    * @return An instance of the Retrofit interface implemented. an instance that implements the retrofit interface.
         */


    @Override
    public HttpDegradeApi create(Throwable cause) {
    log.error("Trigger circuit breaker!) , cause.getMessage(), cause);
            return new HttpDegradeApi() {
                @Override
                public Result test() {
                    Result fallback = new Result<>();
                    fallback.setCode(100)
                            .setMsg("fallback")
                            .setBody(1000000);
                    return fallback;            } }}} Copy HTTP

    calls

    between microservices in code In order to be able to use microservice calls

    , you need to configure the following configuration:

    Configure ServiceInstanceChooser to Spring Container Bean users can implement the ServiceInstanceChooser interface by themselves, complete the selection logic of the service instance, and configure it as a Spring container bean

    . For Spring Cloud applications, retrofit-spring-boot-starter provides a SpringCloud ServiceInstanceChooser implementation, which you can simply configure as a Spring Bean Can.

    @Bean
    @Autowired
    public ServiceInstanceChooser serviceInstanceChooser( LoadBalancerClient loadBalancerClient) {
        return new SpringCloudServiceInstanceChooser(loadBalancerClient); The copy code

    uses the @Retrofit serviceId and path properties to implement HTTP calls

     between microservices  @RetrofitClient(serviceId = "${jy-helicarrier-api.serviceId}", path = "/m/count", errorDecoder = HelicarrierErrorDecoder. class)
    @Retry
    public interface ApiCountService
    {}Copy code

    calls adapter and data transcoder

    Calling the adapter Retrofit can adapt the Call object to the return value type of the interface method by calling

    the adapter CallAdapterFactory.

    retrofit-spring-boot-starter extends 2 CallAdapterFactory implementations:

      > BodyCallAdapterFactory
      • enabled by default and can be turned off by configuring retrofit.enable-body-call-adapter=false
      • Synchronously execute HTTP requests and adapt the response body content to the return value type instance of the interface method.
      • With the exception of Retrofit.Call,

      • Retrofit.Response, java.util.concurrent.CompletableFuture, all other return types can use the adapter.
    1. ResponseCallAdapterFactory
      • enabled by default. You can configure retrofit.enable-response-call-adapter=false to turn off
      • synchronous execution of http requests, and adapt the response body content to Retrofit.Response Return.
      • If the return value type of the method is Retrofit.Response, you can use the adapter.

    Retrofit automatically selects the corresponding CallAdapterFactory to perform adaptation processing according to the method return value type! Together with Retrofit’s default CallAdapterFactory, it can support multiple forms of method return value types

    :

      >Call: Directly return Call without performing adaptation processing Object

      CompletableFuture:

    • Adapts the response body content to a CompletableFuture object to
    • return

    • Void: Can be used without concern for the return type Void。 If the HTTP status code is not 2xx, throw the error directly!
    • Response: Adapt the response content to the Response object and return any other Java type

    • : Adapt
    • the

    • response content to a corresponding Java type object to return, if the http status code is not 2xx, throw the error directly!
    /** * Call *  
    does not perform adaptation processing, and directly returns the Call object *

    @param id


    * @return
    * /

    @GET ("person")
        Call> getPersonCall(@Query("id") Long id); /** * CompletableFuture

    adapts the response body content to the CompletableFuture object return *

    @param id


    * @return
    */


    @GET ( "person")
        CompletableFuture> getPersonCompletableFuture(@Query("id") Long id);

    /** * Void * Void can be used without concern for the return type. If the HTTP status code is not 2xx, throw the error directly!

         * @param id


         * @return
         */


        @GET("person")
        Void getPersonVoid( @Query("id") Long id); /** * Response *

    Adapts the response content to the Response object and returns *

    @param id


    * @return
    * /


    @GET ("person"
    ).     Response> getPersonResponse(@Query("id") Long id);

    /** * Any other Java type * Adapt the response body content to a corresponding Java type object and return, if the http status code is not 2xx, throw the error directly!

         * @param id


         * @return
         */


        @GET("person")
        Result getPerson (@Query("id") Long id); We

    can also implement our own CallAdapter by inheriting the CallAdapter.Factory extension!

    retrofit-spring-boot-starter supports configuring the global call adapter factory via retrofit.global-call-adapter-factories, where factory instances are fetched from the Spring container first, and if not, reflection is created. The default global call adapter factory is [BodyCallAdapterFactory, ResponseCallAdapterFactory]!

    retrofit: 
    # Global call adapter factory
    global-call-adapter-factories:
    - com.github.lianjiatech.retrofit.spring.boot.core.BodyCallAdapterFactory
        -  com.github.lianjiatech.retrofit.spring.boot.core.ResponseCallAdapterFactory
    copies code

    for each Java interface and can also be @RetrofitClient The callAdapterFactories() of the annotation specifies the CallAdapter.Factory used by the current interface, and the specified factory instance is still fetched from the Spring container first.

    Note: If CallAdapter.Factory does not have a public parameterless constructor, manually configure it as a Bean object for the Spring container!

    The data transcoder

    Retrofit uses Converter to convert @Body annotated objects into request bodies and response body data into a Java object, using the following options Converter

    • [Gson]
    • [Jackson]
    • [Moshi]
    • [Protobuf]
    • [Wire]
    • [Simple XML]
    • [JAXB]

    retrofit-spring-boot-starter supports configuring a global data converter factory via retrofit.global-converter-factories, where the converter factory instance is fetched from the Spring container first, and if it is not obtained, reflection is created. The default global data converter factory is retrofit2.converter.jackson.JacksonConverterFactory, you can directly configure jackson serialization rules through spring.jackson.*, the configuration can be referred to [Customize the Jackson ObjectMapper]

    retrofit: 
    # Global converter factory
    global-converter-factories:
    - retrofit2.converter.jackson.JacksonConverterFactory
    copies code

    for each Java interface and can also be annotated through @RetrofitClient converterFactories() specifies the Converter.Factory used by the current interface, and the specified converter factory instance is still obtained from the Spring container first.

    Note: If Converter.Factory does not have a public parameterless constructor, manually configure it as a Bean object for the Spring container!

    retrofit-spring-boot-starter A lightweight HTTP client framework for SpringBoot projects, has been running stably online for more than a year, and has been used by multiple external companies. Interested friends can try the

    original | https://juejin.cn/post/6898485806587969544