好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

详解Spring MVC/Boot 统一异常处理最佳实践

前言

在 web 开发中, 我们经常会需要处理各种异常, 这是一件棘手的事情, 对于很多人来说, 可能对异常处理有以下几个问题:

什么时候需要捕获(try-catch)异常, 什么时候需要抛出(throws)异常到上层. 在 dao 层捕获还是在 service 捕获, 还是在 controller 层捕获. 抛出异常后要怎么处理. 怎么返回给页面错误信息.

异常处理反例

既然谈到异常, 我们先来说一下异常处理的反例, 也是很多人容易犯的错误, 这里我们同时讲到前端处理和后端处理 :

捕获异常后只输出到控制台

前端代码

?

1

2

3

4

5

6

7

8

$.ajax({

   type: "get" ,

   url: "/user/add" ,

   datatype: "json" ,

   success: function(data){

     alert( "添加成功" );

   }

});

后端代码

?

1

2

3

4

5

try {

   // do something

} catch (exception e) {

   e.printstacktrace();

}

这是见过最多的异常处理方式了, 如果这是一个添加商品的方法, 前台通过 ajax 发送请求到后端, 期望返回 json 信息表示添加结果. 但如果这段代码出现了异常:

那么用户看到的场景就是点击了添加按钮, 但没有任何反应(其实是返回了 500 错误页面, 但这里前端没有监听 error 事件, 只监听了 success 事件. 但即使加上了error: function(data) {alert("添加失败");}) 又如何呢? 到底因为啥失败了呢, 用户也不得而知. 后台 e.printstacktrace() 打印在控制台的日志也会在漫漫的日志中被埋没, 很可能会看不到输出的异常. 但这并不是最糟的情况, 更糟糕的事情是连 e.printstacktrace() 都没有, catch 块中是空的, 这样后端的控制台中更是什么都看不到了, 这段代码会像一个隐形的炸弹一样一直埋伏在系统中.

混乱的返回方式

前端代码

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

$.ajax({

   type: "get" ,

   url: "/goods/add" ,

   datatype: "json" ,

   success: function(data) {

     if (data.flag) {

       alert( "添加成功" );

     } else {

       alert(data.message);

     }

   },

   error: function(data){

     alert( "添加失败" );

   }

});

后端代码

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@requestmapping ( "/goods/add" )

@responsebody

public map add(goods goods) {

   map map = new hashmap();

   try {

     // do something

     map.put(flag, true );

   } catch (exception e) {

     e.printstacktrace();

     map.put( "flag" , false );

     map.put( "message" , e.getmessage());

   }

   reutrn map;

}

这种方式捕获异常后, 返回了错误信息, 且前台做了一定的处理, 看起来很完善? 但用 hashmap 中的 flag 和 message 这种字符串来当键很容易处理, 例如你这里叫 message, 别人起名叫 msg, 甚至有时手抖打错了, 怎么办? 前台再改成 msg 或其他的字符?, 前端后端这样一直来回改?

更有甚者在情况 a 的情况下, 返回 json, 在情况 b 的情况下, 重定向到某个页面, 这就更乱了. 对于这种不统一的结构处理起来非常麻烦.

异常处理规范

既然要进行 统一异常处理 , 那么肯定要有一个规范, 不能乱来. 这个规范包含前端和后端.

不要捕获任何异常

对的, 不要在业务代码中进行捕获异常, 即 dao、service、controller 层的所以异常都全部抛出到上层. 这样不会导致业务代码中的一堆 try-catch 会混乱业务代码.

统一返回结果集

不要使用 map 来返回结果, map 不易控制且容易犯错, 应该定义一个 java 实体类. 来表示统一结果来返回, 如定义实体类:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public class resultbean<t> {

   private int code;

   private string message;

   private collection<t> data;

 

   private resultbean() {

 

   }

 

   public static resultbean error( int code, string message) {

     resultbean resultbean = new resultbean();

     resultbean.setcode(code);

     resultbean.setmessage(message);

     return resultbean;

   }

 

   public static resultbean success() {

     resultbean resultbean = new resultbean();

     resultbean.setcode( 0 );

     resultbean.setmessage( "success" );

     return resultbean;

   }

 

   public static <v> resultbean<v> success(collection<v> data) {

     resultbean resultbean = new resultbean();

     resultbean.setcode( 0 );

     resultbean.setmessage( "success" );

     resultbean.setdata(data);

     return resultbean;

   }

 

   // getter / setter 略

}

正常情况: 调用 resultbean.success() 或 resultbean.success(collection<v> data), 不需要返回数据, 即调用前者, 需要返回数据, 调用后者. 如:

?

1

2

3

4

5

6

@requestmapping ( "/goods/add" )

@responsebody

public resultbean<goods> getallgoods() {

   list<goods> goods = goodsservice.findall();

   return resultbean.success(goods);

}

?

1

2

3

4

5

6

@requestmapping ( "/goods/update" )

@responsebody

public resultbean updategoods(goods goods) {

   goodsservice.update(goods);

   return resultbean.success();

}

一般只有查询方法需要调用 resultbean.success(collection<v> data) 来返回 n 条数据, 其他诸如删除, 修改等方法都应该调用 resultbean.success(), 即在业务代码中只处理正确的功能, 不对异常做任何判断. 也不需要对 update 或 delete 的更新条数做判断(个人建议, 实际需要根据业务). 只要没有抛出异常, 我们就认为用户操作成功了. 且操作成功的提示信息在前端处理, 不要后台返回 [操作成功] 等字段.

前台接受到的信息为:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

{

   "code" : 0 ,

   "message" : "success" ,

   "data" : [

     {

       "name" : "商品1" ,

       "price" : 50.00 ,

     },

     {

       "name" : "商品2" ,

       "price" : 99.99 ,

     }

   ]

}

抛出异常: 抛出异常后, 我们应该调用 resultbean.error(int code, string message), 来将状态码和错误信息返回, 我们约定 code 为 0 表示操作成功, 1 或 2 等正数表示用户输入错误, -1, -2 等负数表示系统错误.
前台接受到的信息为:

?

1

2

3

4

5

{

   "code" : - 1 ,

   "message" : "xxx 参数有问题, 请重新填写" ,

   "data" : null

}

前端统一处理:

返回的结果集规范后, 前端就很好处理了:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

/**

  * 显示错误信息

  * @param result: 错误信息

  */

function showerror(s) {

   alert(s);

}

 

/**

  * 处理 ajax 请求结果

  * @param result: ajax 返回的结果

  * @param fn: 成功的处理函数 ( 传入data: fn(result.data) )

  */

function handlerresult(result, fn) {

   // 成功执行操作,失败提示原因

   if (result.code == 0 ) {

     fn(result.data);

   }

   // 用户操作异常, 这里可以对 1 或 2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定.

   else if (result.code == 1 ) {

     showerror(result.message);

   }

   // 系统异常, 这里可以对 -1 或 -2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定.

   else if (result.code == - 1 ) {

     showerror(result.message);

   }

   // 如果进行细粒度的状态码判断, 那么就应该重点注意这里没出现过的状态码. 这个判断仅建议在开发阶段保留用来发现未定义的状态码.

   else {

     showerror( "出现未定义的状态码:" + result.code);

   }

}

 

/**

  * 根据 id 删除商品

  */

function deletegoods(id) {

   $.ajax({

     type: "get" ,

     url: "/goods/delete" ,

     datatype: "json" ,

     success: function(result){

       handlerresult(result, deletedone);

     }

   });

}

 

function deletedone(data) {

   alert( "删除成功" );

}

showerror 和 handlerresult 是公共方法, 分别用来显示错误和统一处理结果集.

然后将主要精力放在发送请求和处理正确结果的方法上即可, 如这里的 deletedone 函数, 用来处理操作成功给用户的提示信息, 正所谓各司其职, 前端负责操作成功的消息提示更合理, 而错误信息只有后台知道, 所以需要后台来返回.

后端统一处理异常

说了这么多, 还没讲到后端不在业务层捕获任何异常的事, 既然所有业务层都没有捕获异常, 那么所有的异常都会抛出到 controller 层, 我们只需要用 aop 对 controller 层的所有方法处理即可.

好在 spring 为我们提供了一个注解, 用来统一处理异常:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

@controlleradvice

@responsebody

public class webexceptionhandler {

 

   private static final logger log = loggerfactory.getlogger(webexceptionhandler. class );

 

   @exceptionhandler

   public resultbean unknownaccount(unknownaccountexception e) {

     log.error( "账号不存在" , e);

     return resultbean.error( 1 , "账号不存在" );

   }

 

   @exceptionhandler

   public resultbean incorrectcredentials(incorrectcredentialsexception e) {

     log.error( "密码错误" , e);

     return resultbean.error(- 2 , "密码错误" );

   }

 

   @exceptionhandler

   public resultbean unknownexception(exception e) {

     log.error( "发生了未知异常" , e);

     // 发送邮件通知技术人员.

     return resultbean.error(- 99 , "系统出现错误, 请联系网站管理员!" );

   }

}

在这里统一配置需要处理的异常, 同样, 对于未知的异常, 一定要及时发现, 并进行处理. 推荐出现未知异常后发送邮件, 提示技术人员.

总结

总结一下统一异常处理的方法:

不使用随意返回各种数据类型, 要统一返回值规范. 不在业务代码中捕获任何异常, 全部交由 @controlleradvice 来处理.

一个简单的演示项目:   https://github测试数据/zhaojun1998/exception-handler-demo

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

原文链接:http://HdhCmsTestzhaojun.im/springboot-exception/

查看更多关于详解Spring MVC/Boot 统一异常处理最佳实践的详细内容...

  阅读:11次