Expressive Exception: expressive-exception-spring-mvc

Exception handler for spring boot 2.1.* (spring mvc 5.1.*) with Expressive Exception

License

License

Categories

Categories

Spring MVC User Interface Web Frameworks
GroupId

GroupId

com.yangxiaochen
ArtifactId

ArtifactId

expressive-exception-spring-mvc
Last Version

Last Version

1.3.1-RELEASE
Release Date

Release Date

Type

Type

jar
Description

Description

Expressive Exception: expressive-exception-spring-mvc
Exception handler for spring boot 2.1.* (spring mvc 5.1.*) with Expressive Exception
Project URL

Project URL

https://github.com/yxc023/expressive-exception
Source Code Management

Source Code Management

https://github.com/yxc023/expressive-exception

Download expressive-exception-spring-mvc

How to add to project

<!-- https://jarcasting.com/artifacts/com.yangxiaochen/expressive-exception-spring-mvc/ -->
<dependency>
    <groupId>com.yangxiaochen</groupId>
    <artifactId>expressive-exception-spring-mvc</artifactId>
    <version>1.3.1-RELEASE</version>
</dependency>
// https://jarcasting.com/artifacts/com.yangxiaochen/expressive-exception-spring-mvc/
implementation 'com.yangxiaochen:expressive-exception-spring-mvc:1.3.1-RELEASE'
// https://jarcasting.com/artifacts/com.yangxiaochen/expressive-exception-spring-mvc/
implementation ("com.yangxiaochen:expressive-exception-spring-mvc:1.3.1-RELEASE")
'com.yangxiaochen:expressive-exception-spring-mvc:jar:1.3.1-RELEASE'
<dependency org="com.yangxiaochen" name="expressive-exception-spring-mvc" rev="1.3.1-RELEASE">
  <artifact name="expressive-exception-spring-mvc" type="jar" />
</dependency>
@Grapes(
@Grab(group='com.yangxiaochen', module='expressive-exception-spring-mvc', version='1.3.1-RELEASE')
)
libraryDependencies += "com.yangxiaochen" % "expressive-exception-spring-mvc" % "1.3.1-RELEASE"
[com.yangxiaochen/expressive-exception-spring-mvc "1.3.1-RELEASE"]

Dependencies

compile (1)

Group / Artifact Type Version
com.yangxiaochen : expressive-exception-core jar 1.3.1-RELEASE

runtime (1)

Group / Artifact Type Version
org.slf4j : slf4j-api jar 1.7.25

Project Modules

There are no modules declared in this project.

A More Expressive Exception Library

A More Expressive Exception Library.

Build an exception with optional fields: code, message, tip, level, data.

一个能够包含更多信息的异常基础库. 是一套异常设计和处理的方法论的落地.

如何抛出一个异常

  1. 继承 BaseExprException

    public class ServiceException extends BaseExprException {
        // ...
    }
  2. 抛出个富有表达性的异常吧

    throw new ServiceException("Order not found")
                    .code("ORDER_NOT_FOUND")
                    .tip("订单不存在, 请确认订单号是否正确")
                    .data(new HashMap<String, String>(){{put("key", "value");}})
                    .var("orderId", 1002)
                    .var("time", new Date())
                    .serviceLevel();
  3. 异常打印出来信息是这样

    com.yangxiaochen.exception.test.application.exception.ServiceRuntimeException: [ORDER_NOT_FOUND] Order not found, tip: 订单不存在, 请确认订单号是否正确, ctxVars: {orderId=1002, time=Thu Feb 25 17:22:09 CST 2021}
  4. 如果 web 层捕获这个异常, 我们可以:

    1. code: ORDER_NOT_FOUND 作为返回 code - 给程序读

    2. tip: 订单不存在, 请确认订单号是否正确 作为弹出提示 - 给用户读

    3. ctxVars: orderId=1002message: Order not found 为排查问题提供更多的信息 - 给程序员读

动机

在业务项目实践中, 异常经常用来传递一些业务错误或者警告.

通常, 这些业务错误和警告, 经常要包含更多的信息, 比如错误编码, 错误消息. 有时为了给用户更好体验, 还会放入一些便于用户阅读的消息. 甚至, 还会需要一些数据.

设计意图

在多个业务系统实践中, 我做了一个总结, 一个好用的异常, 要包含以下几个数据:

  • message - 异常都会包含的消息

  • code - 异常编码

  • tip - 异常提示

  • data - 可选, 异常携带的数据

  • level - 可选, 异常级别

下面对每一项进行详细说明.

message

通常意义下的 exception message, 通常是对异常的描述. 比如当要删除一个订单, 但给的订单号并不存在时:

Order not found, id: 1001

通常是英文, 且格式标准专业, 包含了异常相关足够的信息.

code

因为业务比较复杂, 异常情况也很多, 我们基本不会对每一种异常设计一个异常类型. 比如在处理订单操作时, 我们只定义一种异常类型: OrderOperationException.

那么更细节的异常我们可以通过编码来表示:

SUCCESS - 成功都是相同的

// 而失败各有不同
FAILURE - 通用的失败编码
ORDER_NOT_FOUND - 订单不存在
ORDER_HAD_PAYED - 订单状态异常: 已经支付过了
...

code 使用字符串, 好处是更易读.

tip

tipmessage 很像, 都是用来表达异常的信息. tip 的设计意图在于提供用户可读的异常信息. 比如

要操作的订单号[1001]不存在.
订单[1001]已经支付过了.

data

data 的作用是与请求成功响应时返回的数据项对齐.

在发生异常时, data 其实并不常用, 场景比较少. 只是在发生异常时需要返回一些关联数据. 举一个场景:

当用户购买一个比较抢手的产品时, 有一个购买限制: 一个用户下单后必须支付才能下第二单.

那么, 当用户触发这个限制时, 返回的异常中要包含未支付的订单号, 再由统一的异常处理转换成带有 data 的异常返回信息.

level

异常为什么要分级? 因为我在业务逻辑中, 所有不符合最常规业务逻辑流程的, 都使用异常来返回.

那么有的异常可能并不算是错误. 比如登录时账号密码不匹配, 这并不是系统 bug 引起的错误, 也不需要记录 error 日志, 甚至报警.

而有的, 比如逻辑执行中, 某个数据一定应该存在的, 结果没有查询到, 代表着数据完整性异常, 那么这是真真正正的 error.

而其他的, 甚至还有说偶尔异常没问题, 大量异常有问题的. 比如客户端断开连接, 偶尔出现很正常, 但大量出现就是有问题的.

所以在设计中, 默认将异常 level 分为了两类:

  • SERVICE_LEVEL

  • ERROR_LEVEL

USAGE

引入

dependencies {
    compile 'com.yangxiaochen:expressive-exception-core:1.2.1-RELEASE'
}

使用

exception-core 中, 提供了 HasTip, HasCode, HasData, HasLevel 几个接口, 你需要定义自己的异常类时:

public class MyException extends Exception implements HasTip, HasCode, HasData, HasLevel {
    ...
}

为了方便定义异常类, 提供了两个抽象类 BaseExprException, BaseExprRuntimeException, 可以直接继承这两个类:

public class MyException extends BaseExprException {
    ...
}

打印出的异常 log 例子:

com.yangxiaochen.exception.test.application.exception.ServiceRuntimeException: [SERVICE_EXCEPTION] default service exception, tip: 默认业务异常, ctxVars: {fooId=1002, time=Wed Aug 21 18:17:26 CST 2019}
...

拓展异常 level

可以通过实现 ExceptionLevel 来定义新的异常 level.

如何处理异常

异常定义只是一个方面, 如何看待, 解释, 处理我们定义的异常是另一个方面.

spring mvc

dependencies {
    compile 'com.yangxiaochen:expressive-exception-spring-mvc:1.2.1-RELEASE'
}

提供了一个默认的 ExceptionHandler, 来统一处理异常, 其核心异常处理方法实现如下:

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    if (pathPrefixs.stream().noneMatch(prefix -> request.getRequestURI().startsWith(prefix))) {
        return null;
    }

    ex = translateException(ex, request);
    if (ex == null) {
        return null;
    }
    logAction.log(request, ex);

    return errorViewResolver.resolve(request, response, ex);
}

加入到 spring mvc 框架中实现异常的统一处理:

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    private boolean printStack = false;
    private MappingJackson2JsonView view = new MappingJackson2JsonView();

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        ExceptionHandler exceptionHandler = new ExceptionHandler();
        resolvers.add(0, exceptionHandler);
    }
}

可以对 ExceptionHandler 的处理行为进行定制:

exceptionHandler.setPathPrefixs(Arrays.asList("/web/", "/api/"));
exceptionHandler.setErrorViewResolver((request, response, ex) -> {
    ModelAndView mv = new ModelAndView();
    mv.addObject("msg", ex.getMessage());
    mv.addObject("success", false);
    if (ex instanceof HasCode) {
        mv.addObject("code", ((HasCode) ex).getCode());
        if (((HasCode) ex).getCode() == null) {
            mv.addObject("code", 0);
        }
    }
    if (ex instanceof HasTip) {
        mv.addObject("tip", ((HasTip) ex).getTip());
        if (ex.getMessage() == null) {
            mv.addObject("msg", ((HasTip) ex).getTip());
            mv.addObject("message", ((HasTip) ex).getTip());
        }
    }
    if (ex instanceof HasData) {
        mv.addObject("data", ((HasData) ex).getData());
    }
    if (printStack) {
        mv.addObject("stackTrace", getStackFrames(ex));
    }
    mv.setView(view);
    return mv;
});

dubbo

意见收集

这个项目即是一个类库, 更是一个异常设计和处理的方法论, 类库是方便方法论落地的措施.

如果你有不同的想法和意见, 欢迎 issue 交流.

Versions

Version
1.3.1-RELEASE
1.3.0-RELEASE
1.2.1-RELEASE
1.2.0-RELEASE
1.1.2-RELEASE
1.1.0-RELEASE