Java异常处理SOP

一、常见异常处理方式

Service层func2,调用网关层func1

  • 直接吃掉

    public void func1() throws Exception1 {
      // ...
    }
    
    public void func2() {
      //...
      try {
        func1();
      } catch(Exception1 e) {
        log.warn("...", e); //吐掉:try-catch打印日志
      }
      //...
    }
    
  • 原封不动地re-throw

    public void func1() throws Exception1 {
      // ...
    }
    
    
    public void func2() throws Exception1 {//原封不动的re-throw Exception1
      //...
      func1();
      //...
    }
    
  • 包装成新的异常re-throw

    public void func1() throws Exception1 {
      // ...
    }
    
    
    public void func2() throws Exception2 {
      //...
      try {
        func1();
      } catch(Exception1 e) {
       throw new Exception2("...", e); // wrap成新的Exception2然后re-throw
      }
      //...
    }
    

当我们面对函数抛出异常的时候,应该选择上面的哪种处理方式呢?三个参考原则:

  • 如果func1()抛出的异常是可以恢复,且func2()的调用方并不关心此异常(弱依赖),我们完全可以在func2()内将func1()抛出的异常吞掉;
  • 如果func1()抛出的异常对func2()的调用方来说,强依赖,我们可以选择直接将func1抛出的异常re-throw;
  • 如果func1()抛出的异常太底层,对func2()的调用方来说,缺乏背景去理解、且业务概念上无关,我们可以将它重新包装成调用方可以理解的新异常,然后re-throw
  • 补充:网关层catch住异常的时候,可以不打error日志,因为很多网关层的方法都有重试3次,如果都是time out,则会打三个error信息,造成噪音干扰。方式一:网关层catch住异常,不打error只往上抛异常,调用方自己catch打异常信息。方式二:使用spring的retry注解,使用recover自定义方法,在原本网关方法内只抛不打error,在recover方法中打error,这样如果第一次、二次失败了,第三次成功了,则无error日志,如果三次全部失败,才有会打error日志
@Resource
private ProcesstService processtService;

	@Retryable(recover = "recoverSubmit")
    public String submit(BillSubmitRequest req) {
        try {
            BillSubmitResponse resp = processtService.submit(req);
            int code = resp.getErrorCode();
            if (code != 0) {
                throw new GatewayException(ExceptionCodeConstant.GATEWAY_EXCEPTION_CODE,"创建流程失败,resp:" + GsonUtils.toJsonStr(resp));
            }
            return resp.getBillNo();
        } catch (TException e) {
            throw new GatewayException(ExceptionCodeConstant.GATEWAY_EXCEPTION_CODE, "创建流程发生异常", e);
        }
    }

    @Recover
    public String recoverSubmit(Exception e, BillSubmitRequest req) throws Exception {
        log.error("创建流程异常,req:[{}]", GsonUtils.toJsonStr(req), e);
        throw e;
    }

二.try catch

1.catch类型

  • 业务内部逻辑校验、处理等

    catch (BusinessException e) {
                log.warn("xxx-内部校验失败~ ", e);
                response.setCode(Constants.BUSINESS_ERROR);
                response.setMessage(e.getMessage());
    } 
    
  • rpc失败:超时、网络、对方服务等不知道的原因导致的失败,内部异常

    catch (Exception e) {
                log.error("xxx~ ", e);
                response.setCode(Constants.INTERNAL_ERROR);
                response.setMessage("系统异常");
    }
    
  • 一次普通的rpc异常判断

    try {
                
            } catch (TException e) {
                throw new BusinessException(500, true, e.getMessage());
            }
    

2 插入数据的时候,需要更加精细的判断是为幂等冲突异常么

Lists.partition(doList, batchThreshold).forEach(partitionData -> {
            try {
                this.xxxMapper.batchInsert(partitionData);
            } catch (Exception e) {
                if (e instanceof DuplicateKeyException) {
                    log.warn("保存xxx数据重复,data:[{}]", GsonUtil.toJson(partitionData), e);
                } else {
                    log.error("保存xxx数据发生异常,data:[{}]", GsonUtil.toJson(partitionData), e);
                }
            }
        });

3. catch的作用

  • 作用:是用来捕获异常的

  • 若先用Exception捕获,则后面的异常类型都不会走到,一旦发生了异常,直接被最大的Exception捕获了

  • 如果rpc调用结果code != 0 ,则说明rpc调用失败,此时如果后续逻辑强依赖这个rpc结果,而这个结果又没有查出来/错误的,就需要throw异常。如果不是强依赖,比如一品多码,即使错误了,后续流程仍可以正常走,则仅需要log.error打个错误日志即可

  • 一般使用gateway调用的rpc,catch的异常,要看调用的方法,抛出的是TException、还是Exception,对应的catch住,如果需要throw则抛Gateway_error

  • Business_error和interalError的区别:interan异常主要是一些无法预料的原因导致的rpc失败,比如网络抖动超时等。

  • catch匹配到异常后,会把异常吃掉。如果你在catch中打了相关信息,没有再向上抛出异常,则异常就在此处被吃掉了。如果是@Trasactional注解,异常就不能被吃掉,就需要在catch中再向上throw,这样事物才能一致。

  • 调用者为前端的时候,如果你不想让前端在调用时抛出红色异常。那么你就不在最外层catch中再次throw一个异常,而是吃掉这个异常,并且给出相应的code值和message即可。打出error日志即可

    前提是和前端约定好,不是0code则为异常,前端自己定义错误信息也可以

  • 如果你的接口是给别人调用的(前端、别人)你需要给出rpc异常的,比如TException

4.异常处理sop

1、不要忽略捕捉的异常

catch (NoSuchMethodException e) {
   return null;
}

虽然捕捉了异常但是却没有做任何处理,除非你确信这个异常可以忽略,不然不应该这样做。这样会导致外面无法知晓该方法发生了错误,无法确定定位错误原因。

2、在你的方法里抛出定义具体的检查性异常

public void foo() throws Exception { //错误方式
}
推荐:
public void foo() throws SpecificException1, SpecificException2 { //正确方式
}

3、捕获具体的子类而不是捕获 Exception 类

try {
   someMethod();
} catch (Exception e) { //错误方式
   LOGGER.error("method has failed", e);
}
推荐:
try {
   rpc();
} catch (TException e) { 
   LOGGER.error("method has failed", e);
}

4、始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " + e.getMessage());  //错误方式
}
推荐:
catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " , e);  //正确方式
}

5、要么记录异常要么抛出异常,但不要一起执行

catch (NoSuchMethodException e) {  
//错误方式 
   LOGGER.error("Some information", e);
   throw e;
}

正如上面的代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,并且对尝试分析日志的同事很不友好。

6、finally 块中永远不要抛出任何异常

7、始终只捕获实际可处理的异常

catch (NoSuchMethodException e) {
   throw e; //避免这种情况,因为它没有任何帮助
}

不要为了捕捉异常而捕捉,只有在想要处理异常时才捕捉异常,或者希望在该异常中提供其他上下文信息。如果你不能在 catch 块中处理它,那么最好的建议就是不要只为了重新抛出它而捕获它。

8、不要使用 printStackTrace() 语句或类似的方法

最终别人可能会得到这些堆栈,并且对于如何处理它完全没有任何方法,因为它不会附加任何上下文信息。

9、记住早 throw 晚 catch 原则

应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。应该等到有足够的信息来妥善处理它。

10、在异常处理后清理资源

则仍应使用 try-finally 块来清理资源。 在 try 模块里面访问资源,在 finally 里面最后关闭资源。即使在访问资源时发生任何异常,资源也会优雅地关闭。

11、尽早验证用户输入以在请求处理的早期捕获异常

12、一个异常只能包含在一个日志中,在日志文件中这两个日志消息可能会间隔 100 多行。应该这样做:

LOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");
推荐:
LOGGER.debug("Using cache sector A, using retry sector B");

13、编写多重catch语句块注意事项:顺序问题:先小后大,即先子类后父类

否则,捕获底层异常类的catch子句将可能会被屏蔽。

14、多层try、catch

throw后面的任何语句不被执行,最邻近的try块用来检查它是否含有一个与异常类型匹配的catch语句。如果发现了匹配的块,控制转向该语句;如果没有发现,次包围的try块来检查,以此类推。如果没有发现匹配的catch块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。

5、throw new Exception中添加信息

throw new GatewayException(500,
                        String.format("xxxx失败,req:[%s]", GsonUtils.toJsonStr(day+" "+id+" " +type)), e)

5、捕获异常的粒度

1、多仓,异常粒度要小到单仓

2、并发查询200、200,异常处理要小到每一次查询

 	public void test(){
        ExecutorService pool = Executors.newFixedThreadPool(3);

		List<Long> poiIdList = Lists.newArrayList(1L, 2L, 3L);
        List<String> result = poiIdList .stream()
                .map(poiId -> CompletableFuture.supplyAsync(() -> {
                    try {
                        return queryPoiNameById(poiId);
                    } catch (Exception e) {
                        System.out.println(e);
                    }
                    return new ArrayList<String>();
                }, pool))
                .collect(Collectors.toList())
                .stream()
                .map(CompletableFuture::join)
                .flatMap(List::stream)
                .collect(Collectors.toList());

        System.out.println(result);
    }

    public List<String> queryPoiNameById(Long poiId) {
        return Lists.newArrayList("-" + poiId);
    }

6、网关异常定义

@Data
@EqualsAndHashCode(callSuper = true)
public class GatewayException extends RuntimeException{
    
    private Integer code;
    private String message;
    
    public GatewayException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
    
    public GatewayException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.message = message;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值