Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 273|回复: 0

Spring项目中优雅的异常处理

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-6-1 10:59:58 | 显示全部楼层 |阅读模式

    前言

    如今的Java Web项目多是以 MVC 模式构建的,通常我们都是将 Service 层的异常统一的抛出,包括自定义异常和一些意外出现的异常,以便进行事务回滚,而 Service 的调用者 Controller 则承担着异常处理的责任,因为他是与 Web 前端交互的最后一道防线,如果此时还不进行处理则用户会在网页上看到一脸懵逼的

    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
    	at cn.keats.TestAdd.main(TestAdd.java:20)
    

    这样做有以下几点坏处:

    1. 用户体验很不友好,可能用户会吐槽一句:这是什么XX网站。然后不再访问了
    2. 如果这个用户是同行,他不仅看到了项目代码的结构,而且看到抛出的是这么低级的索引越界异常,会被人家看不起
    3. 用户看到网站有问题,打电话给客服,客服找到产品,产品叫醒正在熟睡/打游戏的你。你不仅睡不好游戏打不了还得挨批评完事改代码

    哎,真惨。因此一般我们采用的方法会是像这样:

    异常处理

    一般的Controller处理

    Service代码如下:

    @Service
    public class DemoService {
        public String respException(String param){
            if(StringUtils.isEmpty(param)){
                throw new MyException(ExceptionEnum.PARAM_EXCEPTION);
            }
            int i = 1/0;
            return "你看不见我!";
        }
    }
    

    Controller代码如下:

    @RestController
    public class DemoController {
        @Autowired
        private DemoService demoService;
        
        @PostMapping("respException")
        public Result respException(){
            try {
                return Result.OK(demoService.respException(null)); 
            } catch (MyException e){
                return Result.Exception(e, null);
            }
            catch (Exception e) {
                return Result.Error();
            }
        } 
    }
    

    如果此时发送如下的请求:

    http://localhost/respException
    

    服务器捕捉到自定义的异常 MyException,而返回参数异常的Json串:

    {
        "code": 1,
        "msg": "参数异常",
        "data": null
    }
    

    而当我们补上参数:

    http://localhost/respException?param=zhangsan
    

    则服务器捕捉到 by zero 异常,会返回未知错误到前端页面

    {
        "code": -1,
        "msg": "未知错误",
        "data": null
    }
    

    这样就会在一定程度上规避一些问题,例如参数错误就可以让用户去修改其参数,当然这一般需要前端同学配合做页面的参数校验,必传参数都有的时候再向服务器发送请求,一方面减轻服务器压力,一方面将问题前置节省双方的时间。但是这样写有一个坏处就是所有的Controller方法中关于异常的部分都是一样的,代码非常冗余。且不利于维护,而且一些不太熟悉异常机制的同学可能会像踢皮球一样将异常抓了抛,抛完又抓回来,闹着玩呢。。。(笔者就曾经接手过一个跑路同学的代码这样处理异常,那简直是跟异常捉迷藏呢!可恨)我们在Service有全局事务处理,在系统中可以有全局的日志处理,这些都是基于Spring 的一大杀器:AOP(面向切面编程) 实现的,AOP是什么呢?

    AOP

    AOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。如果说我们常用的OOP思想是从上到下执行业务流程的话,AOP就相当于在我们执行业务的时候横切一刀,如下图所示:

    image-20191201205830708

    而Advice(通知)是AOP思想中重要的一个术语,分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)和环绕通知(Around)五种。具体通知所表示的意义我这里不多赘述,网上关于Spring核心原理的讲解都会提及。而我们熟知的 Service 事务处理其实就是基于AOP AfterThrowing 通知实现的事务回滚。我们自定义的日志处理也可以根据不同的需求定制不同的通知入口。那既然如此,我们为何不自定义一个全局异常处理的切面去简化我们的代码呢?别急,且继续向下看。

    优雅的处理异常

    Spring 在 3.2 版本已经为我们提供了该功能: @ControllerAdvice 注解。此注解会捕捉Controller层抛出的异常,并根据 @ExceptionHandler 注解配置的方法进行异常处理。下面是一个示例工程,主要代码如下:

    Result类:

    此 Result 采用泛型的方式,便于在 Swagger 中配置方法的出参。使用静态工厂方法是的对象的初始化更加见名只意。对于不存在共享变量问题的 Error 对象,采用双重校验锁懒汉单例模式来节省服务器资源(当然最好还是整个项目运行中一直没有初始化它让人更加舒服。)

    package cn.keats.util;
    
    import cn.keats.exception.MyException;
    import lombok.Data;
    
    /**
     * 功能:统一返回结果,直接调用对应的工厂方法
     *
     * @author Keats
     * @date 2019/11/29 18:20
     */
    @Data
    public class Result<T>  {
        private Integer code;
        private String msg;
        private T data;
    
        /**
         * 功能:响应成功
         *
         * @param data 响应的数据
         * @return woke.cloud.property.transformat.Result
         * @author Keats
         * @date 2019/11/30 8:54
         */
        public static <T> Result<T> OK(T data){
            return new Result<>(0, "响应成功", data);
        }
    
        private static Result errorResult;
        /**
         * 功能:返回错误,此错误不可定制,全局唯一。一般是代码出了问题,需要修改代码
         *
         * @param
         * @return Result
         * @author Keats
         * @date 2019/11/30 8:55
         */
        public static Result Error(){
            if(errorResult == null){
                synchronized (Result.class){
                    if(errorResult == null){
                        synchronized (Result.class){
                            errorResult = new Result<>(-1, "未知错误", null);
                        }
                    }
                }
            }
            return errorResult;
        }
    
        /**
         * 功能:返回异常,直接甩自定义异常类进来
         *
         * @param e 自定义异常类
    	 * @param data 数据,如果没有填入 null 即可
         * @return woke.cloud.property.transformat.Result<T>
         * @author Keats
         * @date 2019/11/30 8:55
         */
        public static <T> Result<T> Exception(MyException e, T data){
            return new Result<>(e.getCode(), e.getMsg(), data);
        }
    
        /**
         * 功能:为了方便使用,使用静态工厂方法创建对象。如需新的构造方式,请添加对应的静态工厂方法
         *
         * @author Keats
         * @date 2019/11/30 8:56
         */
        private Result(Integer code, String msg, T data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
    }
    
    

    自定义异常类:

    package cn.keats.exception;
    
    import lombok.Getter;
    
    /**
     * 功能:系统自定义异常类。继承自RuntimeException,方便Spring进行事务回滚
     *
     * @author Keats
     * @date 2019/11/29 18:50
     */
    @Getter
    public class MyException extends RuntimeException{
        private Integer code;
        private String msg;
    
        public MyException(ExceptionEnum eEnum) {
            this.code = eEnum.getCode();
            this.msg = eEnum.getMsg();
        }
    }
    
    
    

    异常代码枚举类:

    package cn.keats.exception;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    /**
     * 功能:异常枚举
     *
     * @author Keats
     * @date 2019/11/29 18:49
     */
    @Getter
    @AllArgsConstructor
    public enum ExceptionEnum {
        PARAM_EXCEPTION(1,"参数异常"),
        USER_NOT_LOGIN(2,"用户未登录"),
        FILE_NOT_FOUND(3,"文件不存在,请重新选择");
    
    
        private Integer code;
        private String msg;
    }
    
    

    异常切面:

    其中 @RestControllerAdvice 是spring 4.3 添加的新注解,是 @ControllerAdvice 和 @ResponseBody 的简写方式,类似与 @RestController 与 @Controller 的关系

    package cn.keats.advice;
    
    import cn.keats.exception.MyException;
    import cn.keats.util.Result;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    /**
     * 功能:全局异常处理器,Controller异常直接抛出
     *
     * @return
     * @author Keats
     * @date 2019/11/30 10:28
     */
    @Slf4j
    @RestControllerAdvice
    public class ExceptionAdvice {
        /**
         * 功能:其余非预先规避的异常返回错误
         *
         * @param e
         * @return woke.cloud.property.transformat.Result
         * @author Keats
         * @date 2019/11/30 10:08
         */
        @ExceptionHandler(value = Exception.class)
        @ResponseBody
        public Result ResponseException(Exception e) {
            log.error("未知错误,错误信息:", e);
            return Result.Error();
        }
    
        /**
         * 功能:捕捉到 MyException 返回对应的消息
         *
         * @param e
         * @return woke.cloud.property.transformat.Result
         * @author Keats
         * @date 2019/11/30 10:07
         */
        @ExceptionHandler(value = MyException.class)
        @ResponseBody
        public Result myException(MyException e) {
            log.info("返回自定义异常:异常代码:" + e.getCode() + "异常信息:" + e.getMsg());
            return Result.Exception(e, null);
        }
    }
    
    
    

    此时的 Controller 方法可以这样写:

    package cn.keats.controller;
    
    import cn.keats.service.DemoService;
    import cn.keats.util.Result;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class DemoController {
        @Autowired
        private DemoService demoService;
    
        @PostMapping("respException")
        public Result respException(String param) throws Exception {
            return Result.OK(demoService.respException(param));
        }
        
        @PostMapping("respError")
        public Result respError() throws Exception {
            return Result.OK(demoService.respException(null));
        }
    }
    
    
    

    省略的大部分的异常处理代码,使得我们只需要关注业务,一方面提高了代码质量,可阅读性,另一方面也提高了我们的开发速度。美哉!

    启动项目,进行测试没有问题。

    image-20191201213847490

    image-20191201213912251

    我是 Keats,一个热爱技术的程序员,鉴于技术有限,如果本文有什么纰漏或者兄台还有其他更好的建议/实现方式,欢迎留言评论,谢谢您!

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-2 16:46 , Processed in 0.066041 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表