在我们平时的项目研发过程中,异常一般都是程序员最为头疼的问题,异常的抛出、捕获、处理等既涉及事务回滚,还会涉及返回前端消息提醒信息。那么我们怎么设计可以解决上面的两个的痛点呢?我们可不可以统一处理业务逻辑然后给出前端对应的异常提醒内容呢?
本章目标
基于SpringBoot
平台构建业务逻辑异常统一处理,异常消息内容格式化。
构建项目
我们将逻辑异常核心处理部分提取出来作为单独的jar
供其他模块引用,创建项目在parent
项目pom.xml
添加公共使用的依赖,配置内容如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <dependencies> <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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
|
项目创建完成后除了.idea
、iml
、pom.xml
保留,其他的都删除。
异常处理核心子模块
我们创建一个名为springboot-core-exception
的子模块,在该模块内自定义一个LogicException
运行时异常类,继承RuntimeException
并重写构造函数,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
public class LogicException extends RuntimeException {
private Logger logger = LoggerFactory.getLogger(LogicException.class);
protected String errMsg;
protected String errCode;
protected String[] params;
public String getErrMsg() { return errMsg; }
public String getErrCode() { return errCode; }
public String[] getParams() { return params; }
public LogicException(String errCode, String... params) { this.errCode = errCode; this.params = params; this.errMsg = ErrorMessageTools.getErrorMessage(errCode, params); logger.error("系统遇到如下异常,异常码:{}>>>异常信息:{}", errCode, errMsg); } }
|
在重写的构造函数内需要传递两个参数errCode
、params
,其目的是为了初始化类内的全局变量。
errCode
:该字段是对应的异常码,我们在后续文章内容中创建一个存放异常错误码的枚举,而errCode
就是枚举对应的字符串的值。
params
:这里是对应errCode
字符串含义描述时所需要的参数列表。
errMsg
:格式化后的业务逻辑异常消息描述,我们在构造函数内可以看到调用了ErrorMessageTools.getErrorMessage(errCode,params);
,这个方法作用是通过异常码在数据库内获取未格式化的异常描述,通过传递的参数进行格式化异常消息描述。
创建异常核心包的目的就是让其他模块直接添加依赖,那异常描述内容该怎么获取呢?
定义异常消息获取接口
我们在springboot-exception-core
模块内添加一个接口LogicExceptionMessage
,该接口提供通过异常码获取未格式化的异常消息描述内容方法,接口定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public interface LogicExceptionMessage {
public String getMessage(String errCode); }
|
在需要加载springboot-exception-core
依赖的项目中,创建实体类实现LogicExceptionMessage
接口并重写getMessage(String errCode)
方法我们就可以通过spring IOC
获取实现类实例进行操作获取数据,下面我们在编写使用异常模块时会涉及到。
格式化异常消息工具类
下面我们再回头看看构造函数格式化异常消息工具类ErrorMessageTools
,该工具类内提供getErrorMessage
方法用于获取格式化后的异常消息描述,代码实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
public class ErrorMessageTools {
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
就可以直接从接口实现类实例中获取到未格式化的异常描述!
当然实现类可以是以Redis
、Map集合
、数据库
、文本
作为数据来源。
获取到未格式化的异常描述后通过String.format
方法以及传递的参数直接就可以获取格式化后的字符串,如:
1 2 3
| 未格式化异常消息 => 用户:%s已被冻结,无法操作. 格式化代码 => String.format("%s已被冻结,无法操作.","恒宇少年"); 格式化后效果 => 用户:恒宇少年已被冻结,无法操作.
|
具体的格式化特殊字符含义可以去查看String.format
文档,如何获取ApplicationContext
上下文对象,请访问第三十二章:如何获取SpringBoot项目的applicationContext对象查看。
我们再回到LogicException
构造函数内,这时errMsg
字段对应的值就会是格式化后的异常消息描述,在外部我们调用getErrMsg
方法就可以直接得到异常描述。
到目前为止,我们已经将springboot-exception-core
模块代码编码完成,下面我们来看下怎么来使用我们自定义的业务逻辑异常并且获取格式化后的异常消息描述。
异常示例模块
基于parent
我们来创建一个名为springboot-exception-example
的子模块项目,项目内需要添加一些额外的配置依赖,当然也需要将我们的springboot-exception-core
依赖添加进入,pom.xml
配置文件内容如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <dependencies> <dependency> <groupId>com.hengyu</groupId> <artifactId>springboot-exception-core</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <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> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.6</version> </dependency> </dependencies>
|
下面我们来配置下我们示例项目application.yml
文件需要的配置,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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: show_sql: true format_sql: true
|
在上面我们有讲到LogicExceptionMessage
获取的内容可以从很多种数据源中读取,我们还是采用数据库来进行读取,建议正式环境放到redis
缓存内!!!
异常信息表
接下来在数据库内创建异常信息表sys_exception_info
,语句如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| DROP TABLE IF EXISTS `sys_exception_info`; ; ; 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='系统异常基本信息'; ;
LOCK TABLES `sys_exception_info` WRITE; ; INSERT INTO `sys_exception_info` VALUES (1,'USER_NOT_FOUND','用户不存在.'),(2,'USER_STATUS_FAILD','用户状态异常.'); ; UNLOCK TABLES;
|
我们通过spring-data-jpa
来实现数据读取,下面对应数据表创建对应的Entity
。
异常信息实体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
@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; }
|
异常信息数据接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public interface ExceptionRepository extends JpaRepository<ExceptionInfoEntity,Integer> {
ExceptionInfoEntity findTopByCode(String code); }
|
在数据接口内通过spring-data-jpa
方法查询方式,通过errCode
读取异常信息实体内容。
在开发过程中异常跑出时所用到的errCode
一般存放在枚举类型或者常量接口内,在这里我们选择可扩展相对来说比较强的枚举类型
,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public enum ErrorCodeEnum {
USER_NOT_FOUND,
USER_STATUS_FAILD, }
|
异常码枚举内容项是需要根据数据库异常信息表对应变动的,能够保证我们在抛出异常时,在数据库内有对应的信息。
LogicExceptionMessage实现类定义
我们在springboot-exception-core
核心模块内添加了LogicExceptionMessage
接口定义,需要我们实现该接口的getMessage
方法核心模块,这样才可以获取数据库内对应的异常信息,实现类如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
@Component public class LogicExceptionMessageSupport implements LogicExceptionMessage {
@Autowired private ExceptionRepository exceptionRepository;
@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>
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@Data @Builder public class ApiResponseEntity<T extends Object> {
private String errorMsg;
private T data; }
|
在ApiResponseEntity
实体内,采用了Lombok
的构造者设计模式@Builder
注解,配置该注解的实体会自动在.class
文件内添加内部类实现设计模式,部分自动生成代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| 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项目中的全局异常处理](https:
#### 全局异常通知定义 我们本章节仅仅添加业务逻辑异常的处理,具体编码如下所示: ```java
@ControllerAdvice(annotations = RestController.class) @ResponseBody public class ExceptionAdvice {
Logger logger = LoggerFactory.getLogger(this.getClass());
@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
的赋值。
测试
异常相关的编码完成,下面我们来创建一个测试的控制器模拟业务逻辑发生时,系统是怎么做出的返回?
测试控制内容如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
@RestController public class IndexController {
@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
数据,下面我们运行项目访问查看效果。
界面输出内容如下所示:
1 2 3 4
| { "errorMsg": "用户状态异常.", "data": null }
|
而在控制台由于我们编写了日志信息,也同样有对应的输出,如下所示:
1 2 3 4 5 6 7 8 9 10 11
| 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 2018-01-09 18:54:00.649 ERROR 2024
|
如果业务逻辑异常在Service
层时,我们根本不需要去操心事务回滚的问题,因为LogicException
本身就是运行时异常,而项目中抛出运行时异常时事务就会自动回滚。
我们把业务逻辑异常屏蔽掉,把true
改成false
查看正确时返回的格式,如下所示:
1 2 3 4
| { "errorMsg": null, "data": "this is index mapping" }
|
如果想把对应的null
改成空字符串,请访问查看第五章:配置使用FastJson返回Json视图。
总结
本章将之前章节的部分内容进行了整合,主要是全局异常、统一格式返回等;这种方式是目前我们公司产品中正在使用的方式,已经可以满足平时的业务逻辑异常定义以及返回,将异常消息存放到数据库
中我们可以随时更新提示内容,这一点还是比较易用的。