全局异常处理机制

异常处理机制在后端开发中起着至关重要的作用。在Java后端开发中,Service层一般用来做业务逻辑层,这一层所出现的和业务无关的异常,应当往上一层抛出,最终在Controller层处理所有的异常,所以要定制一套完成的异常处理方案,针对不同的异常做不同的处理。

针对业务层的不同异常,要做各自的处理并合理返回,这里就不详细说明,主要记录一下全局异常的捕获和处理。

Spring Boot中默认对全局异常做了处理,用浏览器访问一个异常接口时,会返回一个error页面,并且把异常信息显示出来。
当用PostMan等工具进行API请求的方式访问异常的接口时,会返回一个json数据。

1
2
3
4
5
6
7
{
"timestamp": "2018-09-09T03:52:05.850+0000",
"status": 404,
"error": "Not Found",
"message": "异常信息",
"path": "/xxxPath"
}

在大多数情况下,应该为不同的设备访问方式返回相同格式的信息,这里的json格式字段可能并不是业务所需要的。
在实际业务中,当服务调用出现异常时,应该为调用者返回约定好的与业务相关的数据模型。
比如Controller层在处理捕获到的异常后可以向外抛出一个ApiException,用来表示与业务无关的全局异常。
新建ApiException类继承Exception

1
2
3
4
5
6
7
8
package com.lanshiqin.apiboot.controller.exception

/**
* API异常类
* 用来标注Controller调用的通用异常
* @author 蓝士钦
*/
class ApiException(message: String?) : Exception(message)

新建全局异常处理类 GlobalExceptionHandler

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
50
51
52
53
54
55
56
package com.lanshiqin.apiboot.controller.handler

import com.lanshiqin.apiboot.controller.exception.ApiException
import com.lanshiqin.apiboot.model.ErrorInfo
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.servlet.ModelAndView
import javax.servlet.http.HttpServletRequest

/**
* 全局异常处理
* @author 蓝士钦
*/
@ControllerAdvice
class GlobalExceptionHandler{

/**
* 统一异常处理方法
* 打印异常日志,并将错误信息输出到指定的页面
* @ httpServletRequest 请求对象
* @ exception 异常信息
*/
@ExceptionHandler(value = [Exception::class])
fun defaultExceptionHandler(httpServletRequest: HttpServletRequest, exception: Exception): ModelAndView{
logger.error("全局异常: "+exception.printStackTrace().toString())
val modelAndView = ModelAndView()
modelAndView.addObject("exception",exception)
modelAndView.addObject("url",httpServletRequest.requestURL)
modelAndView.viewName = DEFAULT_EXCEPTION_PAGE
return modelAndView
}

/**
* API异常处理方法
* @ httpServletRequest 请求对象
* @ exception 异常信息
*/
@ResponseBody
@ExceptionHandler(value = [ApiException::class])
fun apiExceptionHandler(httpServletRequest: HttpServletRequest, exception: Exception): ErrorInfo<String> {
logger.error("API异常: "+exception.printStackTrace().toString())
val errorInfo: ErrorInfo<String> = ErrorInfo()
errorInfo.code=500
errorInfo.message=exception.message
errorInfo.data="API异常"
return errorInfo

}

companion object {
const val DEFAULT_EXCEPTION_PAGE = "exception"
val logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)!!
}
}

处理异常的方法上需要添加@ExceptionHandler注解,value值是一个数组,用来声明这个方法要处理哪些异常。
当异常匹配方法时才会进入方法体执行相应的逻辑。

在resources/template下新建页面 exception.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>统一异常处理</title>
</head>
<body>
<h1>发生异常</h1>
<div th:text="${url}"></div>
<div th:text="${exception.message}"></div>
</body>
</html>

需要注意的是,SpringBoot有几个可选的视图模板引擎,虽然做了默认配置的,但是还是需要在Gradle中引用依赖,否则无法正常解析指定的模板页面

1
compile('org.springframework.boot:spring-boot-starter-thymeleaf')

在全局异常处理类中,定义了两个方法,一个用来处理未捕获的异常,并且返回html页面进行显示。在API后端接口服务开发中,应当在Controller层处理掉所有异常,所以以非json来输出这个页面一方面是用来显示错误信息,另一方面是用来说明API接口写的有问题。
客户端在API请求时如果接受到这样的html,会无法解析按照约定的json格式。这个异常页面用来提示后端开发人员应当在Controller层处理掉异常,抛出ApiException。

一个相对正确的API接口,Controller示例如下

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
/**
* 用户信息Controller
* @author 蓝士钦
*/
@RestController
class UserInfoController{

/**
* 用户信息Service
*/
@Autowired
val userInfoService: UserInfoService?=null

/**
* 添加用户信息
*/
@PutMapping("/addUserInfo")
fun addUserInfo(@RequestBody userInfo: UserInfo): Any{
return try {
userInfoService!!.saveUserInfo(userInfo)
}catch (e: Exception){
// 捕获并处理异常
// ...
// 抛出ApiException
throw ApiException(e.message)
}
}
}

Controller一定要捕获并且处理掉所有异常,然后向外返回一个约定好的处理失败的信息格式。
这里最后抛出ApiException,全局异常处理类中会拦截到这个异常并且做异常日志记录,返回异常信息给调用者。

注:业务层捕获到自己业务范围内的异常时要自己处理掉,并且根据情况终止业务,把异常状态返回给调用者告知异常。
业务层不应该处理掉不属于自己业务范围的异常,比如Exception等,而是应该往上抛出给Controller,让Controller来处理。Controller处理掉Service抛出的异常,要处理掉并且返回约定好的异常对象格式给调用者,记录全局异常日志。

0%