“This is the fifth day of my participation in the First Challenge 2022. For details: First Challenge 2022.”

The introduction

Hello, this is Anyin.

In our microservice development process, it is inevitable to involve the invocation between microservices. For example, the authentication Auth service needs to obtain User information from the User service. In the context of Spring Cloud buckets, we typically use Feign components to make calls between services.

We are all familiar with the general usage of Feign components, but are we familiar with the problems encountered by Feign components when building the entire microservices architecture? Let’s talk about it today.

Based on using

First, let’s implement a Feign component usage method.

  1. Import packages
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
Copy the code

A Loadbalancer component is also imported here, since the Loadbalancer is also used for client load underneath Feign.

  1. Enable FeignClient
@EnableFeignClients(basePackages = { "org.anyin.gitee.cloud.center.upms.api" })
Copy the code

Add the @enableFeignClients annotation to the class in our main entry and specify the location of the package scan

  1. Write the FeignClient interface
@FeignClient(name = "anyin-center-upms", contextId = "SysUserFeignApi", configuration = FeignConfig.class, path = "/api/sys-user")
public interface SysUserFeignApi {
    @GetMapping("/info/mobile")
    ApiResponse<SysUserResp> infoByMobile(@RequestParam("mobile") String mobile);
}
Copy the code

We customized a SysUserFeignApi and added the @FeignClient annotation. The related attributes are described as follows:

  • nameThe name of the application, which is essentiallyspring.application.nameIs used to identify an application and get the corresponding running instance information from the registry
  • contextIdWhen you have multiple interfaces using the samenameValue, you need to passcontextIdTo distinguish between
  • configurationSpecifies a specific configuration class
  • pathRequest prefix
  1. Write FeignClient interface implementation
@RestController
@RequestMapping("/api/sys-user")
public class SysUserFeignController implements SysUserFeignApi {
    @Autowired
    private SysUserRepository sysUserRepository;
    @Autowired
    private SysUserConvert sysUserConvert;
    @Override
    public ApiResponse<SysUserResp> infoByMobile(@RequestParam("mobile") String mobile) {
        SysUser user = sysUserRepository.infoByMobile(mobile);
        SysUserResp resp = sysUserConvert.getSysUserResp(user);
        returnApiResponse.success(resp); }}Copy the code

This is a simple Controller class with corresponding methods for querying user information based on mobile phone numbers.

  1. Client use
@Component
@Slf4j
public class MobileInfoHandler{
    @Autowired
    private SysUserFeignApi sysUserFeignApi;
    @Override
    public SysUserResp info(String mobile) {
        SysUserResp sysUser = sysUserFeignApi.infoByMobile(mobile).getData();
        if(sysUser == null) {throw AuthExCodeEnum.USER_NOT_REGISTER.getException();
        }
        returnsysUser; }}Copy the code

This is the code that uses the Feign component in the client Service, just like a Service method, called directly. There is no need to deal with the conversion of parameters during request and response.

At this point, the basic code for one of our Feign components is complete. At this time we are confident to run our code, test the interface is normal.

The above code is able to run normally. However, as we encountered more scenarios, we found that the ideal was very fleshly and thin, and the above code didn’t fit 100% of the scenarios we encountered.

Next, let’s take a look at what scenarios we encounter and how they need to be addressed.

Scenario 1: Logs

In the above code, because we do not make any configuration, so sysUserFeignApi infoByMobile way for us like a black box.

Although we have passed the mobile value, we do not know what the value of the real request user service is. Is there any other information passed together? Although the method returns a SysUserResp entity, we do not know what the user service returns. Is there any other information returned along with it? Although we know that the Feign component is an HTTP implementation underneath, does the request process pass header information?

All of this is a black box for us, which greatly hinders our ability to pull out the knife. Therefore, we need to configure logging to show all information passing during the request.

The @feignClient annotation has a parameter, Configuration, that specifies the specific configuration class, where we can specify the logging level. As follows:

public class FeignConfig {
    @Bean
    public Logger.Level loggerLevel(a){
        returnLogger.Level.FULL; }}Copy the code

You also need to specify the log level of the specific FeignClient as DEBUG in the configuration file.

logging:
  level:
    root: info
    org.anyin.gitee.cloud.center.upms.api.SysUserFeignApi: debug
Copy the code

At this point, when you request the interface, you will find a lot more logs.

As you can see in detail, all header information and request parameter information are carried at the beginning of the request, and all response information is generally printed when the response comes back.

Scenario 2: Transparent transmission of header information

In the previous section, we saw a lot of request header information in the log. Did the program add these by itself? Clearly not. For example, x-application-name and x-request-id are parameters we added ourselves.

The scenario in which header information needs to be transparently transmitted is usually the scenario in which the tenant ID or request ID is required. We here request ID, for example, a request, we know that the user may involve more than one service instance, when the application problems in order to convenient, we usually use the request ID to identify a user request, the request ID and throughout all the after service instance, and printed in the log. In this way, when a problem occurs, all log information requested by the user can be retrieved based on the request ID.

For the RequestId printed to the log, see: really, you can’t read the log with RequestId?

In this scenario, we need to manually set up pass-through information, and the Feign component gives us a way to do that. If the RequestInterceptor interface is implemented, header information can be passed through.

public class FeignRequestInterceptor implements RequestInterceptor {
    @Value("${spring.application.name}")
    private String app;
    @Override
    public void apply(RequestTemplate template) {
        HttpServletRequest request = WebContext.getRequest();
        // Job tasks may have no Request
        if(request ! =null&& request.getHeaderNames() ! =null){
            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                // The Accept value is not passed to avoid the need to respond to XML
                if ("Accept".equalsIgnoreCase(name)) {
                    continue; } String values = request.getHeader(name); template.header(name, values); } } template.header(CommonConstants.APPLICATION_NAME, app); template.header(CommonConstants.REQUEST_ID, RequestIdUtil.getRequestId().toString()); template.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); }}Copy the code

Scenario 3: Exception handling

In the first section, when our client called, we did not handle the exception, we directly returned.getData(), which is actually a very dangerous operation,.getData() may return null, which is easy to cause NPE situation.

        SysUserResp sysUser = sysUserFeignApi.infoByMobile(mobile).getData();
Copy the code

Recall that when we call the Service method of another current Service and get an exception, do we just throw an exception and send it to the unified exception handler? So, here we also expect Feign and Service to encounter an exception and be handled by a unified exception.

How do you handle this requirement? We can process it as we decode it.

@Slf4j
public class FeignDecoder implements Decoder {
    // The proxy's default decoder
    private Decoder decoder;
    public FeignDecoder(Decoder decoder) {
        this.decoder = decoder;
    }
    @Override
    public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
        // serialize to JSON
        String json = this.getResponseJson(response);
        this.processCommonException(json);
        return decoder.decode(response, type);
    }
    // Handle common business exceptions
    private void processCommonException(String json){
        if(! StringUtils.hasLength(json)){return;
        }
        ApiResponse resp = JSONUtil.toBean(json, ApiResponse.class);
        if(resp.getSuccess()){
            return;
        }
        log.info("feign response error: code={}, message={}", resp.getCode(), resp.getMessage());
        // Throw the expected business exception
        throw new CommonException(resp.getCode(), resp.getMessage());
    }
    // The response value is converted to json string
    private String getResponseJson(Response response) throws IOException {
        try (InputStream inputStream = response.body().asInputStream()) {
            returnStreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); }}}Copy the code

In this case, when decoding, we first get the judgment whether there is a business exception from the response result. If so, we construct the business exception instance and then throw the information.

Running the code, we can see that the unified exception side still can’t handle exceptions returned by the downstream service. The reason is that although we throw a CommonException, it will be caught by Feign, repackaged as a DecodeException, and then thrown

Object decode(Response response, Type type) throws IOException {
    try {
      return decoder.decode(response, type);
    } catch (final FeignException e) {
      throw e;
    } catch (final RuntimeException e) {
      // Rewrap the exception
      throw newDecodeException(response.status(), e.getMessage(), response.request(), e); }}Copy the code

So, we also need to do a bit more processing in the unified exception side, the code is as follows:

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(DecodeException.class)
    public ApiResponse decodeException(DecodeException ex){
        log.error("Decoding failed: {}", ex.getMessage());
        String id = RequestIdUtil.getRequestId().toString();
        if(ex.getCause() instanceof CommonException){
            CommonException ce = (CommonException)ex.getCause();
            return ApiResponse.error(id, ce.getErrorCode(), ce.getErrorMessage());
        }
        return ApiResponse.error(id, CommonExCodeEnum.DATA_PARSE_ERROR.getCode(), ex.getMessage());
    }
Copy the code

Under the running code, we can see the exception from user service -> authentication service -> gateway -> front-end such a process.

  • An exception thrown by the user service

  • An exception thrown by the authentication service

  • The front-end display is abnormal

Scenario 4: Time zone problems

As the business changes, we may add a parameter about the Date type to the request parameter or the response parameter. In this case, you may find that the time zone is wrong, 8 hours less.

This problem is actually caused by the Jackson component, and there are different solutions to this problem.

  1. At the end of eachDateAttribute add to@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
  2. The transfer parameter is changedyyyy-MM-dd HH:mm:ssFormatted character
  3. Unified configurationspring.jackson

Obviously, the third solution is the most suitable, we can do the following configuration in the configuration file.

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
Copy the code

It’s important to note that our @FeignClient configuration is a custom configured FeignConfig class in which the decoder is loaded. The decoder relies on the global HttpMessageConverters instance, which is the same instance SpringMVC relies on. Therefore, the configuration takes effect. This configuration does not take effect in some scenarios where HttpMessageConverters are customized.

public class FeignConfig {
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;
    // Custom decoder
    @Bean
    public Decoder decoder(ObjectProvider<HttpMessageConverterCustomizer> customizers){
        return new FeignDecoder(
                new OptionalDecoder(
                        new ResponseEntityDecoder(
                                newSpringDecoder(messageConverters, customizers)))); }}Copy the code

Other problems

I don’t know if you noticed in the first section when I defined the SysUserFeignApi, I used a path attribute on the @FeignClient annotation and didn’t use the @requestMapping annotation on the interface.

Remember when we used Feign, did we use it like this:

@FeignClient(name = "anyin-center-upms", contextId = "SysUserFeignApi", configuration = FeignConfig.class)
@RequestMapping("/api/sys-user")        
public interface SysUserFeignApi {}
Copy the code

The reason I don’t use this is because my current version of Spring Cloud OpenFeign doesn’t recognize @RequestMapping anymore, it doesn’t prefix requests with requests, So even if the @requestMapping annotation is recognized by SpringMVC as a Controller class, it doesn’t work.

So, the path attribute is used here.

The last

These are most of the scenarios we encounter when using OpenFeign components, do you understand?

Anyin Cloud