Feign实现全局自定义异常处理

问题描述:开发中,A服务使用Feign调用B服务时,B服务中参数校验未通过抛出了自定义异常,错误码是自定义错误码,错误消息是“XXXXX不能为空”,返回到A服务时,A服务的feign异常拦截无法获取到自定义错误码。OpenFeign的FeignException返回的异常信息默认status为500。导致自定义错误码丢失。。Feig默认异常信息:
{
“timestamp”: 1698304783339,
“status”: 500,
“error”: “Internal Server Error”,
“path”: “/xx/get”
}
因此,我们返回自定义的异常。大概步骤如下:

1.定义一个自定义的异常

package com.test.ft.common.exception;

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

/**
 * @author aaa
 * @description 自定义异常
 */
@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.创建Feign异常响应全局拦截

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 全局异常拦截
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler  {
    @ResponseBody
    @ExceptionHandler(CommonException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 可不加
    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.创建Feign异常拦截

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 {
    /**
     * 重新实现feign的异常处理,捕捉restful接口返回的json格式的异常信息
     */
    @Override
    public Exception decode(String methodKey, Response response) {
        Exception exception = null;
        ObjectMapper mapper = new ObjectMapper();
        //设置输入时忽略在JSON字符串中存在但Java对象实际没有的属性
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //禁止使用int代表enum的order来反序列化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();
            // 业务异常包装成自定义异常类
            if (status != HttpStatus.OK.value()) {
                // 业务异常默认抛出500,此处捕捉非业务抛出的异常
                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(), "异常信息:" + error + ",错误路径:" + readTree.get("path").asText());
                } else {
                    exception = mapper.readValue(json, CommonException.class);
                }
            }
        } catch (IOException ex) {
            log.error("啥子哦", ex);
            exception = new CommonException(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());
        }
        return exception;
    }


}


测试结果:

 在服务的被调用方抛出异常:

在这里插入图片描述
服务方的捕获异常信息:
在这里插入图片描述
可以看到已经拦截成功,返回了自定义的异常

项目结构:

配置所在位置:
在这里插入图片描述
服务被调用方位置:
在这里插入图片描述

Feign代码示例:

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示例,name配置在web模块的配置文件中,也可以直接写;url为被调用方地址和端口,path为被调用方链接路径
 */
@FeignClient(name = "${feign.server.name}",url = "localhost:8081",path = "/cc/xx")
public interface TestFeign {
    @GetMapping("get")
    List<CompanyEntity> getCompany();

}

被调用方启动类配置,需要扫描到Feign异常拦截处理器所在位置:
在这里插入图片描述

服务调用方位置:
在这里插入图片描述
服务调用方的启动类也需要将Feign所在位置的包,异常处理器位置所在的包扫描进去:
在这里插入图片描述

将相关的配置放置在通用模块里面,这样就不用在每个模块都配置Feign异常响应全局拦截,也可以在每个模块里面设置Feign异常响应全局拦截,每个拦截设置不同的异常响应信息。如在A模块的设置:

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

	//抛出的异常可能是自定义异常,也可能是其他运行时异常
    @ResponseBody
    @ExceptionHandler(value = {Exception.class})
    public ExceptionInfo handleFeignStatusException(Exception e, HttpServletRequest request, HttpServletResponse response) {
		log.warn(e.getMessage(), e);
		//必须要设置response的status。不是200就行。
        //比如统一约定服务间调用异常为555错误码
		response.setStatus(555);
        //如果是自定义业务异常
        if (e instanceof CommonException) {
            CommonException bize = (CommonException) e;
			//构建返回实体
            ExceptionInfo ei = new ExceptionInfo();
            //异常时间
            ei.setTimestamp("时间随便想怎么写就怎么写");
            //自定义的错误码
            ei.setCode(bize.getCode());
            //自定义的错误消息提示
            ei.setMessage(bize.getMessage());
            //请求的URI
            ei.setPath(request.getRequestURI());
            return ei;
        } else if (e instanceof UserException){
			//如果有其他的自定义异常,在这里添加即可
		}
        //如果是其他的运行时异常,可以统一返回"系统异常,请稍后重试"
        //或者报警、邮件等其他处理方式
        ExceptionInfo ei = new ExceptionInfo();
        ei.setTimestamp("时间随便想怎么写就怎么写");
        ei.setCode("111111");
        ei.setMessage("系统异常,请稍后重试");
        ei.setPath(request.getRequestURI());
        return ei;
    }
}

在前端模块的拦截:

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

    /**
     * ResponseBody 的controller 统一处理异常 自定义异常
     * @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();
            //拼接message
            StringJoiner sj = new StringJoiner(",");
            for (FieldError error : errors) {
                sj.add(error.getDefaultMessage());
            }
            return Response.buildFailed("400", sj.toString());
        } else {
            return Response.buildFailed("500", "系统异常,请稍后重试");
        }
    }
}

Feign是Netflix的一个开源库,用于声明式API客户端,但在请求远程服务时可能会遇到`feign.FeignException`,其中包含一个特定的异常`NotFoundLabException`,表示目标服务未找到(404错误)。当Feign尝试访问一个不存在的URL时,它会抛出这个异常。 对于这种异常的统一处理,通常我们会采用以下步骤: 1. **全局异常处理器**:在应用程序启动时,设置一个全局Feign client拦截器或者全局异常处理器,如Spring Cloud的`GlobalResponseHandler`或`GlobalFeignClientInterceptor`。在这里,可以捕获所有`NotFoundLabException`并将其转化为自定义的异常或者日志信息。 ```java @FeignClient(name = "lab-service") public interface LabService { //... } @Configuration public class FeignConfiguration { @Bean public GlobalResponseHandler globalResponseHandler() { return new GlobalResponseHandler() { @Override public Response handleResponse(Response original) { try { if (original.status() == HttpStatus.NOT_FOUND) { throw new NotFoundException("Resource not found"); } } catch (IOException e) { // log and handle the error } return original; } }; } ``` 2. **业务逻辑层处理**:如果需要对这类异常进行更具体的业务处理,比如记录错误日志、提供友好的用户提示等,可以在调用Feign客户端的地方添加额外的检查和处理。 3. **异常传播**:如果不想中断整个请求流程,可以选择让程序继续执行,但忽略找不到的服务。这可以通过将处理逻辑放入try-catch块中,并返回默认值或者错误状态码给前端。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值