异常信息
异常信息
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-utilities的I18N服务,很好的支持了国际化。
注
本案例中 ErrorCode 和 Service 在同一个包下,如果你的工程分开维护,记得把 ErrorCodes 的包或者类也注册到 Application 中
进阶
Concrete的异常模板使用的是coodex数据渲染器,如果你需要对 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;
    //....
}
// ....
好吧,实际上怎么划分无所谓,只要不重复,用起来方便,其他看心情就好。我们还是推荐团队做好约定。