问题描述:开发中,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", "系统异常,请稍后重试");
}
}
}