异常信息

Davidoff Shen2022年5月11日大约 6 分钟

异常信息

Concrete的理念是,服务根据传入数据给出结果,我们称之为“正常”,其他情况都是异常,每个异常给定一个异常码和异常信息,由调用者决定如何处理。

我们通过一个示例来进行说明。

示例

我们设计一个新的 AddService,它负责处理个位数加法,超出范围时,给调用者返回一个太难了的异常信息。

demo-api模块中,定义一个DemoErrorCodes类,用来放异常码常量

package org.coodex.concrete.demo.api;

import org.coodex.concrete.api.ErrorCode;
import org.coodex.concrete.api.ErrorCodeDef;

import static org.coodex.concrete.common.ErrorCodeConstants.CUSTOM_LOWER_BOUND;

@ErrorCode("demo")// 声明此类型是错误码定义,相关定义的命名空间为demo
public class DemoErrorCodes implements ErrorCodeDef {
    protected static final int DEMO_BASED = CUSTOM_LOWER_BOUND + 5000;

    @ErrorCode.Key("too_hard")// 指明消息模板的key为 demo(命名空间).too_hard
    public static final int TOO_HARD = DEMO_BASED + 1;
}

提示

实现org.coodex.concrete.api.ErrorCodeDef并放入java SPI中,可达到自动注册异常码。

新建十以内加法服务

package org.coodex.concrete.demo.api;

import org.coodex.concrete.api.ConcreteService;
import org.coodex.concrete.api.Description;
import org.coodex.util.Parameter;

@ConcreteService
@Description(name = "十以内加法")
public interface AddWithIn10Service {
    Integer add(@Parameter("x1") Integer x1, @Parameter("x2") Integer x2);
}

demo-api的资源目录中i18n目录下定义异常信息模板

  • demo.yml
demo:
  too_hard: "{0} + {1} is too hard ~>_<~"
  • demo_zh_CN.yml
demo:
  too_hard: "{0} + {1} 太难了 ~>_<~"

demo-impl模块增加实现。先增加concrete-core的依赖,在concrete-core中,对 ErrorCode 规范有一套默认实现,开箱即用。

<dependency>
    <groupId>org.coodex</groupId>
    <artifactId>concrete-core</artifactId>
</dependency>

实现类

package org.coodex.concrete.demo.impl;

import org.coodex.concrete.common.IF;
import org.coodex.concrete.demo.api.AddWithIn10Service;

import javax.inject.Named;

import static org.coodex.concrete.demo.api.DemoErrorCodes.TOO_HARD;

@Named
public class AddWithIn10ServiceImpl implements AddWithIn10Service {
    @Override
    public Integer add(Integer x1, Integer x2) {
        // IF是concrete工具链提供的工具之一
        // 下面的接口就是说,如果满足条件(参数1),则抛出TOO_HARD异常(参数2),并传入信息渲染的参数(参数3,可变参数)
        IF.is(x1 < 0 || x1 > 9 || x2 < 0 || x2 > 9, TOO_HARD, x1, x2);
        return x1 + x2;
    }
}

提示

IF 是 concrete 工具链提供的工具之一,点我查看

还有个工作,上一步中,我们开启了 mock,因此需要把这个服务例外出来

demo-boot的 test 作用域中的mock.excepted中增加

org.coodex.concrete.demo.api.AddWithIn10Service

跑起来,然后用 swagger 试试

x1=1,x2=2,结果是正确的,3

x1=10,x2=2,结果如下:

{
  "code": 105001,
  "msg": "10 + 2 太难了 ~>_<~"
}

切换一下浏览器的语言顺序,把英语提前,你会看到信息变成了

{
  "code": 105001,
  "msg": "10 + 2 is too hard ~>_<~"
}

concrete的 errorCode 机制使用了coodex-utilitiesI18Nopen in new window服务,很好的支持了国际化。

本案例中 ErrorCode 和 Service 在同一个包下,如果你的工程分开维护,记得把 ErrorCodes 的包或者类也注册到 Application 中

进阶

Concrete的异常模板使用的是coodex数据渲染器open in new window,如果你需要对 freemarker 模板的支持,需要依赖org.coodex:coodex-renderer-freemarker

我们再拿一个 service 专门演示各种 ErrorCode 的使用。

  • 新建 service
package org.coodex.concrete.demo.api;

import org.coodex.concrete.api.ConcreteService;
import org.coodex.concrete.api.Description;
import org.coodex.concrete.demo.pojo.CarInfo;

@ConcreteService
public interface ErrorCodeService {

    @Description(name = "无配置")
    void noneAnnotation();

    @Description(name = "使用Template注解")
    void templateAnnotation();

    @Description(name = "freeMarker模板")
    void freeMarker(CarInfo carInfo);
}

  • 增加 errorCode 定义

    @ErrorCode.Key("too_hard")// 指明消息模板的key为 demo(命名空间).too_hard
    public static final int TOO_HARD = DEMO_BASED + 1;

    // 不指定注解,则表示使用 命名空间.错误码 作为模板key
    public static final int NONE_ANNOTATION = DEMO_BASED + 2;

    @ErrorCode.Key("not_exists")
    public static final int NOT_EXISTS = DEMO_BASED + 3;

    @ErrorCode.Template("使用模板注解的例子,参数为: {0}")
    public static final int TEMPLATE_ANNOTATION = DEMO_BASED + 4;

    @ErrorCode.Template("使用freeMarker的例子: ${o1.ownerName} 驾驶着 ${o1.plateCode} 跑过去了")
    public static final int FREE_MARKER_EXAMPLE = DEMO_BASED + 5;
  • demo_zh_CN.yml 中增加 NONE_ANNOTATION(demo.105002)的模板
demo:
  too_hard: "{0} + {1} 太难了 ~>_<~"
  105002: 我是105002错误信息
  • demo-impl 中增加实现
package org.coodex.concrete.demo.impl;

import org.coodex.concrete.common.ConcreteException;
import org.coodex.concrete.common.IF;
import org.coodex.concrete.demo.api.ErrorCodeService;
import org.coodex.concrete.demo.pojo.CarInfo;

import javax.inject.Named;

import static org.coodex.concrete.demo.api.DemoErrorCodes.*;

@Named
public class ErrorCodeServiceImpl implements ErrorCodeService {

    @Override
    public void noneAnnotation() {
        throw new ConcreteException(NONE_ANNOTATION);
    }

    @Override
    public void templateAnnotation() {
        throw new ConcreteException(TEMPLATE_ANNOTATION, "11111");
    }

    @Override
    public void freeMarker(CarInfo carInfo) {
        throw new ConcreteException(
                FREE_MARKER_EXAMPLE,// 使用下面返回的carInfo作为模板渲染的参数
                IF.isNull(carInfo, NOT_EXISTS)// 当carInfo为空时,走NOT_EXISTS异常,否则返回carInfo
        );
    }
}
  • demo-boot中例外org.coodex.concrete.demo.api.ErrorCodeService,并增加org.coodex:coodex-renderer-freemarker依赖
<dependency>
    <groupId>org.coodex</groupId>
    <artifactId>coodex-renderer-freemarker</artifactId>
</dependency>

跑起来。我们针对这四种情况分别在 swagger 中分别尝试一下:

无配置

{
  "code": 105002,
  "msg": "我是105002错误信息"
}

没有对错误码声明注解的,将使用错误号的命名空间.错误码值作为模板 key 来渲染。命名空间(@ErrorCode 的 value)默认为message

注意,声明了 ErrorCode 但是没有被注册到 Concrete 中,将无法读取错误的配置信息,按照默认行为指定模板 key,也就是message.错误码值

使用 Template 注解

{
  "code": 105004,
  "msg": "使用模板注解的例子,参数为: 11111"
}

可以看到,此错误信息直接使用了@ErrorCode.Template 注解的内容作为模板渲染了错误细腻

freeMaker 模板

{
  "code": 105005,
  "msg": "使用freeMarker的例子: 阎庞 驾驶着 皖B3193F 跑过去了"
}

上述错误信息按照使用freeMarker的例子: ${o1.ownerName} 驾驶着 ${o1.plateCode} 跑过去了进行了渲染

把请求内容置空后

{
  "code": 105003,
  "msg": "demo.not_exists"
}

这个错误码定义了@ErrorCode.Key,但是资源文件中并没有这个定义,所以直接使用 key 进行渲染

最佳实践

虽然说上面的例子各种情况都可以很好的运行,但是不同模式在实践方面还是有一定的不足:

  • 不配置,或配置了不注册到 concrete 运行中:资源文件只能按照命名空间.错误码值方式来组织,可读性差
  • 使用@ErrorCode.Template 注解:与代码紧耦合,扩展性差,无法 I18N

因此,我们推荐使用@ErrorCode.Key 对每个错误码进行可读性注解,并且把 ErrorCode 类都注入到 concrete 环境中

团队协作如何减少冲突?

按模块划分

public class ProjectErrorCodes{
    //非具体错误号不要用public
    protected static final int MODULE1 = CUSTOM_LOWER_BOUND;

    protected static final int MODULE2 = MODULE1 + 5000;

    // ....
}

@ErrorCode("module1")
public class Module1ErrorCodes extends ProjectErrorCodes{

    public static final int ERROR1 = MODULE1 + 1;

    //....
}

// ....

团队协作时按人划分

public class ProjectErrorCodes{

    //非具体错误号不要用public
    protected static final int 西门吹雪 = CUSTOM_LOWER_BOUND;

    protected static final int 陆小凤 = 西门吹雪 + 5000;

    // ....
}

@ErrorCode("ximenchuixue")
public class 西门吹雪ErrorCodes extends ProjectErrorCodes{

    public static final int 挂点 = 西门吹雪 + 1;

    //....
}

// ....

好吧,实际上怎么划分无所谓,只要不重复,用起来方便,其他看心情就好。我们还是推荐团队做好约定。

上次编辑于:
贡献者: Davidoff Shen
Loading...