一、背景
对于异常处理在我们日常开发javaweb项目中犹如家常便饭,相信谁也不会陌生。但是有时候前后端对接和API联调时由于接口异常就导致返回模型不统一,从而导致问题定位效率变低和解决的时间变长,为了提了工作效率和快速定位问题个人总结在项目中异常处理的实战经验。
二、方案设计
本案例介绍在项目实战过程中如何定义统一的异常和处理异常以及在filter运行时的异常如何统一处理。
-
核心技术点:
Springmvc项目@RestControllerAdvice,@ExceptionHandler
@RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。
@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常 -
异常设计:
设计一个异常定义的模板,
设计一个通过异常编码解析异常信息的工具包,
设计一个自定义项目异常类在项目统一使用流通
- 异常处理:
基于SpringMvc框架特性定义一个自定义的项目异常处理器,组装自定义的异常信息返回的结果视图
基于SpringMvc框架特性定义一个全局异常处理器,组装未知的异常信息返回的结果视图
- 定义初始化traceLog的filter
通过UUID生成一个TraceId存储在线程名称中,在发生未知异常时可以借助TraceId快速定位未知,在微服务中web容器也是很好输入参考。
- filter中的异常处理:
设计一个ExceptionController统一接收处理filter中抛出的异常, 流程为:filter–>转发ExceptionController–>异常处理器
三、代码实现
- 核心代码:
//异常信息模板
##APP_100001=APP_100001|304|zh=中语;en=英语;ph=菲律宾语. PS:国际化内容中不能出现;
SYS_100001=SYS_100001|500|zh=发生未知异常,异常是:{0};en=service is error.exception:{1}
APP_100001=APP_100001|404|zh=用户信息不存在;en=User is not exists.
APP_100002=APP_100002|409|zh=用户信息内部更新异常;en=User information is updated abnormally.
// 自定义异常类
public class ApplicationException extends RuntimeException {
private String errorCode;
private Object[] args;
public ApplicationException(){
super();
}
public ApplicationException(String errorCode){
super(errorCode);
this.errorCode = errorCode;
}
public ApplicationException(String errorCode, Object... args){
super(errorCode);
this.errorCode = errorCode;
this.args = args;
}
public ApplicationException(Throwable throwable){
super(throwable);
this.errorCode = throwable.getMessage();
}
public ApplicationException(String errorCode, Throwable throwable){
super(throwable);
this.errorCode = errorCode;
}
public String getErrorMessage(){
return getErrorMessage(ExceptionUtils.getLang());
}
public String getErrorMessage(String lang){
if(!StringUtils.hasLength(errorCode)){
return getMessage();
}
return ExceptionUtils.getMessage(errorCode, args, lang);
}
}
// 异常使用
@RestController
@RequestMapping("/api/tests")
public class TestController {
@GetMapping("/test1")
public void test1(){
throw new ApplicationException("APP_100001");
}
@GetMapping("/test2")
public void test2(){
throw new NullPointerException();
}
@GetMapping("/test3")
public void test3(){
int num = 1/0;
}
}
// 异常处理
@Slf4j
@RestControllerAdvice
public class ApplicationExceptionHandler {
@ExceptionHandler(ApplicationException.class)
public ResponseEntity<Object> handle(final ApplicationException exception){
log.error("ApplicationExceptionHandler handle exception={}",exception.getErrorMessage());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ApiExceptionResult data = ApiExceptionResult.of(exception);
return new ResponseEntity<>(ApiResult.fail(data), headers, HttpStatus.resolve(data.getHttpCode()) );
}
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handle(final Exception exception){
// 异常为空打印堆栈信息
if(StringUtils.hasLength(exception.getMessage())){
log.error("GlobalExceptionHandler exception={}",exception.getMessage());
}else{
log.error("GlobalExceptionHandler exception.",exception);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
try{
ApiExceptionResult data = ApiExceptionResult.of("SYS_100001",exception.getMessage());
return new ResponseEntity<>(ApiResult.fail(data), headers, HttpStatus.resolve(data.getHttpCode()) );
}catch (Exception exception1){
log.error("cast ApiExceptionResult error. exception1={}",exception1.getMessage());
}
return new ResponseEntity<>(ApiResult.fail(exception.getMessage()), headers, HttpStatus.INTERNAL_SERVER_ERROR );
}
}
// 跟踪日志traceId初始化
@Slf4j
public class TraceLogFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {
initTraceId(servletRequest);
}catch (RuntimeException exception){
log.error("TraceLogFilter initTraceId error. exception={}",exception);
}
filterChain.doFilter(servletRequest,servletResponse);
}
private void initTraceId(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
// traceId模板 主机名称|线程名称|traceId
String template = "WebContainer:{0}|[Thread]{1}|[TraceId]{2}";
String traceIdStr = MessageFormat.format(template, request.getLocalAddr(), Thread.currentThread().getName(), UUID.randomUUID().toString());
Thread.currentThread().setName(traceIdStr);
}
}
// filer中的异常处理
@Slf4j
@Component
public class TestExceptionFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest request = (HttpServletRequest)servletRequest;
if(request.getRequestURI().contains("test4")){
// 测试空指针异常
//test1();
// 测试除数0异常
//test2();
// 测试业务校验
test3();
}
filterChain.doFilter(servletRequest,servletResponse);
}catch (RuntimeException exception){
log.error("TestExceptionFilter doFilter error. ex=",exception.getMessage());
// 异常转发到控制器处理
servletRequest.setAttribute(ExceptionUtils.RETHROW_EXCEPTION,exception);
servletRequest.getRequestDispatcher(ExceptionUtils.RETHROW_EXCEPTION_API).forward(servletRequest,servletResponse);
}
}
// 测试空指针异常
private void test1(){
throw new NullPointerException();
}
// 测试除数0异常
private void test2(){
int num = 1/0;
}
// 测试业务校验
private void test3(){
throw new ApplicationException("APP_100002");
}
}
// 异常重新抛出
@RestController
public class ExceptionController {
@RequestMapping(ExceptionUtils.RETHROW_EXCEPTION_API)
public void throwException(HttpServletRequest request){
Object filterException = request.getAttribute(ExceptionUtils.RETHROW_EXCEPTION);
if(filterException instanceof ApplicationException){
throw (ApplicationException)filterException;
}else{
throw (RuntimeException)filterException;
}
}
}
-
测试案例:
业务校验异常:
未知异常空指针处理:
除数为0的异常: -
源码地址:
https://gitee.com/xiangguangming/case-exception-handling.git
四、总结
本章暂未实现内容如下,有兴趣的小伙伴可以拉取代码实践验证。
1.基于断言类代替非空判断逻辑抛出异常
2.前后端接口调试,后端校验可以基于JSR303校验,在异常中添加统一异常处理返回。
3.微服务环境下使用skywalking采集日志,生成全局的traceId,上面filter生成为单机的。