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", "系统异常,请稍后重试");
        }
    }
}

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在使用 Feign 进行远程服务调用时,可能会遇到各种异常情况,比如连接超时、请求失败等。Feign 提供了一些异常处理机制,可以方便地对这些异常进行统一处理。 Feign异常处理主要包括两个方面: 1. 对于 HTTP 响应码在 200 ~ 299 范围内的请求,Feign 不会抛出异常,而是将响应数据封装为方法返回值。如果响应码不在该范围内,Feign 会抛出 FeignException 异常。 2. FeignException 是 Feign 中的基础异常类,它包含了请求的相关信息,如请求方法、请求 URL、请求头、响应状态码等。我们可以通过捕获 FeignException 异常来统一处理 Feign 调用出现的异常情况。 例如,我们可以使用 @FeignClient 中的 fallback 属性来指定一个实现了当前接口的熔断器类,在请求失败时会触发该熔断器中的方法,从而实现异常的处理。具体实现可以参考以下代码: ```java @FeignClient(name = "service-provider", fallback = MyFallback.class) public interface MyFeignClient { @RequestMapping("/hello") String hello(); } @Component public class MyFallback implements MyFeignClient { @Override public String hello() { return "fallback"; } } ``` 在上述代码中,我们定义了一个名为 MyFeignClient 的 Feign 接口,并指定了 fallback 属性为 MyFallback.class。当调用 hello 方法时,如果请求失败,就会触发 MyFallback 类中的 hello 方法,并返回 "fallback" 字符串。 当然,除了使用 fallback 属性之外,我们还可以通过实现 ErrorDecoder 接口来自定义 FeignException 的处理方式,例如将异常信息记录到日志中,或者将异常信息转换为业务自定义异常类型等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值