【实战总结】大型javaweb项目中的统一异常设计与处理

一、背景

对于异常处理在我们日常开发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的异常:除数为0的异常

  • 源码地址:
    https://gitee.com/xiangguangming/case-exception-handling.git

四、总结

本章暂未实现内容如下,有兴趣的小伙伴可以拉取代码实践验证。
1.基于断言类代替非空判断逻辑抛出异常
2.前后端接口调试,后端校验可以基于JSR303校验,在异常中添加统一异常处理返回。
3.微服务环境下使用skywalking采集日志,生成全局的traceId,上面filter生成为单机的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值