Feign implements global custom exception handling

Problem description: During development, when service A uses Feign to call service B, the parameter verification in service B fails and a custom exception is thrown. The error code is a custom error code. The error message is “XXXXX cannot be empty”. Return to When serving A, the feign exception interception of service A cannot obtain the custom error code. The default status of the exception information returned by OpenFeign’s FeignException is 500. As a result, the custom error code is lost. . Feig default exception information:
{
“timestamp”: 1698304783339,
“status”: 500,
“error”: “Internal Server Error”,
“path”: “/xx/get”
}
Therefore, we return a custom exception. The approximate steps are as follows:

1. Define a custom exception

package com.test.ft.common.exception;

import cn.hutool.http.HttpStatus;
import lombok.Data;

/**
 * @author aaa
 * @description Custom exception
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class CommonException extends RuntimeException {<!-- -->
    private static final long serialVersionUID = 91805175818790920L;
    private int code;

    private String msg;


    public CommonException(String msg) {<!-- -->
        super(msg);
        this.code = HttpStatus.HTTP_INTERNAL_ERROR;
        this.msg = msg;
    }

    public CommonException(int code, String msg) {<!-- -->
        super(msg);
        this.code = code;
        this.msg = msg;
    }


    public CommonException(String msg, Throwable cause) {<!-- -->
        super(msg, cause);
        this.code = HttpStatus.HTTP_INTERNAL_ERROR;
        this.msg = msg;
    }

    /**
     * Constructs a new runtime exception with the specified cause and a
     * detail message of <tt>(cause==null ? null : cause.toString())</tt>
     * (which typically contains the class and detail message of
     * <tt>cause</tt>). This constructor is useful for runtime exceptions
     * that are little more than wrappers for other throwables.
     *
     * @param cause the cause (which is saved for later retrieval by the
     * {@link #getCause()} method). (A <tt>null</tt> value is
     * permitted, and indicates that the cause is nonexistent or
     * unknown.)
     * @since 1.4
     */
    public CommonException(Throwable cause) {<!-- -->
        super(cause);
        this.code = HttpStatus.HTTP_INTERNAL_ERROR;
        this.msg = cause.getMessage();
    }

    public int getCode() {<!-- -->
        return code;
    }

    public void setCode(int code) {<!-- -->
        this.code = code;
    }

    @Override
    public String getMessage() {<!-- -->
        return msg;
    }

    public void setMsg(String msg) {<!-- -->
        this.msg = msg;
    }

}

2. Create Feign exception response global interception

package com.test.ft.common.config;

import com.test.ft.common.exception.CommonException;
import com.test.ft.common.wrapper.ResultBean;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletResponse;

/**
 * @author aaa
 * @description feign global exception interception
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {<!-- -->
    @ResponseBody
    @ExceptionHandler(CommonException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // Can be omitted
    public ResultBean<T> getResult(HttpServletResponse response, CommonException com){<!-- -->
        int code = com.getCode() == 0 ? HttpStatus.INTERNAL_SERVER_ERROR.value() : com.getCode();
        response.setStatus(code);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString());
        return ResultBean.error(code, com.getMsg());
    }
}

3. Create Feign exception interception

package com.test.ft.common.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.ft.common.exception.CommonException;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @author aaa
 * @description
 */
@Slf4j
@Configuration
public class FeignErrorDecoder implements ErrorDecoder {<!-- -->
    /**
     * Reimplement feign's exception handling and capture the exception information in json format returned by the restful interface
     */
    @Override
    public Exception decode(String methodKey, Response response) {<!-- -->
        Exception exception = null;
        ObjectMapper mapper = new ObjectMapper();
        //When setting the input, ignore attributes that exist in the JSON string but do not actually exist in the Java object
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //It is forbidden to use int to represent the order of enum to deserialize enum.
        mapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, true);
        try {<!-- -->
            String json = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
            if (StringUtils.isEmpty(json)) {<!-- -->
                return null;
            }
            int status = response.status();
            //Business exceptions are packaged into custom exception classes
            if (status != HttpStatus.OK.value()) {<!-- -->
                //Business exceptions throw 500 by default. Exceptions thrown by non-business exceptions are captured here.
                if (status != HttpStatus.INTERNAL_SERVER_ERROR.value()) {<!-- -->
                    JsonNode readTree = mapper.readTree(json);
                    String error = readTree.get("error").asText();
                    exception = new CommonException(readTree.get("status").intValue(), "Exception information: " + error + ", Error path: " + readTree.get("path").asText());
                } else {<!-- -->
                    exception = mapper.readValue(json, CommonException.class);
                }
            }
        } catch (IOException ex) {<!-- -->
            log.error("What the hell", ex);
            exception = new CommonException(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
        }
        return exception;
    }


}


Test results:

 Throws an exception on the callee of the service:


Exception information captured by the server:

You can see that the interception was successful and a custom exception was returned

Project structure:

Configuration location:

Service callee location:

Feign code example:

package com.example.ftcontract.feign;

import com.test.ft.common.entity.CompanyEntity;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

/**
 * @author aaa
 * @description feign example, name is configured in the configuration file of the web module, or can be written directly; url is the callee address and port, path is the callee link path
 */
@FeignClient(name = "${feign.server.name}",url = "localhost:8081",path = "/cc/xx")
public interface TestFeign {<!-- -->
    @GetMapping("get")
    List<CompanyEntity> getCompany();

}

The callee starts the class configuration and needs to scan the location of the Feign exception interception processor:

Service caller location:

The startup class of the service caller also needs to scan the package where Feign is located and the package where the exception handler is located:

Place relevant configurations in the general module, so that you do not need to configure Feign exception response global interception in each module. You can also set Feign exception response global interception in each module, and set different exception response information for each interception. As in the settings of module A:

@Slf4j
@RestControllerAdvice({<!-- -->"com.demo.center.feignimpl"})
public class FeignExceptionHandler {<!-- -->

//The exception thrown may be a custom exception or other runtime exception
    @ResponseBody
    @ExceptionHandler(value = {<!-- -->Exception.class})
    public ExceptionInfo handleFeignStatusException(Exception e, HttpServletRequest request, HttpServletResponse response) {<!-- -->
log.warn(e.getMessage(), e);
//The status of the response must be set. Not just 200.
        //For example, it is agreed that the call exception between services is the 555 error code
response.setStatus(555);
        //If it is a custom business exception
        if (e instanceof CommonException) {<!-- -->
            CommonException bize = (CommonException) e;
//Construct the return entity
            ExceptionInfo ei = new ExceptionInfo();
            //Exception time
            ei.setTimestamp("Write the time however you want");
            //custom error code
            ei.setCode(bize.getCode());
            //Customized error message prompt
            ei.setMessage(bize.getMessage());
            //Requested URI
            ei.setPath(request.getRequestURI());
            returnei;
        } else if (e instanceof UserException){<!-- -->
//If there are other custom exceptions, just add them here
}
        //If it is other runtime exceptions, you can uniformly return "System exception, please try again later"
        //Or other processing methods such as alarm, email, etc.
        ExceptionInfo ei = new ExceptionInfo();
        ei.setTimestamp("Write the time however you want");
        ei.setCode("111111");
        ei.setMessage("System exception, please try again later");
        ei.setPath(request.getRequestURI());
        returnei;
    }
}

Interception in the front-end module:

@Slf4j
@RestControllerAdvice({<!-- -->"com.demo.center.controller"})
public class GlobalJsonExceptionController {<!-- -->

    /**
     * ResponseBody's controller uniformly handles exceptions and custom exceptions
     * @param e
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = {<!-- -->Exception.class})
    public Response exception(Exception e) {<!-- -->
        log.warn(e.getMessage(), e);
        if (e instanceof IllegalArgumentException) {<!-- -->
            return Response.buildFailed(ResultCode.ILLEGAL_PARAM.getCode(),
                    ResultCode.ILLEGAL_PARAM.getDesc());
        } else if (e instanceof BizException) {<!-- -->
            return Response.buildFailed(((BizException) e).getCode(), e.getMessage());
        } else if (e instanceof MethodArgumentNotValidException) {<!-- -->
            BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
            List<FieldError> errors = bindingResult.getFieldErrors();
            //splicing message
            StringJoiner sj = new StringJoiner(",");
            for (FieldError error : errors) {<!-- -->
                sj.add(error.getDefaultMessage());
            }
            return Response.buildFailed("400", sj.toString());
        } else {<!-- -->
            return Response.buildFailed("500", "System exception, please try again later");
        }
    }
}