Feign调用之RuntimeException子异常透传实战

1,379 阅读4分钟

本文已参与「新人创作礼」活动.一起开启掘金创作之路。

Feign调用之RuntimeException子异常透传实战

一、背景

  • 在单体应用架构中,有时为了方便,在调用函数的深处出现了错误,希望不要沿着调用路径原路返回,而是希望直接通过抛出RuntimeException子异常的方式,直接统一处理异常并返回给客户端。
  • 在Dubbo调用中也存在上述情况。
  • 本文介绍了在微服务Feign调用链中从底层微服务抛出一个特定RuntimeException,并沿微服务调用链往外传递该异常的方法。

 

二、原理

image.png 调用过程解析:

(1) Consumer通过feign调用provider

(2) Provider执行过程中抛出BusinessException异常

(3) Provider的ExceptionHandler拦截到该异常,并返回HTTP状态码为510的响应,并序列化异常信息

(4) Consumer的Feign调用ErrorDecoder,解析后抛出HystrixBadRequestException

(5) Consumer的ExceptionHandler拦截到该异常,并返回HTTP状态码为510的响应,并序列化异常信息

注:在Consumer的FeignErrorDecoder中只有抛出HystrixBadRequestException,Hystrix才不会走fallback,其它异常都会走fallback流程,并计算调用错误一次(失败率累计到阈值会导致熔断)。hystrix的熔断和其它相关概念原理见本微服务吧Hystrix相关文档。

 

三、准备工作

Provider微服务

Consumer微服务

Consul注册发现服务中心

 

四、实战

  1. 实现Provider和Consumer都用到的ExceptionHandler
package cn.ucmed.otaku.healthcare.exception.handler;

import cn.ucmed.common.rubikexception.BusinessException;
import com.alibaba.fastjson.JSONObject;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import cn.ucmed.rubik.api.Result;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;


@ControllerAdvice
@Slf4j
public class MyExceptionHandler {

//    @ExceptionHandler(Throwable.class)
//    @ResponseStatus(value = HttpStatus.OK)
//    @ResponseBody
//    public ResponseEntity handleUnexpectedServerError(Throwable ex) {
//
//        log.error("ApiExceptionHandler.handleUnexpectedServerError", ex);
//        return getErrorResponse("目前业务繁忙,请您稍后!");
//    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity handleBusinessException(BusinessException be) {
        log.error("MyExceptionHandler.handleBusinessException", be);
        return getErrorResponse(be.getMessage(), be.getCode());
    }

    @ExceptionHandler(HystrixBadRequestException.class)
    public ResponseEntity HystrixBadRequestException(HystrixBadRequestException hbre) {
        log.error("MyExceptionHandler.HystrixBadRequestException", hbre);
        Result res = toResult(hbre);
        if (res == null || res.getMsg() == null) {
            throw hbre;
        }
        return getErrorResponse((String)res.getMsg(), res.getCode());
    }

    private ResponseEntity<ResultgetErrorResponse(String message) {
        Result result = new Result();
        result.setMsg(message);
        result.setCode(500);
        return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    private ResponseEntity<ResultgetErrorResponse(String message, Integer code) {
        Result result = new Result();
        result.setMsg(message);
        result.setCode(code);
        return new ResponseEntity<>(result, HttpStatus.NOT_EXTENDED);
    }


    private Result toResult(HystrixBadRequestException hbre) {
        if (hbre == null) {
            return null;
        }

        JSONObject obj = null;
        obj = JSONObject.parseObject(hbre.getMessage());

        Integer code = obj.getInteger("code");
        if (code == null){
            return null;
        }

        String message = obj.getString("msg");
        if(message == null){
            return null;
        }

        return new Result(code.intValue(), message);
    }
}

 2.  实现Consumer用到的FeignErrorDecoder

package cn.ucmed.otaku.healthcare.exception.handler;

import com.alibaba.fastjson.JSONObject;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;


@Component
@Slf4j
public class FeignErrorDecoder extends ErrorDecoder.Default {
    @Override
    public Exception decode(String s, Response response) {
        log.info("FeignErrorDecoder.decode(): method key: " + s);
        HystrixBadRequestException hbre = decodeBusinessException(response);
        if (hbre != null) {
            log.info("FeignErrorDecoder.decode(): receive bussiness exception! return HystrixBadRequestException");
            return hbre;
        }

        log.info("FeignErrorDecoder.decode(): return super.decode()");
        return super.decode(s, response);
    }

    private HystrixBadRequestException decodeBusinessException(Response response){
        String body = getBody(response);
        if ( body == null ){
            return null;
        }
        JSONObject obj = null;
        obj = JSONObject.parseObject(body);

        Integer code = obj.getInteger("code");
        if (code == null){
            return null;
        }

        String message = obj.getString("msg");
        if(message == null){
            return null;
        }

        return new HystrixBadRequestException(body);
    }

    private String getBody(Response response){
        if( response == null || response.body() == null ) {
            return null;
        }

        try {
            return Util.toString(response.body().asReader());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

 3. 测试

(1)在Provider中模拟exception

package com.example.exceptionprovider;

import cn.ucmed.common.rubikexception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;

@RestController
@Slf4j
public class ProviderController {
    private final AtomicLong counter = new AtomicLong(new Random().nextInt(1000));

    @Value("${server.port}")
    private int port;

    @RequestMapping("/test")
    public ProviderPOJO test(@RequestParam(value = "name", defaultValue = "provider") String name) throws IOException {
        if ("be".equalsIgnoreCase(name)) {
            throw new BusinessException(1,"simulation business exception");
        } else if ("re".equalsIgnoreCase(name)) {
            throw new RuntimeException("runtime exception");
        } else if ("io".equalsIgnoreCase(name)) {
            throw new IOException("io exception");
        }

        ProviderPOJO providerPOJO = new ProviderPOJO(counter.incrementAndGet(), name, port);
        log.info("ProviderController:test(): " + providerPOJO);
        return providerPOJO;
    }
}

 (2)运行Consul,Provider,Consumer

 (3)正常请求,浏览器访问: http://localhost:8090/test?name=normal

{"id":367,"name":"normal","port":8086,"providerPOJO":{"id":787,"name":"normal","port":8085}}

 (4)模拟BussinessException请求,浏览器访问:http://localhost:8090/test?name=be

{"code":1,"data":null,"msg":"test BusinessException"}

 (5)模拟其它Exception请求,浏览器访问:http://localhost:8090/test?name=re, 或http://localhost:8090/test?name=io

{"id":372,"name":"re","port":8086,"providerPOJO":null}

注:这一步模拟非BusinessException,feign的hystrix最终将按fallback流程处理 

  1. 实际项目建议

(1)FeignErrorDecoder和MyExceptionHandler实现可以放到公共jar中,被所有微服务依赖

(2)在具体微服务中启用上述实现,如果微服务包名和上述实现类的包名不同,需要自定义扫描包名:

@SpringBootApplication(scanBasePackages={"com.example.*""cn.ucmed.otaku.healthcare.exception.handler"})
public class ProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExceptionProviderApplication.class, args);
    }
}

 其中com.example.*表示provider包名,cn.ucmed.otaku.healthcare.exception.handler表示FeignErrorDecoder和MyExceptionHandler类所在包名

(3)在微服务实现中,抛出BusinessException要谨慎,毕竟java对exception的处理效率比正常处理要低,一般应避免滥用。

(4)一般而言要禁止随意在微服务层抛出除BusinessException外其它exception,也就是说微服务中抛出的非BusinessException需要在本微服务中Catch并处理,因为在微服务层抛出的非BusinessException低效,如果不处理最终将走到Consumer的feign的fallback流程,并计算一次错误,累计错误可能会导致熔断效果。