所有分类
  • 所有分类
  • 未分类

Spring Cloud Feign–全局响应与异常处理

简介

说明

本文用实例介绍feign的全局响应与异常处理。

之前介绍过feign的fallback,它可以出现异常时进行处理,使得代码出现问题能继续往下走,而不是直接报错。但实际上,一般出现异常时是出错直接抛异常来处理的,很少去使用fallback进行降级。

本文介绍的全局响应与异常处理,开发中更加常用。(与SpringMVC的全局处理用法基本相同,就多了个解码器(decoder)。SpringMVC的全局处理见:JavaWeb-全局响应处理与全局异常处理 – 自学精灵

代码结构

说明

本文将以电商中常见的下单减库存为例进行说明。

一共两个微服务:订单微服务(order),库存微服务(storage)。用户访问order的controller,order通过feign调用storage来减库存。流程图如下图所示:

  1. 本文只展示如何处理feign的全局异常,所以,不创建数据库之类的,只展示全局异常处理这个核心逻辑。
  2. 看本文(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]
0

评论0

请先

显示验证码
没有账号?注册  忘记密码?

社交账号快速登录