简介
说明
本文用实例介绍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]
请先
!