乐趣区

关于后端:Service-层异常抛到-Controller-层处理还是直接处理

0 前言

个别初学者学习编码和 [错误处理] 时,先晓得 [编程语言] 有一种处理错误的模式或约定(如 Java 就抛异样),而后就开始用这些工具。但却漠视这问题实质:处理错误是为了写正确程序。可是

1 啥叫“正确”?

由解决的问题决定的。问题不同,解决方案不同。

如一个 web 接口承受用户申请,参数 age,兴许业务要求字段是 0~150 之间整数。如输出字符串或正数就必定不承受。个别在后端某地做输出合法性检查,不过就抛异样。

但归根到底这问题“正确”解决办法总是要以某种模式提醒用户。而提醒用户是某种前端工作,就要看界面是 app,H5+AJAX 还是相似于 [jsp] 的服务器产生界面。不论啥,你要依据需要去”设计一个修复谬误“的流程。如一个常见的流程要后端抛异样,而后一路到某个集中处理错误的代码,将其转换为某个 HTTP 的谬误(业务错误码)提供给前端,前端再映射做”提醒“。如用户输出非法申请,从逻辑上后端都没法本人修复,这是个“正确”的策略。

2 报 500 了嘞!

如用户上传一个头像,后端将图片发给[云存储],后果云存储报 500,咋办?你可能想重试,因为兴许仅是[网络抖动],重试就能失常执行。但若重试屡次有效,若设计了某种热备计划,可能改为发到另一个服务器。“重试”和“应用备份的依赖”都是“立即解决“。

但若重试有效,所有的 [备份服务] 也有效,兴许就能像下面那样把谬误抛给前端,提醒用户“服务器开小差”。从这计划易看出,你想把谬误抛到哪里是因为那个 catch 的中央是解决问题最不便的中央。一个问题的解决方案可能要几个不同的错误处理组合起来能力办到。

3 NPE 了!

你的程序抛个 NPE。这个别就是程序员的 bug:

  • 要不就是程序员想表白一个货色”没有“,后果在后续解决中忘判断是否为 null
  • 要不就是在写代码时感觉 100% 不可能为 null 的中央呈现了一个 null

不论哪种,这谬误用户总会看到一个很含混的报错信息,这远远不够。“正确”方法是程序员本人能尽快发现它,并尽快修复。要做到这点,须要 [监控零碎] 一直爬 log,把问题报警进去。而非等用户找客服投诉。

4 OOM 了!

比方你的 [后端程序] 忽然 OOM 挂了。挂的程序没法复原本人。要做到“正确”,须在服务之外的容器思考这问题。

如你的服务跑在[k8s],他们会监控你程序状态,而后重启新的服务实例补救挂掉的服务,还得调整流量,把去往宕机服务的流量切换到新实例。这的复原因为跨零碎所以不能仅用异样实现,但情理一样。

但光靠重启就“正确”了?若服务是齐全无状态,问题不大。但若有状态,局部用户数据可能被执行一半的申请搞乱。因而重启要注意先“复原数据到非法状态”。这又回到你要晓得咋样才是“正确”的做法。只依附简略的语法性能不能无脑解决这事。

5 晋升维度

  • 一个工作线程的“内部容器“是管理工作线程的“master”
  • 一个网络申请的“内部容器”是一个 Web Server
  • 一个用户过程的“内部容器”是[操作系统]
  • Erlang 把这种 supervisor-worker 的机制融入到语言的设计

Web 程序很大水平能把异样抛给顶层,是因为:

  • 申请来自前端,对因为用户申请有误(数据合法性、权限、用户上下文状态)造成的问题,最终根本只能通知用户。因而抛异样到一个集中处理错误的中央,把异样转换为某个业务错误码的办法,正当
  • 后端服务个别无状态。这也是软件系统设计的个别准则。无状态才意味着可随时随地安心重启。用户数据不会因为因为下一条而会出问题
  • 后端对数据的批改依赖 DB 的事务。因而一个改一半的、没提交的事务不会造成副作用。

但这 3 条件并非总成立。总能遇到:

  • 一些解决逻辑并非无状态
  • 也并非所有的数据批改都能用一个事务爱护

尤其要留神对 [微服务] 的调用,对内存状态 的批改是没有事务爱护的,一不留神就会搞乱用户数据。比方上面代码段

6 难以排查的代码段

 try {int res1 = doStep1();
   this.status1 += res1;
   int res2 = doStep2();
   this.status2 += res2;
   // 抛个异样
   int res3 = doStep3();
   this.status3 = status1 + status2 + res3;
} catch (...) {// ...}

先假如 status1、status2、status3 之间需保护某种不变的束缚(invariant)。而后执行这段代码时,如在 doStep3 抛异样,上面对 status3 的赋值就不会执行。这时如不能将 status1、status2 的批改 rollback,就会造成数据违反束缚的问题。

而程序员很难发现这个数据被改坏了。坏数据还可能导致其余依赖这数据的代码逻辑出错(如本来应该给积分的,却没给)。而这种谬误个别很难排查,从大量数据里找到不正确的那一小段何其艰难。

7 更难搞定的代码段

// controller
void controllerMethod(/* 参数 */) {
  try {return svc.doWorkAndGetResult(/* 参数 */);
  } catch (Exception e) {return ErrorJsonObject.of(e);
  }
}

// svc
void doWorkAndGetResult(/* some params*/) {int res1 = otherSvc1.doStep1(/* some params */);
    this.status1 += res1;
    int res2 = otherSvc2.doStep2(/* some params */);
    this.status2 += res2;
    int res3 = otherSvc3.doStep3(/* some params */);
    this.status3 = status1 + status2 + res3;
    return SomeResult.of(this.status1, this.status2, this.status3);
}

难搞在于你写的时候可能认为 doStep1~3 这种货色即便抛异样也能被 Controller 里的 catch。

在 svc 这层是不必解决任何异样,因而不写 [try……catch] 理所当然。但实际上 doStep1、doStep2、doStep3 任何一个抛异样都会造成 svc 的数据状态不统一。甚至你一开始都能够通过文档或其余沟通确定 doStep1、doStep2、doStep3 一开始都是必然可胜利,不会抛错的,因而你写的代码一开始是对的。

但你可能无法控制他们的实现(如他们是另外一个团队开发的 [jar] 提供的),而他们的实现可能会改成抛错。你的代码可能在齐全不自知状况下从“不会出问题”变成“可能出问题”…… 更可怕的相似代码不能正确工作:

void doWorkAndGetResult(/* some params*/) {
    try {int res1 = otherSvc1.doStep1(/* some params */);
       this.status1 += res1;
       int res2 = otherSvc2.doStep2(/* some params */);
       this.status2 += res2;
       int res3 = otherSvc3.doStep3(/* some params */);
       this.status3 = status1 + status2 + res3;
       return SomeResult.of(this.status1, this.status2, this.status3);
   } catch (Exception e) {// do rollback}
}

你认为这样就会解决好数据 rollback,甚至 感觉这种代码优雅。但实际上 doStep1~3 每一个中央抛错,rollback 的代码都不一样。

得这么写

void doWorkAndGetResult(/* some params*/) {
    int res1, res2, res3;
    try {res1 = otherSvc1.doStep1(/* some params */);
       this.status1 += res1;
    } catch (Exception e) {throw e;}

    try {res2 = otherSvc2.doStep2(/* some params */);
      this.status2 += res2;
    } catch (Exception e) {
      // rollback status1
      this.status1 -= res1;
      throw e;
    }
  
    try {res3 = otherSvc3.doStep3(/* some params */);
      this.status3 = status1 + status2 + res3;
    } catch (Exception e) {
      // rollback status1 & status2
      this.status1 -= res1;
      this.status2 -= res2;
      throw e;
   } 
}

这才是失去正确后果的代码,在任何中央出错都能保护数据一致性。优雅吗?

看起来很丑。比 go 的 if err != nil 还丑。但要在正确性和优雅性取舍,必定毫不犹豫选前者。作为程序员不能间接认为抛异样可解决任何问题,须学会写出有正确逻辑的程序,哪怕很难且看起来丑。

为达成高正确性,你不能总将本人大部分注意力放在“所有都 OK 的流程“,而把谬误看作是可轻易应酬了事的工作或简略的置信 exception 可主动搞定所有。

8 总结

对错误处理要有敬畏之心:

  • Java 因为 Checked Exception 设计问题不得不防止应用
  • 而 Uncaughted Exception切实弱鸡,不能给程序员提供更好帮忙

因而,程序员在每次抛错或者处理错误的时候都要三省吾身:

  • 这个谬误的解决是正确吗?
  • 会让用户看到啥?
  • 会不会搞乱数据?

不要认为本人抛个异样就完事了。在 [编译器] 不能帮上太多忙时,好好写 UT 来爱护代码可怜的正确性。

请多写正确的代码

本文由博客一文多发平台 OpenWrite 公布!

退出移动版