Service Invocation/Communication – OpenFeign Best Practices

Spring Cloud OpenFeign is a declarative service call and load balancing component officially launched by Spring. Its bottom layer is based on Netflix Feign, which is an open source declarative WebService client designed by Netflix to simplify communication between services.

Spring Cloud openfeign enhances Feign to support Spring MVC annotations, and integrates Ribbon and Nacos, making Feign more convenient to use.

https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/

Principle

Dynamic Proxy is used

Compare Dubbo

It seems that OpenFeign cannot directly call the service layer. The general way of writing is that the service provider must write the controller interface, but dubbo does not need to write the controller interface. If the service layer cannot be called directly and the controller layer must be written, then this should be a disadvantage, because an additional controller method needs to be written every time.

Moreover, on the service consumer side, an additional remote call service interface with @FeignClient annotation should be written. It seems that Spring Cloud’s OpenFeign is much more troublesome than dubbo.

Introducing dependencies

Using OpenFeign components requires the introduction of client dependencies

<!--OpenFeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Use case

Add the @EnableFeignClients annotation to the startup class

/**
 * callee
 */
@RestController
public class WarehouseController {<!-- -->
    /**
     * Query the inventory status of the corresponding skuId
     * @param skuId skuId
     * @return Stock stock object
     */
    @GetMapping("/stock")
    public Stock getStock(Long skuId){<!-- -->
    // ...omitted
    }
}

/**
 * caller
 */
@FeignClient("warehouse-service")
public interface WarehouseServiceFeignClient {<!-- -->
    
    @GetMapping("/stock")
    public Stock getStock(@RequestParam("skuId") Long skuId);
}

The @FeignClient annotation indicates that the current interface is an OpenFeign communication client, and the parameter value warehouse-service is the service provider ID (note that the OpenFeign service name does not support the underscore _, which is a pitfall). This item must be consistent with the Nacos registration ID.

Before OpenFeign sends a request, it will automatically query all available instance information of warehouse-service in Nacos, and then select an instance to initiate a RESTful request through the built-in Ribbon load balancing, thereby ensuring high availability of communication.

tips: Although the return values here are all written in the same type, they are not necessarily the same class. Essentially, they interact with Json, as long as the attributes are correct.

Detailed explanation of @FeignClient annotation attributes

contextId: If contextId is configured, this value will be used as beanName.

fallback: Defines a fault-tolerant processing class. When calling a remote interface fails or times out, the fault-tolerant logic of the corresponding interface will be invoked. The class specified by fallback must implement the interface marked by @FeignClient

fallbackFactory: Factory class, used to generate fallback class examples, through this property we can implement common fault-tolerant logic for each interface and reduce repetitive code

url: url is generally used for debugging, you can manually specify the address called by @FeignClient

Spring Cloud Circuit Breaker Fallbacks

The default logic executed when the circuit breaker is opened or an error occurs can be configured by fallback

Note: You also need to declare it as a Spring bean.

@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {<!-- -->

    @RequestMapping(method = RequestMethod. GET, value = "/hello")
    Hello getHello();

    @RequestMapping(method = RequestMethod. GET, value = "/hellonotfound")
    String getException();

}

@Component
static class Fallback implements TestClient {<!-- -->

    @Override
    public Hello getHello() {<!-- -->
        throw new NoFallbackAvailableException("Boom!", new RuntimeException());
    }

    @Override
    public String getException() {<!-- -->
        return "Fixed response";
    }

}

If you need to access what caused the fallback to fire, you can use the fallbackFactory attribute

@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
        fallbackFactory = TestFallbackFactory.class)
protected interface TestClientWithFactory {<!-- -->

    @RequestMapping(method = RequestMethod. GET, value = "/hello")
    Hello getHello();

    @RequestMapping(method = RequestMethod. GET, value = "/hellonotfound")
    String getException();

}

@Component
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {<!-- -->

    @Override
    public FallbackWithFactory create(Throwable cause) {<!-- -->
        return new FallbackWithFactory();
    }

}

static class FallbackWithFactory implements TestClientWithFactory {<!-- -->

    @Override
    public Hello getHello() {<!-- -->
        throw new NoFallbackAvailableException("Boom!", new RuntimeException());
    }

    @Override
    public String getException() {<!-- -->
        return "Fixed response";
    }

}

Enable logging

In our work, we have some production problems in the mutual calls of microservices OpenFeign, but the calls of OpenFeign do not log by default.

We can configure by code or configuration file

//Global configuration
@Configuration
public class FeignLoggerConfiguration {<!-- -->
    @Bean
    Logger.Level feignLoggerLevel() {<!-- -->
        return Logger.Level.FULL;
    }
}

//local configuration
//No need to add Configuration annotation
public class FeignConfig {<!-- -->
    @Bean
    public Logger.Level feignLogLevel(){<!-- -->
        //Set the log printing level of feign client to FULL
        return Logger.Level.FULL;
    }
}

//configuration attribute, the value is Class, and the configuration information takes effect for the service named userprivilege
@Resource
@FeignClient(name="userprivilege", fallback = FallBack_PrivilegeService.class , configuration = FeignConfig.class)
public interface PrivilegeService {<!-- -->
}
feign:
  client:
    config:
      default: # project global
        loggerLevel: HEADERS
      order-service: The service name configured in the #@FeignClient annotation
        loggerLevel: FULL

Level has four levels

  • NONE Do not log, the default value
  • BASIC only records method, url, response code, execution time
  • HEADERS Only log request and response headers
  • FULL all logged

The log level of openfeign is modified above to be debug, but the default log level of springboot is info, because debug

logging:
    level:
    com.base.service: debug # This is the package path where the openfeign client is located

Upload files

https://www.baeldung.com/java-feign-file-upload

@PostMapping(value = "/upload-file")
public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {<!-- -->
    // File upload logic
}

public class FeignSupportConfig {<!-- -->
    @Bean
    public Encoder multipartFormEncoder() {<!-- -->
        return new SpringFormEncoder(new SpringEncoder(new ObjectFactory<HttpMessageConverters>() {<!-- -->
            @Override
            public HttpMessageConverters getObject() throws BeansException {<!-- -->
                return new HttpMessageConverters(new RestTemplate().getMessageConverters());
            }
        }));
    }
}

@FeignClient(name = "file", url = "http://localhost:8081", configuration = FeignSupportConfig.class)
public interface UploadClient {<!-- -->
    @PostMapping(value = "/upload-file", consumes = MediaType. MULTIPART_FORM_DATA_VALUE)
    String fileUpload(@RequestPart(value = "file") MultipartFile file);
}

Performance optimization

Replace the default communication component

OpenFeign uses Java’s built-in URLConnection object to create HTTP requests by default, but when connecting to production, if the underlying communication components can be replaced with dedicated communication components such as Apache HttpClient and OKHttp, based on the connection pools that come with these components, you can better Reuse and manage HTTP connection objects.

<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>
<!-- or add httpclient framework dependency -->
<dependency>
   <groupId>io.github.openfeign</groupId>
   <artifactId>feign-httpclient</artifactId>
</dependency>

Then add the following to the configuration file:

feign:
  okhttp:
    enabled: true
# or
Feign:
  httpclient:
    enabled: true

After the above settings, you can use okhttp, because automatic assembly has been realized in FeignAutoConfiguration

Data compression

In OpenFeign, data compression is not enabled by default. But if you transfer more than 1K bytes of data between services, it is strongly recommended to enable data compression. By default, OpenFeign uses Gzip to compress data. For large text, the compressed size is usually only 10%~30% of the original data, which will greatly improve bandwidth utilization. , add the following configuration in the project configuration file application.yml:

feign:
  compression:
    request:
      enabled: true # Turn on the compression function of the request data
      mime-types: text/xml, application/xml, application/json # compression type
      min-request-size: 1024 # The minimum compression value standard, when the data is greater than 1024, it will be compressed
    response:
      enabled: true # Turn on the response data compression function

Tip Reminder: If the application is computationally intensive and the CPU load exceeds 70% for a long time, since data compression and decompression require CPU operations, turning on the data compression function will add an additional burden to the CPU instead, resulting in system performance lower, which is not advisable. In this case It is recommended not to enable data compression

Load balancing

OpenFeign uses Ribbon by default to implement client load balancing, and its default load balancing strategy is the round robin strategy. So how to set Ribbon’s default load balancing strategy?

Just adjust the load balancing class used when microservices communicate in application.yml.

warehouse-service: #The microservice ID of the service provider
  ribbon:
    #Set the corresponding load balancing class
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

Tip Reminder: For performance considerations, we can choose to use weight strategy or region-sensitive strategy instead of polling strategy, because such execution efficiency is the highest.

A practice of encapsulating OpenFegin

Before understanding this practice, we need to know Feign’s writing support for interface inheritance

Feign inheritance support

Reference: https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-inheritance

Feign supports template APIs through single-inheritance interfaces. This allows grouping of common operations into convenient base interfaces.

Feign supports inheritance and multiple inheritance, so that we can expose the API to the interface, the service provider implements the interface to provide services, and the service consumer only needs to import the API module where the interface is located.

But earlier, the government did not advocate doing so. Related thinking can be seen:

  1. why-is-it-such-a-bad-idea-to-share-an-interface-between-server-and-client
  2. should-i-use-a-single-interface-for-feign-and-spring-mvc
public interface UserService {<!-- -->

    @RequestMapping(method = RequestMethod. GET, value = "/users/{id}")
    User getUser(@PathVariable("id") long id);
}
@RestController
public class UserResource implements UserService {<!-- -->
    @Override
    public User getUsers() {<!-- -->
        ...
    }

}
@FeignClient("users")
public interface UserClient extends UserService {<!-- -->

}

tips: @FeignClient interface should not be shared between server and client, and annotating @FeignClient interface with @RequestMapping at class level is not supported.

Practice

Extract Feign’s Client as an independent module, and put the interface-related POJO and default Feign configuration into this module for all consumers to use.

For example: Extract the default configurations of UserClients, User, and Feign into a feign-api package, and all microservices can refer to this dependency package and use it directly.

Service provider development practices

The RPC interface and implementation are placed in separate modules to facilitate service callers to reuse the service interface. The service interface module can only contain the most basic module dependencies (too many will lead to dependency transfer).

The service interface and related models must be placed in the same module, which only contains these contents. The FeignClient annotation cannot be specified in the interface of the service provider (specified on the consumer side), and annotations such as @RequestMapping can be used in the method< /strong>.

For example:

account
 -account-api
 -account-service

The account-api module puts the things that consumers need to use, api interface, vo, input parameters, etc…

public interface AccountApi {<!-- -->
    @RequestMapping(method = RequestMethod. GET, value = "/users/{id}")
    User getUser(@PathVariable("id") long id);
}

account-service implements the interface provided by account-api

@RestController
@Api(tags = "User Interface")
public class AccountController implements AccountApi {<!-- -->
    ...
}

Service consumer development practice

Refer to the RPC interface module, write the feign client class on the consumer side to inherit the relevant interface and handle the fuse, and add the FeignClient annotation to the client interface.

@Component
@FeignClient(name = "account-service", fallbackFactory = AccountClientFallbackFactory.class)
public interface AccountClient extends AccountApi {<!-- -->
    ...
}

@Component
public class AccountClientFallbackFactory implements FallbackFactory<AccountClient> {<!-- -->

    @Override
    public AccountClient create(Throwable throwable) {<!-- -->
        AccountClientFallback accountClientFallback = new AccountClientFallback();
        accountClientFallback.setCause(throwable);
        return accountClientFallback;
    }

}

@Slf4j
public class AccountClientFallback implements AccountClient {<!-- -->
    @Setter
    private Throwable cause;

    @Override
    public ResultData<AccountDTO> getByCode(String accountCode) {<!-- -->
...
    }

}

Extension

Spring Cloud OpenFeign Feature Completion Announcement

Since Spring now provides its own HTTP interface client solution, for example, there are two solutions for implementing interface calls in the latest Spring Boot 3.0:

  • RestTemplate
  • WebClient

Therefore, starting from the Spring Cloud 2022.0.0 version, the Spring Cloud OpenFeign module has been considered functionally complete, which means that the Spring Cloud team will no longer add new features to the module.

Although OpenFeign will not add new features, it will continue to fix bugs and security issues, and will also consider and review small pull requests from the community.

HTTP Interface

Spring 6 brings a new feature – HTTP Interface. This new feature allows developers to define an HTTP service as a Java interface containing a method marked with a specific annotation, and then complete the HTTP request by calling the interface method. It looks a lot like using Feign to complete remote service calls.

https://docs.spring.io/spring-framework/docs/6.0.0-RC1/reference/html/integration.html#rest-http-interface

syntaxbug.com © 2021 All Rights Reserved.