优化用户体验:SpringBoot统一异常处理最佳实践

简介: 优化用户体验:SpringBoot统一异常处理最佳实践

为什么要有统一异常处理?

现在开发的绝大多数JavaWeb项目都是采用了“前后端分离”的这种形式,前后端通过接口来进行JSON数据的交互。那么首先大家先看看下面的这两张图:

A接口是请求成功的样子,B接口虽然显示的是请求失败,但是也还是把请求的接口状态返回了。然后前端就可以根据这个状态去进行相关逻辑操作,比如成功了话刷新页面,失败了跳转到错误页面或者弹框警告。这样其实可以很大程度的提高用户的体验。

但是!有的时候后端在进行业务处理的时候会报错!比如人尽皆知的NullPointerException,当后端报了空指针异常时,在没有进行统一异常处理的时候,接口的返回就变成了这个样子:

这是啥玩意,就告诉我服务器出错了?来个status=500就没了?一点也不友好!连后端到底报啥错也没有,而且这个根本也不是JSON的格式,说好了前后端通过接口来进行JSON数据的交互呢?前端都拿不到JSON,所以也就没法做业务判断、没法做错误页面跳转、没法做警告弹框。

所以,为了让后端即使出错了,也可以进行正常的JSON数据的返回,顶多就是在JSON里声明后端报错了,然后再把报的什么错告诉前端,然后前端就可以继续做业务处理了,你看,这种多人性化!所以,我们就需要对后端框架的异常报错进行全局的捕获处理。具体怎么做那就往下看吧~

注解@ControllerAdvice

SpringBoot中有一个@ControllerAdvice的注解,使用该注解即表示开启全局异常捕获,接下来我们只需在自定义的方法上使用@ExceptionHandler注解,并定义捕获异常的类型,那么这个自定义的方法体就是对这种类型的异常进行统一的处理。

同时还提供@RestControllerAdvice注解,就相当于@RestController

  • @RestController = @Controller + @ResponseBody
  • @RestControllerAdvice = @ControllerAdvice + @ResponseBody
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 捕获全局异常,处理所有不可知的异常
     */
    @ExceptionHandler(value=Exception.class)
    public ResponseResult handleException(Exception e, HttpServletRequest request) {
        log.error("出现未知异常 -> ", e);
        ResponseError error = new ResponseError();
        error.setCode("APPEAR_ERROR");
        error.setMessage(e.getMessage());
        error.setRequestUrl(request.getRequestURL().toString());
        error.setException(e.getClass().getName());
        return error;
    }
    /**
     * 捕获空指针异常
     */
    @ExceptionHandler(value=NullPointerException.class)
    public ResponseResult handleNullPointerException(NullPointerException e, HttpServletRequest request) {
        log.error("出现空指针异常 -> ", e);
        ResponseError error = new ResponseError();
        error.setCode("NULL_POINTER_ERROR");
        error.setMessage("出现空指针异常");
        error.setRequestUrl(request.getRequestURL().toString());
        error.setException(e.getClass().getName());
        return error;
    }
}

这里要注意一下,统一异常处理会优先处理子类异常,也就是说当出现了空指针异常,那么会优先交给handleNullPointerException这个方法处理,如果没有明确的针对某种异常进行处理,那么handleException这个方法会自觉的处理它们。

先看效果

加上统一异常处理之后,接口的返回就变得十分优雅了~

瞅见了吧,现在后端无论是怎么报错,返回给前端的永远是JSON数据,之后前端就可以该跳转错误页面的跳转,该弹框警告的弹框!统一异常处理,永远的神!

完整项目代码可前往公众号【爱学习的莫提】,后台回复【统一异常处理】获取。

具体实现

目录结构

引入POM依赖

<dependencies>
        <!--引入Web启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入日志启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!--引入Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--引入fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <!--引入日志依赖-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <!--引入Hutools工具-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

创建接口返回JSON格式

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult {
    /**
     * 响应编码
     */
    protected String code;
    /**
     * 响应信息
     */
    protected String message;
    /**
     * 响应数据
     */
    protected Object data;
    /**
     * 请求成功
     */
    public static ResponseResult success(){
        return ResponseResult.builder().code("SUCCESS").message("请求成功").build();
    }
    /**
     * 请求成功
     */
    public static ResponseResult success(String message){
        return ResponseResult.builder().code("SUCCESS").message(message).build();
    }
    /**
     * 请求成功
     */
    public static ResponseResult success(String message, Object data){
        return ResponseResult.builder().code("SUCCESS").message(message).data(data).build();
    }
    /**
     * 请求失败
     */
    public static ResponseResult fail(){
        return ResponseResult.builder().code("FAIL").message("请求失败").build();
    }
    /**
     * 请求失败
     */
    public static ResponseResult fail(String message){
        return ResponseResult.builder().code("FAIL").message(message).build();
    }
    /**
     * 请求失败
     */
    public static ResponseResult fail(String message, Object data){
        return ResponseResult.builder().code("FAIL").message(message).data(data).build();
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseError extends ResponseResult{
    /**
     * 请求地址(发生异常时返回)
     */
    private String requestUrl;
    /**
     * 异常类(发生异常时返回)
     */
    private String exception;
}

自定义异常

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BusinessException extends RuntimeException {
    /**
     * 错误码
     */
    private String code;
    /**
     * 错误信息
     */
    private String message;
    public BusinessException(String message){
        this.code = "BUSINESS_ERROR";
        this.message = message;
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RequestParamIsNullException extends RuntimeException{
    /**
     * 错误码
     */
    private String code;
    /**
     * 错误信息
     */
    private String message;
}

全局异常处理

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 捕获全局异常,处理所有不可知的异常
     */
    @ExceptionHandler(value=Exception.class)
    public ResponseResult handleException(Exception e, HttpServletRequest request) {
        log.error("出现未知异常 -> ", e);
        ResponseError error = new ResponseError();
        error.setCode("APPEAR_ERROR");
        error.setMessage(e.getMessage());
        error.setRequestUrl(request.getRequestURL().toString());
        error.setException(e.getClass().getName());
        return error;
    }
    /**
     * 捕获空指针异常
     */
    @ExceptionHandler(value=NullPointerException.class)
    public ResponseResult handleNullPointerException(NullPointerException e, HttpServletRequest request) {
        log.error("出现空指针异常 -> ", e);
        ResponseError error = new ResponseError();
        error.setCode("NULL_POINTER_ERROR");
        error.setMessage("出现空指针异常");
        error.setRequestUrl(request.getRequestURL().toString());
        error.setException(e.getClass().getName());
        return error;
    }
    /**
     * 自定义异常处理
     */
    @ExceptionHandler(value=BusinessException.class)
    public ResponseResult handleBusinessException(BusinessException e, HttpServletRequest request) {
        log.error("出现业务异常 -> ", e);
        ResponseError error = new ResponseError();
        error.setCode(e.getCode());
        error.setMessage(e.getMessage());
        error.setRequestUrl(request.getRequestURL().toString());
        error.setException(e.getClass().getName());
        return error;
    }
    /**
     * 处理参数为空异常
     */
    @ExceptionHandler(value = RequestParamIsNullException.class)
    public ResponseResult handleInputParamIsNullException(RequestParamIsNullException e, HttpServletRequest request) {
        log.error("请求参数为空异常 -> ", e);
        ResponseError error = new ResponseError();
        error.setCode(e.getCode());
        error.setMessage(e.getMessage());
        error.setRequestUrl(request.getRequestURL().toString());
        error.setException(e.getClass().getName());
        return error;
    }
}

测试接口

@RestController
public class HelloController {
    /**
     * 成功
     */
    @GetMapping("/hello1")
    public ResponseResult hello1(){
        Map<String,Object> map = new HashMap<>();
        map.put("name", "莫提");
        map.put("sex", "男");
        map.put("age", 22);
        return ResponseResult.success("请求成功", map);
    }
    /**
     * 失败
     */
    @GetMapping("/hello2")
    public ResponseResult hello2(){
        return ResponseResult.fail("请求失败");
    }
    /**
     * 触发自定义业务异常
     */
    @GetMapping("/hello3")
    public ResponseResult hello3(){
        throw new BusinessException("触发自定义业务异常");
    }
    /**
     * 触发其他异常
     */
    @GetMapping("/hello4")
    public ResponseResult hello4(){
        int i = 1 / 0;
        return new ResponseResult();
    }
    /**
     * 触发其他异常
     */
    @GetMapping("/hello5")
    public ResponseResult hello5(){
        Integer.parseInt("a");
        return new ResponseResult();
    }
    /**
     * 触发空指针异常
     */
    @GetMapping("/hello6")
    public ResponseResult hello6(){
        Integer i = null;
        int value = i.intValue();
        return new ResponseResult();
    }
}


相关文章
|
2月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
465 5
|
7月前
|
前端开发 Java UED
从基础到进阶:Spring Boot + Thymeleaf 整合开发中的常见坑与界面优化
本文深入探讨了 **Spring Boot + Thymeleaf** 开发中常见的参数绑定问题与界面优化技巧。从基础的 Spring MVC 请求参数绑定机制出发,分析了 `MissingServletRequestParameterException` 的成因及解决方法,例如确保前后端参数名、类型一致,正确设置请求方式(GET/POST)。同时,通过实际案例展示了如何优化支付页面的视觉效果,借助简单的 CSS 样式提升用户体验。最后,提供了官方文档等学习资源,帮助开发者更高效地掌握相关技能。无论是初学者还是进阶用户,都能从中受益,轻松应对项目开发中的挑战。
333 0
|
7月前
|
存储 Java 数据库
Spring Boot 注册登录系统:问题总结与优化实践
在Spring Boot开发中,注册登录模块常面临数据库设计、密码加密、权限配置及用户体验等问题。本文以便利店销售系统为例,详细解析四大类问题:数据库字段约束(如默认值缺失)、密码加密(明文存储风险)、Spring Security配置(路径权限不当)以及表单交互(数据丢失与提示不足)。通过优化数据库结构、引入BCrypt加密、完善安全配置和改进用户交互,提供了一套全面的解决方案,助力开发者构建更 robust 的系统。
215 0
|
7月前
|
缓存 安全 Java
深入解析HTTP请求方法:Spring Boot实战与最佳实践
这篇博客结合了HTTP规范、Spring Boot实现和实际工程经验,通过代码示例、对比表格和架构图等方式,系统性地讲解了不同HTTP方法的应用场景和最佳实践。
674 5
|
11月前
|
存储 安全 Java
Spring Boot 编写 API 的 10条最佳实践
本文总结了 10 个编写 Spring Boot API 的最佳实践,包括 RESTful API 设计原则、注解使用、依赖注入、异常处理、数据传输对象(DTO)建模、安全措施、版本控制、文档生成、测试策略以及监控和日志记录。每个实践都配有详细的编码示例和解释,帮助开发者像专业人士一样构建高质量的 API。
339 9
|
12月前
|
安全 JavaScript Java
SpringBoot解决跨域最佳实践
本文介绍了跨域问题的起因及最佳实践,重点讲解了SpringBoot中如何利用`CorsFilter`解决跨域问题。首先解释了由于浏览器的同源策略限制导致的跨域现象,然后提出了在服务端入口处解决跨域问题的建议,最后详细展示了三种SpringBoot中配置跨域的方法:使用默认配置、自定义配置规则以及通过配置文件管理跨域设置,以适应不同的应用场景。
571 5
|
缓存 监控 Java
|
JSON 缓存 Java
优雅至极!Spring Boot 3.3 中 ObjectMapper 的最佳实践
【10月更文挑战第5天】在Spring Boot的开发中,ObjectMapper作为Jackson框架的核心组件,扮演着处理JSON格式数据的核心角色。它不仅能够将Java对象与JSON字符串进行相互转换,还支持复杂的Java类型,如泛型、嵌套对象、集合等。在Spring Boot 3.3中,通过优雅地配置和使用ObjectMapper,我们可以更加高效地处理JSON数据,提升开发效率和代码质量。本文将从ObjectMapper的基本功能、配置方法、最佳实践以及性能优化等方面进行详细探讨。
977 2
|
12月前
|
Java 测试技术 数据库连接
使用Spring Boot编写测试用例:实践与最佳实践
使用Spring Boot编写测试用例:实践与最佳实践
1058 0
|
缓存 NoSQL Java
Springboot实战——黑马点评之秒杀优化
【9月更文挑战第27天】在黑马点评项目中,秒杀功能的优化对提升系统性能和用户体验至关重要。本文提出了多项Spring Boot项目的秒杀优化策略,包括数据库优化(如索引和分库分表)、缓存优化(如Redis缓存和缓存预热)、并发控制(如乐观锁、悲观锁和分布式锁)以及异步处理(如消息队列和异步任务执行)。这些策略能有效提高秒杀功能的性能和稳定性,为用户提供更佳体验。
955 6