微服务架构下的异常处理

微服务架构下的统一异常处理

微服务架构实际上是一种将具有不同功能、不同部署位置的服务协调起来共同工作的分布式系统。由于分布式系统的复杂性,对这些服务之间的协调产生的异常处理就显得尤为重要了,好的异常处理不仅便于在异常发生的时候定位跟踪问题,还有利于保证系统的稳定性并可提高系统交互体验的友好性。所以,微服务开发过程中考虑异常情况的处理与考虑正常逻辑同等重要。

本文简单串联Java异常的基础知识和Spring提供的异常处理机制以及Spring Cloud服务间的异常处理机制,以实际项目实践为例说明在Spring Cloud微服务架构下自定义异常及统一处理异常的要点。

Java异常基础知识

Throwable 类是 Java 语言中所有错误或异常的超类,其子类分为Error、Exception。

  • Error是合理的应用程序不应该试图捕获的严重问题

  • Exception是合理的应用程序想要捕获的问题,Exception又分为CheckedException和UnCheckedException。

  • [ ] CheckedException是对于可恢复的条件使用被检查的异常,需要用try…catch…显式的捕获并且进行处理,例如IOException。

  • [ ] UncheckedException又叫做RuntimeException不是必须捕获的,对于导致程序无法执行的异常,必要时转化成有具体业务描述说明的异常。

异常处理的注意事项:

  • 一般来说异常处理首先要防止异常丢失,避免使用覆盖式异常处理块,可以考虑使用异常链的特性处理,即使用从Throwable继承的initCause()。
  • 在截获异常进行处理时要注意对异常进行一番处理,如:修正错误,给出明确位置,描述,原因以及提醒。
  • 使用Try…finally结构保证发生异常时,资源也能正确释放、关闭。
  • 截获异常后进行相应处理,根据业务逻辑需要,必要时可以重新抛出异常。
  • 最好的处理方法是对异常信息分类,然后进行封装处理。
  • 尤其需要注意的是避免把异常截获后不做任何处理,直接吞了,项目上不少初级程序员犯这类错误可能导致程序出错,不好查找原因。

Spring异常处理的三种方式

  1. 使用@ExceptionHandlerannotation 实现在配置的Controller中拦截并处理异常
@Controller
public class UserController {

    private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);

    @RequestMapping(value="/user/{id}", method=RequestMethod.GET)
    public String getUser(@PathVariable("id") int id, Model model) throws Exception{
        if(id==1){
            throw new UserNotFoundException(id);
        }

    }

    @ExceptionHandler(UserNotFoundException.class)
    public ModelAndView handleUserNotFoundException(HttpServletRequest request, Exception ex){
        logger.error("Requested URL="+request.getRequestURL());
        logger.error("Exception Raised="+ex);

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("exception", ex);
        modelAndView.addObject("url", request.getRequestURL());    
        modelAndView.setViewName("error");
        return modelAndView;
    }    

    /**  输入参数仅有exception
    @ExceptionHandler
    public String handleException(UserNotFoundException exception) {
    return "errorView";
    }
    */
}

实现HandlerExceptionResolver接口,进行全局的异常拦截,使用时需要在MVCConfiguration中初始化异常处理器

public class ServiceHandlerExceptionResolver implements HandlerExceptionResolver {
    Logger logger = LoggerFactory.getLogger(ServiceHandlerExceptionResolver.class);
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler, Exception e) {
        e.printStackTrace();
        Throwable rex = e;
        if (rex instanceof CIBaseException) {
            this.writeError(((CIBaseException) rex).getErrCode(), rex.getMessage(), httpServletResponse);
        } else  {
            this.writeError("", "系统运行出错", httpServletResponse);
        }
        return new ModelAndView();
    }
}

@Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter {

   //需要在MVCConfiguration中初始化异常处理器
    @Override
    public void configureHandlerExceptionResolvers(List exceptionResolvers) {
        exceptionResolvers.add(new ServiceHandlerExceptionResolver());
    }


}

使用@ControllerAdviceannotation类里定义@ExceptionHandler, 对项目中所有Controller中带有@ExceptionHandler注释的方法拦截并进行处理异常

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    @ExceptionHandler(SQLException.class)
    public String handleSQLException(HttpServletRequest request, Exception ex){
        logger.info("SQLException Occured:: URL="+request.getRequestURL());
        return "database_error";
    }

    @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="IOException occured")
    @ExceptionHandler(IOException.class)
    public void handleIOException(){
        logger.error("IOException handler executed");
        //returning 404 error code
    }

  /** 输入参数仅有exception
    @ExceptionHandler
    public String handleException(UserNotFoundException exception) {
       return "errorView";
    }
  */
}

Spring Cloud架构下统一处理异常的实例

本小节以实际项目为例,说明在Spring Cloud项目架构下自定义异常并统一处理异常的机制。本项目的服务结构如图所示:

  • 所有的服务都采用Eureka Server注册
  • 所有服务配置都采用Config Server实现服务注册
  • 浅蓝色框中的服务为对外部系统提供服务的Open Service层,统一处理身份认证及安全等问题
  • 橙色框中的服务为内部服务,都是Open Service层的调用向外暴露服务
  • 系统中的服务大都采用声明式样REST客户端Feign作为服务客户端接口

微服务架构下的异常处理_第1张图片

首先,项目统一定义了基础异常类,并根据异常类型及业务分类定义了错误代码编号。所有的Internal Service层的服务对于可预见的异常都包装成系统基础异常,其他异常可以原样抛出。具体的异常类定义如下:

public class CIBaseException extends RuntimeException {
    private static final long serialVersionUID = *****;
    public static final int UNKNOWN_EXCEPTION = 0;
    public static final int BIZ_EXCEPTION = 1;
    public static final int TIMEOUT_EXCEPTION = 2;
    public static final int FORBIDDEN_EXCEPTION = 3;
    public static final int CISYS_EXCEPTION = 4;

    private int typecode = BIZ_EXCEPTION;// CIBaseException,异常类型用tpecode表示
    private String errCode;

    public CIBaseException() {
        super();
    }

    public CIBaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public CIBaseException(String message) {
        super(message);
    }

    public CIBaseException(Throwable cause) {
        super(cause);
    }


    public CIBaseException(int type, String errCode) {
        super();
        this.typecode = type;
        this.errCode = errCode;
    }

    public CIBaseException(int type, String errCode, String message, Throwable cause) {
        super(message, cause);
        this.typecode = type;
        this.errCode = errCode;
    }

    public CIBaseException(int type, String errCode, String message) {
        super(message);
        this.typecode = type;
        this.errCode = errCode;
    }

    public CIBaseException(int type, String errCode, Throwable cause) {
        super(cause);
        this.typecode = type;
        this.errCode = errCode;
    }
    ...
    }

系统在Open Service层的服务中定义了一个CustomerErrorDecoder实现了ErrorDecoder接口处理调用远程服务产生的异常(若在应用中配置了ErrorDecoder的Bean,则Spring Cloud Netflix会在创建服务客户端时在应用上下文里查找该bean以处理服务调用异常获取到的异常)。具体来说ErrorDecoder Bean主要是在Decode方法中从response中获取到异常信息,将所有远程服务产生的业务异常都还原成CIBaseException的业务异常,并把其他类型的异常也包装成提示信息友好异常。

 public Exception decode(String s, Response response) {
        CIBaseException ex = null;
        try {
            if(response.body() != null) {
                String body = Util.toString(response.body().asReader());
                logger.error(body);
                ExceptionInfo ei = this.objectMapper.readValue(body.getBytes("UTF-8"), ExceptionInfo.class);

                String message = ei.getMessage();
                String code = "";

                if (message.matches(this.errCodePattern)) {
                    String [] cm = this.parseCodeMessage(message);
                    code = cm[0];
                    message = cm[1];
                    ex = new CIBaseException(CIBaseException.BIZ_EXCEPTION, code, message);
                }
            }
        } catch (IOException var4) {
            var4.printStackTrace();
            ex = new CIBaseException(CIBaseException.UNKNOWN_EXCEPTION, "", "系统运行异常");
        }

        return null != ex ? ex : new CIBaseException(CIBaseException.UNKNOWN_EXCEPTION, "", "系统运行异常");
    }

在Open Service层的服务中定义统一的Controller层错误处理类,解析异常对象,转化成友好的异常提示写入Response返回给前端页面进行展示,resoveException方法如下:

  public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {

        e.printStackTrace();

        Throwable rex = e;

        if (e instanceof HystrixRuntimeException) {
            rex = e.getCause();
        }

        if (rex instanceof CIBaseException) {

            CIBaseException cbe = (CIBaseException) rex;
            if (StringUtils.isBlank(cbe.getErrCode())
                    && cbe.getMessage().matches(this.errCodePattern)) {
                String [] cm = this.parseCodeMessage(cbe.getMessage());
                this.writeError(cm[0], cm[1], httpServletResponse);
            } else {
                this.writeError(cbe.getErrCode(), rex.getMessage(), httpServletResponse);
            }

        } else  {
            this.writeError("", "系统运行出错", httpServletResponse);
        }

        return new ModelAndView();
    }

开发过程中使用自定义异常的注意事项

系统统一异常处理框架会捕获异常,对于需要处理的异常。 程序开发过程中根据逻辑可以创建异常,必要时转换成可描述清楚异常发生时的业务场景的异常抛出。 需要注意的是不要打印异常,统一异常处理框架会打印异常,避免重复打印。 自定义异常类的使用需要注意以下三点:

  • 包装转换时时注意传入原生异常,避免丢失异常信息,异常消息可以带业务描述,示例如下:
private int countHighScore(String score){
        int counter = 0;
        try{
            float fscore = Float.valueOf(score);
            ....
        }catch(NumberFormatException nex){
             //注意:此处传入原生异常
             throw new CIBaseException(ExceptionConstants.DEMO_SCORE_FORMAT_INVALID,nex);//输入参数格式错误!
        }
        return counter;
    }

未传入原生异常,会丢失具体异常信息,如下所示:

com.isoftstone.cityinsight.exception.CIBaseException: 0X001001=输入参数格式错误!
    at com.isoftstone.ci.user.web.controller.DemoUserController.countHighScore(DemoUserController.java:73)
    at com.isoftstone.ci.user.web.controller.DemoUserController.userTest(DemoUserController.java:58)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)

包含原生异常的完整信息,如下所示:

java.lang.NumberFormatException: For input string: "ww"
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
    at sun.misc.FloatingDecimal.parseFloat(FloatingDecimal.java:122)
    at java.lang.Float.parseFloat(Float.java:451)
    at java.lang.Float.valueOf(Float.java:416)
    at com.isoftstone.ci.user.web.controller.DemoUserController.countHighScore(DemoUserController.java:71)
    at com.isoftstone.ci.user.web.controller.DemoUserController.userTest(DemoUserController.java:58)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
  • 创建的业务异常,必要的时候增加详细的运行时获取的具体信息,便于异常获取端分析问题,示例如下:
/*找到某人的基本信息,并且根据基本信息计算分析其专业能力分值*/
public Score countScoreByUserId(String userid){
  CIUser user = userservice.getById(userid);
  if (user == null) {
     //注意:此处加入了运行时数据信息
     throw new CIBaseException(ExceptionConstants.DEMO_USER_NOT_FOUND+", userid:"+userid);//该用户不存在
  }
  Score score = scoreservice.countScore(user);
  return score;
}
  • 捕获通用异常并进行异常处理,并转换成有明确描述业务异常抛出去,例如IOException、SQLException
public String readfile(String filePath) {
        FileInputStream fileInputStream = null; 
        StringBuffer sb=new StringBuffer();
        String tempstr=null;

        try{
            File file = new File(filePath);
            fileInputStream    = new FileInputStream(file);
            BufferedReader br=new BufferedReader(new InputStreamReader(fis));
            while((tempstr=br.readLine())!=null)
                sb.append(tempstr);
        }catch(FileNotFoundException fex){
            throw  new CIBaseException("ExceptionConstants.DEMO_LICENCEFILE_NOT_FOUND"),e);//文件不存在
        }catch(IOException e){
             throw  new CIBaseException("ExceptionConstants.DEMO_LICENCEFILE_IOEXCEPTION),e);//IO异常
        }finally{
            try{
                //关闭资源()
                if(fileInputStream!=null){
                    fileInputStream.close();        
                }
            }catch(IOException ioex){
               throw  new CIBaseException("0X01C001=".concat("读Licens文件失败(关闭资源失败..)!"),e);
            }
        }
        return sb.toString();
    }

你可能感兴趣的:(微服务)