[Java实战经验]异常处理最佳实践

一些好的异常处理实践。

目录

异常设计

自定义异常

自定义异常设计,如业务异常定义BusinessException,设置一个基础异常类,如XXAppBaseException(或就叫BaseException),然后让各类异常继承,如下:

java 复制代码
public class UserException extends XXAppBaseException { ... }

public class MapException extends XXAppBaseException { ... }

这里异常的划分可以按照模块、业务来区分,也可以分离业务代码异常与技术代码异常。

为异常设计错误代码(状态码)

常见的异常代码设计有HTTP的异常状态码,如404、500、502这种。

这样做主要是便于日志分析和客户端处理,很明显,使用错误代码做筛选能提升检索效率、方便收集、自动化处理,且使用异常状态码来传输异常信息提升了信息传输与存储效率。

等等......

设计粒度

自定义异常和异常错误代码都是比较常见的操作,但是设计时需要考虑粒度。

一般有层级关系的设计更便于理解、维护。

在自定义异常中就是多层继承关系,在异常错误码中就是分层错误码设计,如全局错误码 > 模块错误码 > 具体错误码

复制代码
5xx--->5xxx->5xxxx

全局异常处理

使用Spring的@ControllerAdvice或类似机制统一处理异常。如:

java 复制代码
/**
 * 全局异常处理器
 * 
 * @author ruoyi
 */
@RestControllerAdvice
public class GlobalExceptionHandler
{
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 权限校验异常(ajax请求返回json,redirect请求跳转页面)
     */
    @ExceptionHandler(AuthorizationException.class)
    public Object handleAuthorizationException(AuthorizationException e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
        if (ServletUtils.isAjaxRequest(request))
        {
            return AjaxResult.error(PermissionUtils.getMsg(e.getMessage()));
        }
        else
        {
            return new ModelAndView("error/unauth");
        }
    }

    /**
     * 请求方式不支持
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
            HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 拦截未知的运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生未知异常.", requestURI, e);
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生系统异常.", requestURI, e);
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 业务异常
     */
    @ExceptionHandler(ServiceException.class)
    public Object handleServiceException(ServiceException e, HttpServletRequest request)
    {
        log.error(e.getMessage(), e);
        if (ServletUtils.isAjaxRequest(request))
        {
            return AjaxResult.error(e.getMessage());
        }
        else
        {
            return new ModelAndView("error/service", "errorMessage", e.getMessage());
        }
    }
}

异常日志信息保留

在抛出异常时,建议连带当前业务标识信息一起抛出(每一层日志抛出记录标识),这样方便排查问题。

异常处理时机

偶尔能看到一类将所有代码块都包起来的try-catch异常处理,这种代码被称为"防御式编程"的过度使用,会导致多种问题。

  1. 代码可读性降低:大量的异常处理代码掩盖了业务逻辑
  2. 异常信息丢失:每层都捕获并重新抛出,可能丢失原始堆栈信息
  3. 难以维护:当需要修改异常处理逻辑时,需要修改多处代码
  4. 性能影响:过多的try-catch会对性能产生轻微影响

异常处理应该在能够正确响应的层级进行,如:

  1. 边界层处理:如API层、用户界面层、外部系统集成点
  2. 业务决策点处理:在能够做出恢复决策的地方处理异常
  3. 资源管理点处理:在使用需要清理的资源的地方处理
java 复制代码
// 不好的做法
public void badExceptionHandling() {
    try {
        // 获取用户
        User user = null;
        try {
            user = userRepository.findById(userId);
        } catch (Exception e) {
            log.error("Failed to get user", e);
        }
        
        // 获取订单
        Order order = null;
        try {
            order = orderRepository.findById(orderId);
        } catch (Exception e) {
            log.error("Failed to get order", e);
        }
        
        // 处理业务逻辑
        try {
            processOrder(user, order);
        } catch (Exception e) {
            log.error("Failed to process order", e);
        }
    } catch (Exception e) {
        log.error("Unexpected error", e);
    }
}
java 复制代码
// 好的做法
public void goodExceptionHandling() {
    try {
        User user = userRepository.findById(userId);
        Order order = orderRepository.findById(orderId);
        processOrder(user, order);
    } catch (UserNotFoundException e) {
        // 有针对性地处理用户不存在的情况
        log.warn("Order processing failed: User not found", e);
        notifyAdministrator(e);
    } catch (OrderNotFoundException e) {
        // 有针对性地处理订单不存在的情况
        log.warn("Order processing failed: Order not found", e);
        notifyCustomer(userId, e);
    } catch (BusinessException e) {
        // 处理所有业务异常
        log.warn("Business rule violation during order processing", e);
        // 可能的补救措施
    } catch (Exception e) {
        // 处理所有其他未预期的异常
        log.error("Unexpected error during order processing", e);
        // 紧急措施
    }
}

资源管理

try-with-resources

使用try-with-resources自动关闭资源,防止泄露。为自己的资源类实现AutoCloseable接口,如:

java 复制代码
public class ResourceLock implements AutoCloseable {
    // 获取资源
    public ResourceLock() { /* 获取锁或资源 */ }
    
    @Override
    public void close() { /* 释放锁或资源 */ }
}

使用finally和这个一样。常规操作。

异常中的事务

在Spring框架中,默认情况运行时异常与严重问题会导致事务回滚,检查型异常不会。

  • 运行时异常(unchecked) :继承自RuntimeException的异常,默认导致事务回滚
  • 检查型异常(checked) :继承自Exception但不是RuntimeException的子类,默认不会导致事务回滚
  • Error :严重问题,如OutOfMemoryError,默认导致事务回滚

因此,我们需要注意异常对事务的影响。

相关推荐
invicinble1 小时前
jar包在执行的时候需要关注的细节(提供一个解构jvm问题的视角)
java·jvm·jar
麦芽糖02191 小时前
SSE介绍及使用(Server-Send Events)
java
alan07212 小时前
【Java + Elasticsearch全量 & 增量同步实战】
java·elasticsearch·jenkins
hashiqimiya2 小时前
后端springboot的接收前端发来的数据反序列化原理
java
cat三三2 小时前
java之异常
java·开发语言
浙江第二深情2 小时前
前端性能优化终极指南
java·maven
养乐多07223 小时前
【Java】IO流
java
俊男无期3 小时前
超效率工作法
java·前端·数据库
中国胖子风清扬3 小时前
SpringAI和 Langchain4j等 AI 框架之间的差异和开发经验
java·数据库·人工智能·spring boot·spring cloud·ai·langchain
月明长歌3 小时前
【码道初阶】牛客TSINGK110:二叉树遍历(较难)如何根据“扩展先序遍历”构建二叉树?
java·数据结构·算法