第四十五章:基于SpringBoot 设计业务逻辑异常统一处理

在咱们平时的项目研发过程当中,异常通常都是程序员最为头疼的问题,异常的抛出、捕获、处理等既涉及事务回滚,还会涉及返回前端消息提醒信息。那么咱们怎么设计能够解决上面的两个的痛点呢?咱们可不能够统一处理业务逻辑而后给出前端对应的异常提醒内容呢?前端

本章目标

基于SpringBoot平台构建业务逻辑异常统一处理,异常消息内容格式化。java

福利来了

腾讯云特惠服务器10元/月,点击参团mysql

SpringBoot 企业级核心技术学习专题


专题 专题名称 专题描述
001 Spring Boot 核心技术 讲解SpringBoot一些企业级层面的核心组件
002 Spring Boot 核心技术章节源码 Spring Boot 核心技术简书每一篇文章码云对应源码
003 Spring Cloud 核心技术 对Spring Cloud核心技术全面讲解
004 Spring Cloud 核心技术章节源码 Spring Cloud 核心技术简书每一篇文章对应源码
005 QueryDSL 核心技术 全面讲解QueryDSL核心技术以及基于SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技术 全面讲解SpringDataJPA核心技术
007 SpringBoot核心技术学习目录 SpringBoot系统的学习目录,敬请关注点赞!!!

构建项目

咱们将逻辑异常核心处理部分提取出来做为单独的jar供其余模块引用,建立项目在parent项目pom.xml添加公共使用的依赖,配置内容以下所示:git

<dependencies>
		<!--Lombok-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<!--测试模块依赖-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!--web依赖-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
</dependencies>
复制代码

项目建立完成后除了.ideaimlpom.xml保留,其余的都删除。程序员

异常处理核心子模块

咱们建立一个名为springboot-core-exception的子模块,在该模块内自定义一个LogicException运行时异常类,继承RuntimeException并重写构造函数,代码以下所示:web

/**
 * 自定义业务逻辑异常类
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午2:38
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
public class LogicException extends RuntimeException {

    /**
     * 日志对象
     */
    private Logger logger = LoggerFactory.getLogger(LogicException.class);

    /**
     * 错误消息内容
     */
    protected String errMsg;
    /**
     * 错误码
     */
    protected String errCode;
    /**
     * 格式化错误码时所需参数列表
     */
    protected String[] params;


    /**
     * 获取错误消息内容
     * 根据errCode从redis内获取未被格式化的错误消息内容
     * 并经过String.format()方法格式化错误消息以及参数
     *
     * @return
     */
    public String getErrMsg() {
        return errMsg;
    }

    /**
     * 获取错误码
     *
     * @return
     */
    public String getErrCode() {
        return errCode;
    }

    /**
     * 获取异常参数列表
     *
     * @return
     */
    public String[] getParams() {
        return params;
    }

    /**
     * 构造函数设置错误码以及错误参数列表
     *
     * @param errCode 错误码
     * @param params  错误参数列表
     */
    public LogicException(String errCode, String... params) {
        this.errCode = errCode;
        this.params = params;
        //获取格式化后的异常消息内容
        this.errMsg = ErrorMessageTools.getErrorMessage(errCode, params);
        //错误信息
        logger.error("系统遇到以下异常,异常码:{}>>>异常信息:{}", errCode, errMsg);
    }
}
复制代码

在重写的构造函数内须要传递两个参数errCodeparams,其目的是为了初始化类内的全局变量。redis

  • errCode:该字段是对应的异常码,咱们在后续文章内容中建立一个存放异常错误码的枚举,而errCode就是枚举对应的字符串的值。
  • params:这里是对应errCode字符串含义描述时所须要的参数列表。
  • errMsg:格式化后的业务逻辑异常消息描述,咱们在构造函数内能够看到调用了ErrorMessageTools.getErrorMessage(errCode,params);,这个方法做用是经过异常码在数据库内获取未格式化的异常描述,经过传递的参数进行格式化异常消息描述。

建立异常核心包的目的就是让其余模块直接添加依赖,那异常描述内容该怎么获取呢?spring

定义异常消息获取接口

咱们在springboot-exception-core模块内添加一个接口LogicExceptionMessage,该接口提供经过异常码获取未格式化的异常消息描述内容方法,接口定义以下所示:sql

/**
 * 逻辑异常接口定义
 * 使用项目须要实现该接口方法并提供方法实现
 * errCode对应逻辑异常码
 * getMessage返回字符串为逻辑异常消息内容
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午2:41
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
public interface LogicExceptionMessage {

    /**
     * 获取异常消息内容
     * @param errCode 错误码
     * @return
     */
    public String getMessage(String errCode);
}
复制代码

在须要加载springboot-exception-core依赖的项目中,建立实体类实现LogicExceptionMessage接口并重写getMessage(String errCode)方法咱们就能够经过spring IOC获取实现类实例进行操做获取数据,下面咱们在编写使用异常模块时会涉及到。数据库

格式化异常消息工具类

下面咱们再回头看看构造函数格式化异常消息工具类ErrorMessageTools,该工具类内提供getErrorMessage方法用于获取格式化后的异常消息描述,代码实现以下所示:

/**
 * 异常消息描述格式化工具类
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午2:40
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
public class ErrorMessageTools {
    /**
     * 异常消息获取
     *
     * @param errCode 异常消息码
     * @param params  格式化异常参数所需参数列表
     * @return
     */
    public static String getErrorMessage(String errCode, Object... params) {
        //获取业务逻辑消息实现
        LogicExceptionMessage logicExceptionMessage = SpringBeanTools.getBean(LogicExceptionMessage.class);
        if (ObjectUtils.isEmpty(logicExceptionMessage)) {
            try {
                throw new Exception("请配置实现LogicExceptionMessage接口并设置实现类被SpringIoc所管理。");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //获取错误消息内容
        String errMsg = logicExceptionMessage.getMessage(errCode);
        //格式化错误消息内容
        return ObjectUtils.isEmpty(params) ? errMsg : String.format(errMsg, params);
    }
}
复制代码

注意:因为咱们的工具类都是静态方法调用方式,因此没法直接使用Spring IOC注解注入的方式获取LogicExceptionMessage实例。

因为没法注入实例,在getErrorMessage方法内,咱们经过工具类SpringBeanTools来获取ApplicationContext上下文实例,再经过上下文来获取指定类型的Bean;获取到LogicExceptionMessage实例后调用getMessage方法,根据传入的errCode就能够直接从接口实现类实例中获取到未格式化的异常描述!

固然实现类能够是以RedisMap集合数据库文本做为数据来源。

获取到未格式化的异常描述后经过String.format方法以及传递的参数直接就能够获取格式化后的字符串,如:

未格式化异常消息 => 用户:%s已被冻结,没法操做.
格式化代码 => String.format("%s已被冻结,没法操做.","恒宇少年");
格式化后效果 => 用户:恒宇少年已被冻结,没法操做.
复制代码

具体的格式化特殊字符含义能够去查看String.format文档,如何获取ApplicationContext上下文对象,请访问第三十二章:如何获取SpringBoot项目的applicationContext对象查看。

咱们再回到LogicException构造函数内,这时errMsg字段对应的值就会是格式化后的异常消息描述,在外部咱们调用getErrMsg方法就能够直接获得异常描述。

到目前为止,咱们已经将springboot-exception-core模块代码编码完成,下面咱们来看下怎么来使用咱们自定义的业务逻辑异常而且获取格式化后的异常消息描述。

异常示例模块

基于parent咱们来建立一个名为springboot-exception-example的子模块项目,项目内须要添加一些额外的配置依赖,固然也须要将咱们的springboot-exception-core依赖添加进入,pom.xml配置文件内容以下所示:

<dependencies>
        <!--异常核心依赖-->
        <dependency>
            <groupId>com.hengyu</groupId>
            <artifactId>springboot-exception-core</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!--spring data jpa依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--druid依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.6</version>
        </dependency>
</dependencies>
复制代码

下面咱们来配置下咱们示例项目application.yml文件须要的配置,以下所示:

spring:
  application:
    name: springboot-exception-core
    #数据源配置
  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
  jpa:
    properties:
      hibernate:
        #配置显示sql
        show_sql: true
        #配置格式化sql
        format_sql: true
复制代码

在上面咱们有讲到LogicExceptionMessage获取的内容能够从不少种数据源中读取,咱们仍是采用数据库来进行读取,建议正式环境放到redis缓存内!!!

异常信息表

接下来在数据库内建立异常信息表sys_exception_info,语句以下:

DROP TABLE IF EXISTS `sys_exception_info`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `sys_exception_info` (
  `EI_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键自增',
  `EI_CODE` varchar(30) DEFAULT NULL COMMENT '异常码',
  `EI_MESSAGE` varchar(50) DEFAULT NULL COMMENT '异常消息内容',
  PRIMARY KEY (`EI_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系统异常基本信息';
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `sys_exception_info`
--

LOCK TABLES `sys_exception_info` WRITE;
/*!40000 ALTER TABLE `sys_exception_info` DISABLE KEYS */;
INSERT INTO `sys_exception_info` VALUES (1,'USER_NOT_FOUND','用户不存在.'),(2,'USER_STATUS_FAILD','用户状态异常.');
/*!40000 ALTER TABLE `sys_exception_info` ENABLE KEYS */;
UNLOCK TABLES;
复制代码

咱们经过spring-data-jpa来实现数据读取,下面对应数据表建立对应的Entity

异常信息实体

/**
 * 系统异常基本信息实体
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午3:35
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
@Data
@Entity
@Table(name = "sys_exception_info")
public class ExceptionInfoEntity implements Serializable{
    /**
     * 异常消息编号
     */
    @Id
    @GeneratedValue
    @Column(name = "EI_ID")
    private Integer id;
    /**
     * 异常消息错误码
     */
    @Column(name = "EI_CODE")
    private String code;
    /**
     * 异常消息内容
     */
    @Column(name = "EI_MESSAGE")
    private String message;
}
复制代码

异常信息数据接口

/**
 * 异常数据接口定义
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午3:34
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
public interface ExceptionRepository
    extends JpaRepository<ExceptionInfoEntity,Integer>
{
    /**
     * 根据异常码获取异常配置信息
     * @param code 异常码
     * @return
     */
    ExceptionInfoEntity findTopByCode(String code);
}
复制代码

在数据接口内经过spring-data-jpa方法查询方式,经过errCode读取异常信息实体内容。

在开发过程当中异常跑出时所用到的errCode通常存放在枚举类型或者常量接口内,在这里咱们选择可扩展相对来讲比较强的枚举类型,代码以下:

/**
 * 错误码枚举类型
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午3:25
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
public enum ErrorCodeEnum {
    /**
     * 用户不存在.
     */
    USER_NOT_FOUND,
    /**
     * 用户状态异常.
     */
    USER_STATUS_FAILD,
    //...添加其余错误码
}
复制代码

异常码枚举内容项是须要根据数据库异常信息表对应变更的,可以保证咱们在抛出异常时,在数据库内有对应的信息。

LogicExceptionMessage实现类定义

咱们在springboot-exception-core核心模块内添加了LogicExceptionMessage接口定义,须要咱们实现该接口的getMessage方法核心模块,这样才能够获取数据库内对应的异常信息,实现类以下所示:

/**
 * 业务逻辑异常消息获取实现类
 * - 消息能够从数据库内获取
 * - 消息可从Redis内获取
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午3:16
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
@Component
public class LogicExceptionMessageSupport implements LogicExceptionMessage {

    /**
     * 异常数据接口
     */
    @Autowired
    private ExceptionRepository exceptionRepository;

    /**
     * 根据错误码获取错误信息
     * @param errCode 错误码
     * @return
     */
    @Override
    public String getMessage(String errCode) {
        ExceptionInfoEntity exceptionInfoEntity = exceptionRepository.findTopByCode(errCode);
        if(!ObjectUtils.isEmpty(exceptionInfoEntity)) {
            return exceptionInfoEntity.getMessage();
        }
        return "系统异常";
    }
}
复制代码

getMessage方法内经过ExceptionRepository数据接口定义的findTopByCode方法获取指定异常吗的异常信息,当存在异常信息时返回未格式化的异常描述。

统一返回实体定义

对于接口项目(包括先后分离项目)在处理返回统一格式时,咱们一般会采用固定实体的方式,这样对于前端调用接口的开发者来讲解析内容是比较方便的,一样在开发过程当中会约定遇到系统异常、业务逻辑异常时返回的格式内容,固然这跟请求接口正确返回的格式是同样的,只不过字段内容有差别。 统一返回实体ApiResponseEntity<T extends Object>以下:

/**
 * 接口响应实体
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/9
 * Time:下午3:04
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
@Data
@Builder
public class ApiResponseEntity<T extends Object> {
    /**
     * 错误消息
     */
    private String errorMsg;
    /**
     * 数据内容
     */
    private T data;
}
复制代码

ApiResponseEntity实体内,采用了Lombok的构造者设计模式@Builder注解,配置该注解的实体会自动在.class文件内添加内部类实现设计模式,部分自动生成代码以下:

// ...
public static class ApiResponseEntityBuilder<T> {
        private String errorMsg;
        private T data;

        ApiResponseEntityBuilder() {
        }

        public ApiResponseEntity.ApiResponseEntityBuilder<T> errorMsg(String errorMsg) {
            this.errorMsg = errorMsg;
            return this;
        }

        public ApiResponseEntity.ApiResponseEntityBuilder<T> data(T data) {
            this.data = data;
            return this;
        }

        public ApiResponseEntity<T> build() {
            return new ApiResponseEntity(this.errorMsg, this.data);
        }

        public String toString() {
            return "ApiResponseEntity.ApiResponseEntityBuilder(errorMsg=" + this.errorMsg + ", data=" + this.data + ")";
        }
    }
// ...
复制代码

到目前为止,咱们并未添加全局异常相关的配置,而全局异常配置这块,咱们采用以前章节讲到的@ControllerAdvice来实现,@ControllerAdvice相关的内容请访问第二十一章:SpringBoot项目中的全局异常处理

全局异常通知定义

咱们本章节仅仅添加业务逻辑异常的处理,具体编码以下所示:

/**
 * 控制器异常通知类
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午5:30
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
@ControllerAdvice(annotations = RestController.class)
@ResponseBody
public class ExceptionAdvice {

    /**
     * logback new instance
     */
    Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 处理业务逻辑异常
     *
     * @param e 业务逻辑异常对象实例
     * @return 逻辑异常消息内容
     */
    @ExceptionHandler(LogicException.class)
    @ResponseStatus(code = HttpStatus.OK)
    public ApiResponseEntity<String> logicException(LogicException e) {
        logger.error("遇到业务逻辑异常:【{}】", e.getErrCode());
        // 返回响应实体内容
        return ApiResponseEntity.<String>builder().errorMsg(e.getErrMsg()).build();
    }
}
复制代码

最近技术群内有同窗问我,既然咱们用的是@RestController为何这里还须要配置@ResponseBody?这里给你们一个解释,咱们控制器通知确实是监听的@RestController,而@RestController注解的控制器统一都是返回JSON格式的数据。那么咱们在遇到异常后,请求已经再也不控制器内了,已经交付给控制器通知类,那么咱们通知类若是一样想返回JSON数据,这里就须要配置@ResponseBody注解来实现。

咱们来看上面logicException()方法,该方法返回值是咱们定义的统一返回实体,目的是为了遇到业务逻辑异常时一样返回与正确请求同样的格式。

  • @ ExceptionHandler配置了将要处理LogicException类型的异常,也就是只要系统遇到LogicException异常而且抛给了控制器,就会调用该方法。
  • @ResponseStatus配置了返回的状态值,由于咱们遇到业务逻辑异常前端确定须要的不是500错误,而是一个200状态的JSON业务异常描述。

在方法返回时使用构造者设计模式并将异常消息传递给errorMsg()方法,这样就实现了字段errorMsg的赋值。

测试

异常相关的编码完成,下面咱们来建立一个测试的控制器模拟业务逻辑发生时,系统是怎么作出的返回? 测试控制内容以下所示:

/**
 * 测试控制器
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2018/1/7
 * Time:下午3:12
 * 码云:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
@RestController
public class IndexController {
    /**
     * 首页方法
     *
     * @return
     */
    @RequestMapping(value = "/index")
    public ApiResponseEntity<String> index() throws LogicException {
        /**
         * 模拟用户不存在
         * 抛出业务逻辑异常
         */
        if (true) {
            throw new LogicException(ErrorCodeEnum.USER_STATUS_FAILD.toString());
        }
        return ApiResponseEntity.<String>builder().data("this is index mapping").build();
    }
}
复制代码

根据上面代码含义,当咱们在访问/index时就会发生USER_STATUS_FAILD业务逻辑异常,按照咱们以前的全局异常配置以及统一返回实体实例化,访问后会出现ApiResponseEntity格式JSON数据,下面咱们运行项目访问查看效果。 界面输出内容以下所示:

{
    "errorMsg": "用户状态异常.",
    "data": null
}
复制代码

而在控制台因为咱们编写了日志信息,也一样有对应的输出,以下所示:

Hibernate: 
    select
        exceptioni0_.ei_id as ei_id1_0_,
        exceptioni0_.ei_code as ei_code2_0_,
        exceptioni0_.ei_message as ei_messa3_0_ 
    from
        sys_exception_info exceptioni0_ 
    where
        exceptioni0_.ei_code=? limit ?
2018-01-09 18:54:00.647 ERROR 2024 --- [nio-8080-exec-1] c.h.s.exception.core.LogicException      : 系统遇到以下异常,异常码:USER_STATUS_FAILD>>>异常信息:用户状态异常.
2018-01-09 18:54:00.649 ERROR 2024 --- [nio-8080-exec-1] c.h.s.e.c.advice.ExceptionAdvice         : 遇到业务逻辑异常:【USER_STATUS_FAILD】
复制代码

若是业务逻辑异常在Service层时,咱们根本不须要去操心事务回滚的问题,由于LogicException自己就是运行时异常,而项目中抛出运行时异常时事务就会自动回滚。

咱们把业务逻辑异常屏蔽掉,把true改为false查看正确时返回的格式,以下所示:

{
    "errorMsg": null,
    "data": "this is index mapping"
}
复制代码

若是想把对应的null改为空字符串,请访问查看第五章:配置使用FastJson返回Json视图

总结

本章将以前章节的部份内容进行了整合,主要是全局异常、统一格式返回等;这种方式是目前咱们公司产品中正在使用的方式,已经能够知足平时的业务逻辑异常定义以及返回,将异常消息存放到数据库中咱们能够随时更新提示内容,这一点仍是比较易用的。

本章源码已经上传到码云: SpringBoot配套源码地址:gitee.com/hengboy/spr… SpringCloud配套源码地址:gitee.com/hengboy/spr… SpringBoot相关系列文章请访问:目录:SpringBoot学习目录 QueryDSL相关系列文章请访问:QueryDSL通用查询框架学习目录 SpringDataJPA相关系列文章请访问:目录:SpringDataJPA学习目录,感谢阅读!

微信扫码关注 - 专一分享

欢迎加入恒宇少年的知识星球,恒宇少年带你走之后的技术道路!!!

知识星球
相关文章
相关标签/搜索