简介
说明
本文用实例介绍feign的全局响应与异常处理。
之前介绍过feign的fallback,它可以出现异常时进行处理,使得代码出现问题能继续往下走,而不是直接报错。但实际上,一般出现异常时是出错直接抛异常来处理的,很少去使用fallback进行降级。
本文介绍的全局响应与异常处理,开发中更加常用。(与SpringMVC的全局处理用法基本相同,就多了个解码器(decoder)。SpringMVC的全局处理见:JavaWeb-全局响应处理与全局异常处理 – 自学精灵
代码结构
说明
本文将以电商中常见的下单减库存为例进行说明。
一共两个微服务:订单微服务(order),库存微服务(storage)。用户访问order的controller,order通过feign调用storage来减库存。流程图如下图所示:

- 本文只展示如何处理feign的全局异常,所以,不创建数据库之类的,只展示全局异常处理这个核心逻辑。
- 看本文(feign全局异常处理)的应该都是有一定工作经验的人,所以本文只展示核心代码。省略pom.xml、application.yml、项目搭建等。
代码结构图

(被我涂掉的是与本文无关的项目(配置中心、网关等))
框架的版本
spring-boot-starter-parent:2.3.7.RELEASE
spring-cloud-dependencies:Hoxton.SR9
核心代码
Feign的decoder(本文核心)
配置类
package com.example.common.core.feign;
import feign.Feign;
import feign.Logger;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Field;
@Configuration
public class FeignConfig {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Encoder feignEncoder() {
return new SpringEncoder(messageConverters);
}
@Bean
public Decoder feignDecoder() {
return new ResponseEntityDecoder(new FeignResultDecoder(messageConverters));
}
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public Feign.Builder feignBuilder(Decoder decoder) throws Exception {
Feign.Builder builder = Feign.builder().decoder(decoder);
// 默认为false。此时如果@FeignClient里的方法返回值是void,就执行不到自定义Decoder
Field forceDecoding = builder.getClass().getDeclaredField("forceDecoding");
forceDecoding.setAccessible(true);
forceDecoding.set(builder, true);
forceDecoding.setAccessible(false);
return builder;
}
}
decoder(解码器)
package com.example.common.core.feign;
import com.fasterxml.jackson.databind.JavaType;
import com.knife.example.common.core.entity.ResultWrapper;
import com.knife.example.common.core.util.JsonUtil;
import feign.FeignException;
import feign.Response;
import feign.codec.DecodeException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* feign响应的解码器
*/
@Slf4j
public class FeignResultDecoder extends SpringDecoder {
public FeignResultDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
super(messageConverters);
}
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
if (type == ResultWrapper.class) {
Reader reader = response.body().asReader(StandardCharsets.UTF_8);
String bodyString = reader.toString();
return super.decode(response.toBuilder().body(bodyString, StandardCharsets.UTF_8).build(), type);;
} else {
ResultWrapper resultWrapper = (ResultWrapper) decode(response, ResultWrapper.class);
if (resultWrapper.isSuccess()) {
if (type == Object.class) {
return resultWrapper.getData();
} else {
String json = JsonUtil.toJson(resultWrapper.getData());
JavaType javaType = JsonUtil.getObjectMapper().getTypeFactory().constructType(type);
return JsonUtil.toObject(json, javaType);
}
} else {
log.error("失败原因:" + resultWrapper.getMessage());
throw new DecodeException(response.status(), resultWrapper.getMessage(), response.request());
}
}
}
}
MVC全局异常/全局响应
全局异常
package com.example.common.common.advice;
import com.example.common.common.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE - 1)
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler(Exception.class)
public Result<Object> handleException(Exception e) throws Exception {
log.error(e.getMessage(), e);
// 如果某个自定义异常有@ResponseStatus注解,就继续抛出
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}
// 实际项目中应该这样写,防止用户看到详细的异常信息
// return new Result().failure().message.message("操作失败");
return new Result<>().failure().message(e.getMessage());
}
}
全局响应
package com.example.common.common.advice;
import com.example.common.common.entity.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE)
@ControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 若接口返回的类型本身就是ResultWrapper,则无需操作,返回false
// return !returnType.getParameterType().equals(ResultWrapper.class);
return true;
}
@Override
@ResponseBody
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
// 若返回值为String类型,需要包装为String类型返回。否则会报错
try {
ObjectMapper objectMapper = new ObjectMapper();
Result<Object> result = new Result<>().data(body);
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
throw new RuntimeException("序列化String错误");
}
} else if (body instanceof Result) {
return body;
}
return new Result<>().data(body);
}
}
FeignClient
package com.example.common.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient("storage")
public interface StorageFeignClient {
@PostMapping("/feign/storage/decreaseStorageFault")
void decreaseStorageFault(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
返回值包装类
package com.example.common.common.entity;
import lombok.Data;
@Data
public class Result<T> {
private boolean success = true;
private int code = 1000;
private String message;
private T data;
public Result() {
}
public Result(boolean success) {
this.success = success;
}
public Result<T> success(boolean success) {
Result<T> result = new Result<>(success);
if (success) {
result.code = 1000;
} else {
result.code = 1001;
}
return result;
}
public Result<T> success() {
return success(true);
}
public Result<T> failure() {
return success(false);
}
/**
* @param code {@link ResultCode#getCode()}
*/
public Result<T> code(int code) {
this.code = code;
return this;
}
public Result<T> message(String message) {
this.message = message;
return this;
}
public Result<T> data(T data) {
this.data = data;
return this;
}
}
调用端(order)
OrderController
package com.example.order.controller;
import com.example.common.common.entity.Result;
import com.example.common.feign.StorageFeignClient;
import com.example.order.entity.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* http://localhost:9011/order/createFault/?productId=1&count=10
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private StorageFeignClient storageFeignClient;
// 在减库存时刻意抛出异常
@PostMapping("createFault")
public Result createFault(Order order) {
storageFeignClient.decreaseStorageFault(order.getProductId(), order.getCount());
return new Result().message("创建订单成功");
}
}
当然,实际项目里应该把feign调用写到service中,我为了展示核心逻辑,直接写到了controller。
被调用端(storage)
package com.example.storage.feign;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeignController {
@PostMapping("/feign/storage/decreaseStorageFault")
public void decreaseStorageFault(@RequestParam("productId")Long productId, @RequestParam("count")Integer count) {
int i = 1 / 0;
}
}
测试
postman访问:http://localhost:9011/order/createFault/?productId=1&count=10
postman结果

调用方(order)后端结果
2021-09-19 20:02:47.454 ERROR 26884 --- [strix-storage-1] c.e.c.config.feign.FeignResultDecoder : 失败原因:/ by zero 2021-09-19 20:02:47.473 ERROR 26884 --- [nio-9011-exec-1] c.e.c.c.advice.GlobalExceptionAdvice : StorageFeignClient#decreaseStorageFault(Long,Integer) failed and no fallback available. com.netflix.hystrix.exception.HystrixRuntimeException: StorageFeignClient#decreaseStorageFault(Long,Integer) failed and no fallback available. at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:822) ~[hystrix-core-1.5.18.jar:1.5.18] at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:807) ~[hystrix-core-1.5.18.jar:1.5.18] at rx.internal.operators.OperatorOnErrorResumeNextViaFunction$4.onError(OperatorOnErrorResumeNextViaFunction.java:140) ~[rxjava-1.3.8.jar:1.3.8] at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87) ~[rxjava-1.3.8.jar:1.3.8] ... at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_201] at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_201] Caused by: feign.codec.DecodeException: / by zero at com.example.common.config.feign.FeignResultDecoder.decode(FeignResultDecoder.java:59) ~[classes/:na] at org.springframework.cloud.openfeign.support.ResponseEntityDecoder.decode(ResponseEntityDecoder.java:62) ~[spring-cloud-openfeign-core-2.2.6.RELEASE.jar:2.2.6.RELEASE] at feign.AsyncResponseHandler.decode(AsyncResponseHandler.java:115) ~[feign-core-10.10.1.jar:na] at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:87) ~[feign-core-10.10.1.jar:na] at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-10.10.1.jar:na] at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-10.10.1.jar:na] at feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:109) ~[feign-hystrix-10.10.1.jar:na] at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302) ~[hystrix-core-1.5.18.jar:1.5.18] at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:298) ~[hystrix-core-1.5.18.jar:1.5.18] at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46) ~[rxjava-1.3.8.jar:1.3.8] ... 27 common frames omitted
被调用方(storage)后端结果
2021-09-19 20:02:47.413 ERROR 28488 --- [nio-9021-exec-1] c.e.c.c.advice.GlobalExceptionAdvice : / by zero java.lang.ArithmeticException: / by zero at com.example.storage.feign.FeignController.decreaseStorageFault(FeignController.java:21) ~[classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201] at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.12.RELEASE.jar:5.2.12.RELEASE] at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.12.RELEASE.jar:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE] at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.12.RELEASE.jar:5.2.12.RELEASE] ... at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_201] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.41.jar:9.0.41] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_201]

请先 !