Service 层异常抛到 Controller 层处理还是直接处理?

这篇具有很好参考价值的文章主要介绍了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 发布!文章来源地址https://www.toymoban.com/news/detail-712236.html

到了这里,关于Service 层异常抛到 Controller 层处理还是直接处理?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Spring Boot 单元测试(Controller测试与Service测试)

    🎈博客主页:🌈我的主页🌈 🎈欢迎点赞 👍 收藏 🌟留言 📝 欢迎讨论!👏 🎈本文由 【泠青沼~】 原创,首发于 CSDN🚩🚩🚩 🎈由于博主是在学小白一枚,难免会有错误,有任何问题欢迎评论区留言指出,感激不尽!🌠个人主页 @SpringBootTest相当于springMvc中单元测试中的

    2023年04月26日
    浏览(50)
  • Java中 Controller、Service、Dao/Mapper层的区别与用法

    在Java开发中,通常会采用三层架构(或称MVC架构)来划分程序的职责和功能,分别是Controller层、Service层、Dao/Mapper层。 业务模块的逻辑功能设计,和DAO层一样都是先设计接口,再创建要实现的类,然后在配置文件中进行配置其实现的关联。接下来就可以在service层调用接口进

    2024年02月06日
    浏览(50)
  • Mock单元测试----对Controller层进行单独测试,不调用Service层

    前言:根据相关需求,需要对编写的代码进行逻辑检测以及功能的完整性,从而开始了单元测试之路。在编写的中间段时,突然被 不经过Service层直接测试Controller层 这个要求难住了。在我看来,单元测试除了Junit还是Junit,属实是学艺不精,之后接触了Mock,才发现Mock太牛逼了

    2024年02月05日
    浏览(42)
  • 基于Junit4+Mockito+PowerMock实现Controller+Service的单元测试

    一 导入的依赖 二 依赖版本 三 controller测试示例代码       controller         controllerTest         测试结果:覆盖率100%         带异常的Controller         带异常提示的ControllerTest         测试结果,覆盖率100%   三 service测试示例代码         service         serviceTest    

    2024年02月14日
    浏览(44)
  • SpringBoot(三层框架Controller,Mapper,Service)中遇到的一些注解整理

    本文主要从Controller层,Service层,Mapper层这三层架构中记录用到的各种注解 还有一些MyBatis用到的注解 持续更新到本人的毕设做完为止,太多了太多了根本学不完哈哈哈 用于建立HTTP请求与处理方法之间的映射关系,其中 XXX Mapping限定了提交http请求的方法 用于获取URL中提交过来的

    2024年01月21日
    浏览(43)
  • IDEA直接请求controller,不用postman请求http接口

    generated-requests.http工具用法 第一步:点击下面按钮,HTTP Client  第二步、生成generated-requests.http文件  第三步、更改服务的ip和端口,启动服务  请求实例: 1、post请求,body传参: 2、get请求 2.1 2.2

    2024年02月15日
    浏览(53)
  • SpringBoot框架分层(View层、Controller层、Service层、Mapper层、pojo层)

      SpringBoot框架一般分为View层、Controller层、Service层、Mapper层、pojo层。 View层:视图层,根据接到的数据展示页面给用户 Controller层:响应用户需求,决定用什么视图,需要准备什么数据来显示。Controller层负责前后端交互,接收前端请求,调用Service层,接收Service层返回的数据

    2024年02月07日
    浏览(53)
  • spring boot,DAO层、ENTITY层、SERVICE层、CONTROLLER层之间的关系

    主要用于 定义与数据库对象应的属性,提供get/set方法 ,tostring方法,有参无参构造函数。 DAO层 首先会创建Dao接口 , 接着就可以在配置文件中定义该接口的实现类 ;接着就可以在模块中调用Dao的接口进行数据业务的处理,而不用关注此接口的具体实现类是哪一个类,Dao层的数

    2024年04月10日
    浏览(37)
  • java中的controller、domain、mapper(persistence)、service 都是做什么用的?

    java中的controller、domain、mapper(persistence)、service代表了服务端接口的 4 层,第一层是控制层(controller),负责接口请求/响应的控制,调用第二层业务逻辑层(service 一般分为接口和实现),完成具体业务功能,它会调用第三层数据持久层 mapper(persistence)的逻辑,作用是访

    2024年02月15日
    浏览(49)
  • 后端开发基础概念 Entity,DAO,DO,DTO,VO, Service,Controller

    Entity主要用于ORM(对象关系映射)框架中,如Hibernate、MyBatis等,以便将数据库中的数据映射为对象,方便进行业务操作。 Entity通常与数据库表一一对应,代表 业务数据 的基本单元。 通常放在项目的model或entity包下。   DAO(数据访问对象): DAO是连接业务逻辑和数据库的桥

    2024年04月08日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包