springboot对控制层全局异常的处理

本文将从@controllerAdvice和@ExceptionHandler两个注解到自定义HttpStatus来对控制层的异常进行优雅的处理。

说起惭愧,在前不久,我还悻悻然在控制层通过频繁的try catch语句来进行异常处理及对应model返回,这么做当然是没什么错,但频繁写一样的代码属实让人感觉厌烦,于是开始找寻一种可以对异常进行统一异常处理的方式。

本文意在做一套系统性相关整理笔记,方便以后查看。手懒,延迟了个把月。

几种处理方式

HandlerExceptionResolver接口

1.实现HandlerExceptionResolver接口,将实现类作为Spring Bean,这样Spring就能扫描到它并作为全局异常处理器加载

2.在resolveException中实现异常处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Slf4j
public class CustomHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
Method method = null;
if (handler != null && handler instanceof HandlerMethod) {
method = ((HandlerMethod) handler).getMethod();
}
log.error("[{}] system error", method, ex);
ResponseDTO response = ResponseDTO.builder()
.errorCode(ErrorCode.SYSTEM_ERROR)
.build();
byte[] bytes = JSON.toJSONString(response).getBytes(StandardCharsets.UTF_8));
try {
FileCopyUtils.copy(bytes, response.getOutputStream());
} catch (IOException e) {
log.error("error", e);
throw new RuntimeException(e);
}
return new ModelAndView();
}
}

从参数上,可以看到,不仅能够拿到发生异常的函数和异常对象,还能够拿到HttpServletResponse对象,从而控制本次请求返回给前端的行为。

此外,函数还可以返回一个ModelAndView对象,表示渲染一个视图,比方说错误页面,不过,在前后端分离为主流架构的今天,这个很少用了。如果函数返回的视图为空,则表示不需要视图。

@ExceptionHandler局部处理

通过@ExceptionHandler注解可以对当前控制层进行异常处理,当前控制层抛出的异常都会通过该方法来进行返回,这里是一个简单的返回模板。

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
@Slf4j
@RestController
@RequestMapping(value = "/api/test")
public class MyTestController extends BaseController {

@Autowired
private DiscoveryClient client;
@Autowired
private Registration registration;

@GetMapping(value = "/addValue")
public Integer adValue(@RequestParam("a") Integer a,@RequestParam("b") Integer b){
Integer r = a + b;
ServiceInstance instance = serviceInstance();
String result = new StringBuffer().append("/add: Host:")
.append(instance.getHost())
.append(", service_id:")
.append(instance.getServiceId())
.append(" result:")
.append(a + b).toString();
log.info(result);
return r;
}

//获取当前服务实例
public ServiceInstance serviceInstance(){
List<ServiceInstance> instances = client.getInstances(registration.getServiceId());
if (!instances.isEmpty() && instances.size() > 0) {
for (ServiceInstance instance : instances) {
if (instance.getPort() == 7001) {
return instance;
}
}
}
return null;
}

@ExceptionHandler(Exception.class)
public ResponseEntity exceptionHandler(Exception e)
{
log.error("[{}] system error", e);
return error("出错了!");
}

}

可以通过浏览器来访问一番,比如把参数b写为c,看结果:

java-exception

但是这种方法还是存在局促性,我们需要的是对全局controller进行一个异常处理,那么这个时候就需要通过@ControllerAdvice和@ExceptionHandler来结合使用

通过@ControllerAdvice和@ExceptionHandler来结合使用

两者结合之后可以对所有控制层就行异常控制,并且可以细粒到具体异常,当然,你也可以定义自定义异常,或者在@ExceptionHandler中放置多个异常,等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ParmException.class)
ResponseEntity parmExceptionHandler(Exception e)
{
return new ResponseEntity(false, RetCode.PARM_EXCEPTION,e.getMessage(),null);
}

@ExceptionHandler(JSONException.class)
ResponseEntity jsonExceptionHandler(Exception e)
{
return new ResponseEntity(false, RetCode.JSON_EXCEPTION,e.getMessage(),null);
}

}

看起来是比较方便简单的,其中ParmException为自定义异常,在方法参数内可以通过Exception来接收并获取到异常信息

容器中通过HttpStatus定义异常路径,结合两注解

通过@ControllerAdvice和@ExceptionHandler的确可以对所有controller层进行相对不错的异常控制,但有没有想过这种场景,比如请求404,405,400… 这种异常最好还是通过本系统来进行自定义处理,这样才能提高用户体验。

那么如何设置呢?上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class CustomErrorServeletFactory implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {

//custom error request path
@Override
public void customize(ConfigurableWebServerFactory factory) {
for (HttpStatus e : HttpStatus.values()) {
if (e.value() == HttpStatus.NOT_FOUND.value()) {
factory.addErrorPages(new ErrorPage(e,"/404"));
}else if (e.value() == HttpStatus.METHOD_NOT_ALLOWED.value()) {
factory.addErrorPages(new ErrorPage(e,"/405"));
}else if (e.value() != HttpStatus.OK.value()) {
factory.addErrorPages(new ErrorPage(e,"/500"));
}
}
}
}

首先通过实现WebServerFactoryCustomizer接口中的customize方法来对容器工厂进行配置,可以看到,这里这里我对404,405,500状态码的定义,并设置了一个他们对应的路径,接下来可以自定义这几个路径。

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
@RestController
@Slf4j
public class FlagController extends BaseController {

@Value("${my.git.version:尚未配置}")
private String gitVersion;
@Value("${my.project.buildtime:尚未配置}")
private String buildTime;

@RequestMapping(value = {"/", "/version"})
Object version(){
Map<String, Object> map = new HashMap<>();
map.put("Build_Time",buildTime);
map.put("Build_GIT_Version",gitVersion);
return map;
}

@RequestMapping(value = "404")
public ResponseEntity notFound(){
int code = response.getStatus();
response.setStatus(HttpStatus.OK.value());
return error(RetCode.RET_ERROR,code + "Not Found!");
}

@RequestMapping(value = "405")
public ResponseEntity notAllowed(){
int code = response.getStatus();
response.setStatus(HttpStatus.OK.value());
return error(RetCode.RET_ERROR,code + "Not Allowed!");
}

@RequestMapping(value = "500")
public ResponseEntity apiError(HttpServletRequest request){
int code = response.getStatus();
response.setStatus(HttpStatus.OK.value());
//get exception by request
Exception e = (Exception) request.getAttribute("javax.servlet.error.exception");
String errorMsg = "";
if (e != null) {
errorMsg = e.getMessage();
}
log.error("apiError: {}",errorMsg);
return error(RetCode.RET_ERROR,code + errorMsg);
}
}

总结

个人认为,任何能够给Controller加切面的机制都能变相的进行统一异常处理。比如:

  • 在拦截器内捕获Controller的异常,做统一异常处理。

  • 使用Spring的AOP机制,做统一异常处理。

有没有感觉这种方式让你一看之后感觉可行呢,或许你有更好的处理方式可以通过下方评论区告诉我,不胜感激。