全局代码异常处理封装——让代码逼格更高点

我们开发过程中,不管是通用代码的开发还是业务代码的编写,都涉及到异常的处理,如果不对异常进行封装处理的话,会导致我们的代码十分的不雅观,比较low,所以一个好的全局异常处理的封装不仅仅能加快我们的开发效率,并且也能让我们的代码逼格更高点,何乐而不为呢?

首先我们来看下一般情况下代码的处理

    /**
     * 方法定义声明式异常,明确方法调用者需要手动处理异常
     *
     * @throws Exception
     */
    public void demonstrateExceptionThrows() throws Exception {
        System.out.println(1 / 0);
    }
    
    /************************************************************/
    
    @Autowired
    BizService bizService;

    public String call() {
        try {
            this.bizService.demonstrateExceptionThrows();
        } catch (Exception e) {
            return "error:" + e.getMessage();
        }
        return "ok";
    }

上面的代码我们使用try...catch...来捕获处理异常,如果大量的代码需要手动去处理异常,那么代码就先的很臃肿,代码冗余量也大增,这样的代码不仅阅读不赏心悦目,也显得我们的技术不咋滴啊。

SpringMVC提供的解决方案

那有没有一种解决方案,能很优雅的实现异常的捕捉,让我们更专注于业务层面的开发呢?有的,以前实现起来可能比较繁琐,万幸的是我们有了Spring这一利器,Spring本身为我们提供了一个解决方案来全局处理异常的方案,让我们更好地专注于业务层面的开发。

接下来我们来介绍一下今天的主角@ControllerAdvice@ExceptionHandler;

网上关于@ControllerAdvice@ExceptionHandler的介绍一大堆,我们主要不是为了介绍这两个注解的实现原理,我们只是简单接收这两个注解在我们封装的全局异常处理的方案中是如何运用的,能实现什么样的一个效果

具体的原理和使用方法可以参考如下的博文

Spring MVC之@ControllerAdvice详解

异常的判断和抛出

既然Spring为我们解决了全局异常的处理,那么我们这里的任务就是需要设计一行代码来捕获异常,并且向外抛出自定义的异常

说到一行代码就抛出异常,各位是不是第一个就想到Assert呢;是的,Assert的内部实现很简单,就是判断,不符合条件就抛出异常,但是也有相当大的局限性,Assert抛出的异常全部都是IllegalArgumentException异常,这个跟我们的需求不太符合,所以我们自己设计出一个类似Assert的类,但是可以手动返回自定义的异常以及异常编码和异常信息

我们的异常的判断和抛出需要满足以下几个需求:

  • 同意定义自定义异常的创建
  • 异常的编码能够很方便的扩展,并且异常的信息能够自定义
  • 支持多个自定义异常的创建,并且对扩展开放,定义的异常自动进行全局了拦截

好了,有句话说的话 talk is cheap, show me code,下面我们来贴出主要的代码

使用的SpringBoot和jdk1.8环境

  • 首先定义出自定义的Assert来实现我们的一行代码抛出异常,该接口中定义了创建异常的犯法,并且所以异常对于非空的判断,后面我们自定义的异常都需要实现该接口,并且提供自定义异常实例化的方法。
    /**
     * Copyright © 2018 五月工作室. All rights reserved.
     *
     * @Project: biz-exception
     * @ClassName: BizAssert
     * @Package: com.amos.bizexception.exception
     * @author: zhuqb
     * @Description: 自定义的断言
     * <p/>
     * 仿照Assert的思路来提供异常的处理,让代码更优美
     * <p/>
     * 接口定义创建异常的定义方法,并且默认提供异常的判断方法
     * @date: 2019/7/11 0011 上午 8:58
     * @Version: V1.0
     */
    public interface BizAssert {
        /**
         * 定义创建异常的方法
         *
         * @param args 异常的信息
         * @return
         */
        BaseException newException(Object... args);
    
        /**
         * 定义创建好友异常信息的异常的方法
         *
         * @param throwable 异常的信息
         * @param args      异常的msg
         * @return
         */
        BaseException newException(Throwable throwable, Object... args);
    
        /**
         * 判断对象是否是空
         *
         * @param object 带判断是否为空的对象
         */
        default void notNullAssert(Object object) {
            if (StringUtils.isEmpty(object)) {
                throw this.newException(object);
            }
        }
    
        /**
         * 判断对象是否为空
         *
         * @param object 带判断是否为空的对象
         * @param msg    异常返回的信息
         */
        default void notNullAssert(Object object, String msg) {
            if (StringUtils.isEmpty(object)) {
                throw this.newException(msg);
            }
        }
    
        /**
         * 判断对象是否为空(集合没有元素)
         *
         * @param object 带判断是否为空的对象
         * @param msg    异常返回的信息
         */
        default void notEmptyAssert(Object object, String msg) {
            if (StringUtils.isEmpty(object)) {
                throw this.newException(msg);
            }
            // 如果是集合,则判断集合里面的元素是否存在
            if (object instanceof Collection) {
                int size = ((Collection) object).size();
                if (size == 0) {
                    throw this.newException(msg);
                }
            }
    
            // 如果是数组,则判断数组元素是否存在
            if (object.getClass().isArray()) {
                int length = ((Object[]) object).length;
                if (length == 0) {
                    throw this.newException(msg);
                }
            }
        }
    
    }

  • BaseException继承了RuntimeException类,这里是我们所有类的基类,也是全局拦截所有自定义异常类的基础,主要是通过多态的方式来实现拦截所有子类的目的,获取子类的自定义异常的编码和信息
    /**
     * Copyright © 2018 五月工作室. All rights reserved.
     *
     * @Project: biz-exception
     * @ClassName: BaseException
     * @Package: com.amos.bizexception.exception.bean
     * @author: zhuqb
     * @Description: 自定义业务处理的基类异常
     * @date: 2019/7/11 0011 上午 10:11
     * @Version: V1.0
     */
    public class BaseException extends RuntimeException {
        private static final long serialVersionUID = 6433183307699823302L;
        /**
         * 异常的编码和信息
         */
        private IResult result;
        /**
         * 消息定义的参数
         */
        private Object[] args;
    
        /**
         * 基类异常的构造器
         *
         * @param result
         */
        BaseException(IResult result) {
            super(result.getMsg());
            this.result = result;
        }
    
        /**
         * 通过code和msg来实例化基类异常信息
         *
         * @param code
         * @param msg
         */
        BaseException(int code, String msg) {
            super(msg);
            this.result = new IResult() {
                @Override
                public int getCode() {
                    return code;
                }
    
                @Override
                public String getMsg() {
                    return msg;
                }
            };
        }
    
        /**
         * 基类异常实例化,附带异常参数
         *
         * @param result
         * @param args
         * @param msg
         */
        BaseException(IResult result, Object[] args, String msg) {
            super(msg);
            this.result = result;
            this.args = args;
        }
    
        /**
         * 基类异常实例化,附带异常类信息
         *
         * @param result
         * @param args
         * @param msg
         * @param throwable
         */
        BaseException(IResult result, Object[] args, String msg, Throwable throwable) {
            super(msg, throwable);
            this.result = result;
            this.args = args;
        }
    
    
    }

  • 基类中我们定义了IResult这个接口,这个接口只定义了两个方法,主要是为了我们自定义异常的编码和信息服务的,编码代表各种不同种类的异常,信息则是异常的具体说明,然后在基类中设定不同的参数的实例化方法
    /**
     * Copyright © 2018 五月工作室. All rights reserved.
     *
     * @Project: biz-exception
     * @ClassName: IResult
     * @Package: com.amos.bizexception.exception.type
     * @author: zhuqb
     * @Description: 该接口定义了异常信息返回的通用字段
     * <p/>
     * 让子类去实现该接口,返回异常的编码和信息
     * 这样做的好处是用户可以自定义实现异常的编码和信息,系统的扩展性更强
     * @date: 2019/7/11 0011 上午 10:13
     * @Version: V1.0
     */
    public interface IResult {
        /**
         * 异常的编码
         *
         * @return
         */
        int getCode();
    
        /**
         * 异常的信息
         *
         * @return
         */
        String getMsg();
    }

  • 基类的相关方法和属性我们定义好了,现在我们开始来实现我们的自定义异常,该异常继承BaseException异常,并且定义构造器方便实例化
    /**
     * Copyright © 2018 五月工作室. All rights reserved.
     *
     * @Project: biz-exception
     * @ClassName: BizException
     * @Package: com.amos.bizexception.exception.bean
     * @author: zhuqb
     * @Description: 自定义业务异常
     * <p/>
     * 异常继承基类异常,并且实现对应的构造器来初始化自定义业务异常
     * @date: 2019/7/11 0011 上午 10:41
     * @Version: V1.0
     */
    public class BizException extends BaseException {
        private static final long serialVersionUID = -7118967757594184955L;
    
        /**
         * 自定义业务异常的构造器
         *
         * @param result
         */
        public BizException(IResult result) {
            super(result);
        }
    
        /**
         * 自定义业务异常的构造器
         *
         * @param code
         * @param msg
         */
        public BizException(int code, String msg) {
            super(code, msg);
        }
    
        /**
         * 自定义业务异常的构造器
         *
         * @param result
         * @param args
         * @param msg
         */
        public BizException(IResult result, Object[] args, String msg) {
            super(result, args, msg);
        }
    
        /**
         * 自定义业务异常的构造器
         *
         * @param result
         * @param args
         * @param msg
         * @param throwable
         */
        public BizException(IResult result, Object[] args, String msg, Throwable throwable) {
            super(result, args, msg, throwable);
        }
    }
  • 上面我们已经定义好了所有基类的Assert,其实所有基类的Assert就已经能实现全部捕获异常的要求了,但是不方便扩展,我们的需求是支持各种自定义异常抛出各自的异常,并且能够自定义异常编码和消息, 这里我们先来让我们的程序可以支持创建各种不同的自定义异常
    /**
     * Copyright © 2018 五月工作室. All rights reserved.
     *
     * @Project: biz-exception
     * @ClassName: BizExceptionAssert
     * @Package: com.amos.bizexception.exception
     * @author: zhuqb
     * @Description: 自定义业务异常断言
     * <p/>
     * 该接口异常异常信息和自定义业务断言接口, 继承这两个接口
     * 1. 获取异常的编码和信息接口,方便实现类自定义异常的编码和信息
     * 2. 获取定义异常的创建方法,并且重写创建异常实例的方法,返回对应的业务异常的实例
     * 3. 注意:此处的异常的编码和信息我们可以暂时不处理,交给子类来自定义异常编码和信息
     * @date: 2019/7/11 0011 上午 10:48
     * @Version: V1.0
     */
    public interface BizExceptionAssert extends IResult, BizAssert {
        /**
         * 实现创建异常的方法, 返回自定义异常实例对象
         *
         * @param args 异常的信息
         * @return
         */
        @Override
        default BaseException newException(Object... args) {
            String msg = MessageFormat.format(this.getMsg(), args);
            return new BizException(this, args, msg);
        }
    
        /**
         * 实现创建异常的方法, 返回自定义异常实例对象
         *
         * @param throwable 异常的信息
         * @param args      异常的消息
         * @return
         */
        @Override
        default BaseException newException(Throwable throwable, Object... args) {
            String msg = MessageFormat.format(this.getMsg(), args);
            return new BizException(this, args, msg, throwable);
        }
    }
  • 上面的注释说的没有实现异常编码和信息的定义,这里我们优化下设计,同一种自定义异常也支持异常编码和信息的自定义,说白了,我们这里就是将自定义异常和异常编码和信息解耦了,这样方便我们通过定义不同的对象来组合自定义异常和异常编码和信息,这个其实体现了设计模式中开闭原则的开原则,那么闭原则我们怎么处理呢?别急,还记的枚举么,这个可以初步实现
    /**
     * Copyright © 2018 五月工作室. All rights reserved.
     *
     * @Project: biz-exception
     * @ClassName: BizExceptionEnum
     * @Package: com.amos.bizexception.exception.type
     * @author: zhuqb
     * @Description: 自定义异常的编码和消息
     * <p/>
     * 这里没有实现setter方法,主要是为了不能修改枚举中的code值,
     * 但是我们自定义了 definedMsg 方法,主要是为了方便用户自定义异常的信息
     * 默认是选择枚举的msg信息
     * @date: 2019/7/11 0011 上午 11:15
     * @Version: V1.0
     */
    @AllArgsConstructor
    public enum BizExceptionEnum implements BizExceptionAssert {
        /**
         * 异常编码 1000
         * 异常信息 对象不能为空
         */
        NOT_NULL(1000, "对象不能为空");
    
        private int code;
        private String msg;
    
        @Override
        public int getCode() {
            return this.code;
        }
    
        @Override
        public String getMsg() {
            return this.msg;
        }
    
        /**
         * 此处可以动态修改msg的返回值
         * 这么做是为了实现自定义异常信息
         *
         * @param msg
         * @return
         */
        public BizExceptionEnum definedMsg(String msg) {
            this.msg = msg;
            return this;
        }
    }

好了,至此我们对于自定义异常的捕捉和抛出就已经全部完结了,下面来看看我们业务端的调用吧

    @Service
    public class BizService {
    
        void call(Object object) {
            BizExceptionEnum.NOT_NULL.definedMsg("判断出对象为NULL,手动抛出异常").notNullAssert(object);
            System.out.println("方法执行结束");
        }
    }

是不是发现很简单,一行代码就实现了手动抛出异常,逼格是不是瞬间高了?!!!

不仅仅如此,这个异常的封装还很容易进行扩展,如果想再次自定义新的异常,只需要继承BaseException,提供实例化对象,并且提供自定义的Assert来实现自定异常的初始化方法,最后提供对应的异常编码和信息的枚举来当做自定义异常实现类即可。

类的模型图如下:


image

全局异常的拦截处理

上面我们已经将异常的判断和抛出这个大头已经全部处理完毕了,接下来,得助于Spring提供的@ControllerAdvice@ExceptionHandler,我们可以和方便的实现异常的全局拦截处理

    /**
     * Copyright © 2018 五月工作室. All rights reserved.
     *
     * @Project: biz-exception
     * @ClassName: GlobalExceptionHandler
     * @Package: com.amos.bizexception.advice
     * @author: zhuqb
     * @Description: 全局异常处理类
     * <p/>
     * 该Handler主要处理常见的web访问的信息
     * @date: 2019/7/11 0011 上午 11:46
     * @Version: V1.0
     */
    @Slf4j
    @ControllerAdvice(basePackages = {"com.amos.bizexception"})
    @ResponseBody
    public class GlobalExceptionHandler {
        /**
         * 处理自定义异常
         *
         * @param exception
         * @return
         */
        @ExceptionHandler(BaseException.class)
        public Result handleBizException(BaseException exception) {
            log.error("进入自定义异常拦截中...异常信息为:{}", exception.getMessage());
            return ResultWapper.error(exception.getMessage());
        }
    
    }

这里的拦截了BaseException这个基类,就是为了拦截所有的自定义异常.

至此,我们的全局代码异常处理封装就已经结束了,具体的代码可以查看:

biz-exception

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • PS: 本文讲得比较细,所以篇幅较长。 阅读时间:30m~1h。请认真读完,希望你一小时后能对统一异常处理有一个清...
    sprainkle阅读 19,283评论 21 157
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 写着写着发现简书提醒我文章接近字数极限,建议我换一篇写了。 建议52:推荐使用String直接量赋值 一般对象都是...
    我没有三颗心脏阅读 1,331评论 2 4
  •   由于 JavaScript 本身是动态语言,而且多年来一直没有固定的开发工具,因此人们普遍认为它是一种最难于调...
    霜天晓阅读 758评论 0 1
  • 报到,就是你所想的那样,有点像大学入学报到。交完各种证件,做完体检,就无所事事了。 军训第一个小半天,没有大学那种...
    明半灭阅读 252评论 2 0