Skip to main content

断言 + 异常机制 = 清晰简洁的代码

介绍

tip

断言 + 异常机制 = 清晰简洁的代码

业务框架支持异常机制,有了异常机制可以使得业务代码更加的清晰。 也正是有了异常机制,才能做到零学习成本(让普通的 java 方法成为一个业务动作 action )。

如果有业务上的异常,请直接抛出去,不需要开发者做过多的处理,业务框架会知道如何处理这个业务异常,这些抛出去的业务异常总是能给到游戏的请求端的。

异常示例

GameCode

我们定义了一个枚举类 GameCode,用于存放所有的错误码。 枚举类实现了框架提供的 MsgExceptionInfo 接口, 该接口提供了一系列的 assert_xxx 方法。

@Getter
public enum GameCode implements MsgExceptionInfo {
emailChecked(100, "Email error.");

final int code;
final String msg;

GameCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}

断言示例

该示例是一个标准的 action,由一个请求和一个响应构成。 示例代码会检测请求参数 email 属性格式是否正确,当 email 格式不正确时,会将这个异常码抛出。

@ActionController(10)
public class DemoAction {
@ActionMethod(1)
public boolean login(String email) {

String email = loginVerify.email;

Pattern pattern = Pattern.compile("[a-zA-Z0-9_-]+@\\w+\\.[a-z]+(\\.[a-z]+)?");
var checkedResult = pattern.matcher(email).find();
GameCode.emailChecked.assertTrue(checkedResult);

// custom error info. cn: 自定义错误信息
// GameCode.emailChecked.assertTrue(checkedResult, "This email is wrong!");

return true;
}
}
tip

可以看出,使用枚举(断言)的编码方式,使得代码非常的清晰。 如果有新的异常业务,就在 GameCodeEnum 枚举中新增变量。 在业务框架的 action 中抛出的异常会被捕获,并把异常消息发送到请求方。

warning

业务框架的异常机制,只保存错误码,不会保存错误码对应的异常信息,这是因为:从字段精简的角度,我们不可能每次响应都带上完整的异常信息给客户端排查问题,因此,我们会定义一些响应码,通过编号进行网络传输,方便客户端定位问题

如果想把异常信息携带到请求端(游戏前端),可以参考自定义 ActionAfter

异常码会放在什么地方

由于我们传入的值是 email=a,这并不是一个合法的邮箱,所以控制台会打印如下信息。

┏━━不符合验证━━━ Debug [.(DemoAction.java:5).login] ━━━ [cmd:10 - subCmd:1 - cmdMerge:65537]
┣ 参数: email : a
┣ 错误码: 100
┣ 验证信息: Email error.
┣ 时间: 0 ms (业务方法总耗时)
┗━━━━━ Debug [DemoAction.java] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

业务框架会将 action 抛出的错误码放到 ResponseMessage.responseStatus 并给到对外服协议, 最终会存放到 ExternalMessage.responseStatus。

ExternalMessage 是游戏对外服协议

断言 + 异常机制对性能会有影响吗?

see https://github.com/iohao/iogame/issues/53

在项目中让业务代码表现得很清晰,问下异常机制使用过多对性能会有影响吗?

在实际应用中,不会有性能上的影响。 但似乎这么回答又过于简单,所以这里会拆解分析,将问题理论联系实际。

代码片段

我们先看两个代码片段

// 片段1
int i = 1 + 1;

// 片段2
try {
int i = 1 + 1;
} catch(Throwable e) {
}

代码逻辑相同时,在不触发异常的情况下,两者之间的性能差异是可忽略的,这点可以通过 benchmark 来测试验证。 在触发异常时会略有微微的影响,基本可以忽略。

小故事

我们在分析问题时,不能本本主义,这样容易谬误。 这里举个谬误的小故事,可能你之前听过或在小视频中刷到过《饮用反复烧开的水会致癌》这样的内容,这是谬误之一。 为什么说这个内容是谬误呢?因为内容中只谈结论而没有谈量的问题,要知道只有量变才会引起质变。

卤味吃过吧,无限循环锅里反复煮,也没见吃卤味的人怎么样。 还有那句怎么说来的,拋开什么谈毒性来着。 这里不要杠,杠就你赢

这个故事想表达的是,在分析问题时,要将这个方面纳入到综合分析中。

框架的异常机制

业务框架会在 action 的业务上层做一个 try Throwable 的工作,目的是捕获所有异常。

了为防止触发异常机制,框架提供了 JSR380 验证支持,当触发 JSR380 时。 不会进入 action 业务方法,也不会进入框架 try 的这部分逻辑。


理论联系实际

从上面的代码片段中我们可以知道在没有触发异常时,是不会有性能方面的影响的。 在触发异常时会有微微的影响,但也可以忽略,那么忽略是理由是什么?

忽略的理由是理论联系实际,为了说明这点我们先设定一个业务场景,玩家购买一个商品需要 500 金币。伪代码如下

@ActionController(1)
public class DemoAction {
@ActionMethod(0)
public boolean buyGoods(FlowContext flowContext) {
long userId = flowContext.getUserId();
long gold = getGoldByUserId(userId);
// When the assertion is true, an exception is thrown
// cn: 断言是 true, 就抛出异常
GameCodeEnum.gold.assertTrueThrows(gold < 500);

...
}
}

通过这段伪代码是不是感觉只要玩家金币小于 500 时就会拋异常。不用感觉,事实的确是这样的。

但别忘了,你还有前端的队友。 假设前端也是编写健壮型代码的开发者,通常会在玩家购买商品时做一个购买前的验证逻辑,所以通常情况下是不会触发的异常机制。 如果前端不具备这样的编程意识,你也不需要担心,这不是你的问题,因为你已经做了你该做的了,做好一个产品需要多方的努力与配合。

在实际中,会碰到三类开发者:懒惰型、敷衍型、积极型

  • 懒惰型:比如部分前端开发者不做任何验证就把请求往服务器扔,这样做似乎可以减少一些工作量。
  • 敷衍型:你说一处,他就改一处。
  • 积极型:尽可能的将验证做好。

也许会有偶尔忘记验证的情况,如果出现这种类似的情况我们该如何改进呢?

这里也顺便分享一下,我们可以扩展业务框架提供的插件机制。 通过插件来实现统计哪些 action 业务方法触发的异常次数较多, 得到统计数据后,将这些统计数据给到前端开发者,让其根据统计数据来做对应的验证优化。

顺便开个玩笑,也会有这么一种情况,即使你给出了统计数据,有部分前端开发者也不会对此做改进。 或许此时他在拿到统计数据时,大脑也在飞速的运转,数秒之后将多条不做改进的理由砸向你。 此时,请认命。

传统框架的错误处理

在一些传统框架中,会将验证逻辑放到实际的业务逻辑代码中。 当有触发验证逻辑时,将错误码主动的推送到游戏前端,这里称为传统写法或祖传代码写法。伪代码如下

public void hello() {

if (gold < 500) {
xxx.send(userId, new TheErrorCode(goldCode));
log.error(goldCode + "-" + userId)
return;
}

if (level < 20) {
xxx.send(userId, new TheErrorCode(levelCode));
log.error(levelCode + "-" + userId)
return;
}

if (age < 18) {
xxx.send(userId, new TheErrorCode(agelCode));
log.error(agelCode + "-" + userId)
return;
}
}

在验证逻辑少时还好,但在实际中验证逻辑往往是有多条的, 这将会不断的在业务代码中疯狂使用 if else 输出,使得业务代码混乱。

当使用 ioGame 提供的 JSR380断言 + 异常机制时,你会发现代码是清晰的,可读性也更强。

GameCodeEnum.gold.assertTrueThrows(gold < 500);
GameCodeEnum.level.assertTrueThrows(level < 20);
GameCodeEnum.age.assertTrueThrows(age < 18);

如果使用传统的写法,你会发现想做对应路由异常触发次数的统计是困难的, 而在 ioGame 业务框架的插件机制中实现这些是简单的。 在插件文档就也提到过,你可以通过该机制做很多事情,这要看你的想象力有多丰富了。

理论联系实际小结

  1. 做好一个产品需要多方的努力,当前端也将数据验证做好了,此类请求也就不会进入到服务端。
  2. 框架提供了 JSR380 验证支持,当触发 JSR380 时。不会进入 action 业务方法,也不会进入框架 try 的这部分逻辑。
  3. 量变质变规律,实际中触发异常机制的次数并不会很多,所以并不会有性能上的影响,这是根据实际统计数据得出的结论。
  4. 远离纯本本主义开发者。

通过理论联系实际得出的结论是与开头的回答一样,在实际中并不会有性能上的影响。 所以请放心的使用这种编码方式,这将使得项目中业务代码更加的清晰,就像阅读文章一样轻松。

tip

代码是给人读的,维护代码的也是人,请不要伤害他人