Java 工程师编码规范和注意事项

163 阅读8分钟

前言

本文档内容仅为个人经验和总结,不能保证完全是正确的或合理的,请根据个人习惯适当调整。

编码规范

黄金准则

不要整个文件格式化,选中自己提交的指定代码进行格式化(代码格式化算 change,影响 review)

此处为语雀视频卡片,点击链接查看:iShot_2022-10-18_13.46.57.mp4

快速失败,不要让异常逃离在所属服务/职责之外,流转到下游

举个栗子

用户通过前端页面提交参数,后端 web 服务接口没有对参数做足够的校验,让不合法的参数存储到了数据库。营销服务通过内部 rpc 服务获取该参数时,触发了异常,导致一些列问题等。

这个异常完全可以在后端 web 服务接口做参数校验,对于不合法的参数不予处理,直接将错误告诉用户。

方法实现尽可能的精简,独立,清晰,职责单一

方法实现尽可能简短,能够做到和方法名一样。如果代码和方法名无关,可以抽离出单独的方法。

比如下面的代码,方法定义为doSendReply意为 “发送所有pending的reply”。但是在发送完回复后,还做了 session 相关的操作,那么 session 更新和发送回复无关,即可抽离成单独的 updateSession类似的方法,意为“更新 session”

善用可变和不可变对象

可变对象不可变对象
特点可读可写,灵活,多线程读写不安全,如需线程安全要加锁只读,可复用,线程安全,语义明确

java 集合

合理的接口定义,会让我们的代码具有层次感和辨识度。比如 Java 中最经典的 Collection 接口及其子实现类。

如图所示:

这些集合类都是由其数据结构特性来定义的

  • Set 不可存重复元素的集合
    • HashSet 元素是分散的、无序的
    • TreeSet 元素是有序的,由其内部 comparator实现元素的顺序,为 null 则为自然插入的顺序
  • Queue 队列,以插入为顺序,先插入的元素先出队
  • Deque 双端队列,可从双侧插入和出队
  • List 可存重复元素的列表

以上接口和实现类默认都是可变的,因为都提供了写方法(set/add 等等)但是在实际编码中,我们可能需要一个不可变的集合(常量)。

比如查询中国所有的行政区编码,并不需要每次都去数据库查询数据返给前端。可以将其缓存在服务器本机内存,大大提高查询效率,同时为了避免该集合被其它开发人员修改,我们可以将其定义成不可变的集合。

guava 的集合

比如下图,guava 实现的不可变集合(理论上不应该定义写方法)但是,由于父类 Collection 接口提供了写方法,实现类只能在写方法抛运行时异常。

kotlin 的接口设计

直接从接口层面区分了可变和不可变,可变的提供了add``remove等写方法,不可变的则只提供了 get等读方法

git 分享

cherry-pick

git cherry-pick可以理解为精选、挑选,功能是合并其它分支或 commit 到当前分支,相对于 merge 会更加灵活,因为可以指定单个或多个 commit 进行合并。

举个栗子

🌰 bugfix(dialogue): 解决 session 序列化 MatchedScenario 问题

比如这个 mr, 是为了修复 dialogue 中 gson 对于 proto 对象序列化不兼容的问题。因为乐于助人团队已经修复过该 bug 了,我们可以用 cherry-pick 将其修复的 commit 合并到我们所需的分支,减少重复工作。

🌰 feat(store-home): 拼多多时间组同步优化

比如这个 MR, 时间组优化这个功能,在开发时提交了多个 commit,然后良军在其它分钟也有个小的 commit, 我用 cherry-pick 将他的提交合并到我的分支,再统一合到主分支,因为是两个功能(时间组优化和订单质保期限制)所以我将之前提交的时间组优化的 commit 合并成一个 commit,看起来更加清晰

类似的还有 feat(marketing-home): 营销配置同步

举个反栗

提交多个 commit 的备注信息一样,区分度太低,不清晰,对于 review 的人不友好,提高了后续代码复用或审查的难度。

worktree

当某个迭代,多名开发者在同一个仓库进行开发时,我们可能会遇到以下几个问题

  1. 如果在同一个分支开发,增加了代码冲突的概率,每次提交和拉代码都可能需要解决冲突
  2. 如果按照功能划分,每个功能一个分支,那么在发布到测试环境时,可能会出现代码覆盖

所以针对代码冲突和覆盖,我们通常又会有以下约定

  1. 每个功能一个分支,比如开发者 A 在 featA 分支上开发, 开发者 B 在 featB 分支上开发,以此类推, 独自开发和维护
  2. 再约定一个测试主分支,每个开发者都将自己的代码合并到测试分支,然后用该分支进行发布,避免了代码覆盖
  3. 如果 A 功能 delay ,B 功能可以正常发布,那么只要将 B 分支合并到生产主分支即可,这样就可以通过分支解耦来实现灵活发布和回滚

但是针对以上的约定,对于开发人员本地来说,可能会出现以下两种操作

  1. 本地只有一份仓库,提交代码
  2. 本地克隆两份仓库A, 一份是开发分支,一份是测试分支,来回切换进行代码合并

本地只有一份仓库,提交代码

  • 本地克隆一份仓库,多分支合并代码,来回切换分支

此处为语雀视频卡片,点击链接查看:多分支合代码.mp4

  • 本地克隆一份仓库,多分支合并部分代码,当前开发分支暂存不需要提交的代码,来回切换分支

此处为语雀视频卡片,点击链接查看:多分支合代码,本地暂存.mp4

本地克隆两份仓库

本地克隆了两份仓库,存储在不同的路径。

此处为语雀视频卡片,点击链接查看:本地克隆两份项目.mp4

使用 worktree

使用 worktree 只需要本地克隆一份仓库

如下面的视频所示,先 git worktree add <path> <branch>新建一个工作区,然后 ide 打开该工作区,即可合并其它分支代码到所需的分支

<path>分支存储的本地路径,可以是绝对或相对路径

<branch>分支名

此处为语雀视频卡片,点击链接查看:使用 worktree 管理多分支.mp4

此处为语雀视频卡片,点击链接查看:iShot_2022-10-18_13.38.11.mp4

关于 kafka

主要说下 kafka 消费者的几个配置,因为消费者是我们开发者最常用也是最容易出问题的一端。

自动提交偏移量

我们在使用 kafka 消费者时,通常将 enable.auto.commit设为 true即消费者端自动提交偏移量,间隔时间配置auto.commit.interval.ms默认 5 秒。

举个栗子,如果消费者 A 从 broker 拉取了 10 条消息进行消费,但是由于耗时较长,超过 5 秒没有自动提交偏移量到 broker,broker 则认为该消费者没有正常消费,随后这 10 条消息将会被重新消费(有可能是其他消费者)

如果配置和使用不当,可能导致消费者重复拉取和消费,从 garafana 看起来是消费很慢或没有消费的假象

单次拉取配置

max.poll.records控制单次拉取的最大消息条数(默认 500),如果消费者自身有不错的消息处理速度,那么这个值越大吞吐则越大

fetch.max.wait.ms消费者线程在向 broker 拉取消息时,自身是阻塞的,这个即最大等待时长

fetch.min.bytes单次拉取最小字节,配合fetch.max.wait.ms使用,满足任意条件即返回数据给消费者

针对上述配置,建议配合监控、消息大小和实际场景等因素(比如 galaxy 服务允许消息堆积在 kafka 慢点消费)

进行调优

如下图所示,拼多多兜底 question 消费 dialogue-grpc-session-all-in-one-pdd的kafka 消息,之前的消费者收到消息延迟大约在 100 毫秒左右(消费者收到消息时间 - 生产者发出消息时间),经过调整消费者参数,延迟降低到了 25 毫秒左右

不要再额外阻塞消费者

正如 单次拉取配置 提到的,kafka 消费者在向 broker 请求消息拉取时,已经做了阻塞,所以不要在代码侧做不必要的优化(避免频繁拉取空的消息,休眠一段时间

如下图所示,兜底回复下发量比较小(最大 qps=6)所以在会经常出现拉取不到消息的情况,然后本地休眠。

去除不必要的休眠 后,消费者收到消息延迟降低到 50 毫秒左右

参考文档

git cherry-pick 教程 - 阮一峰的网络日志

再也不用克隆多个仓库啦!git worktree 一个 git 仓库可以连接多个工作目录

kafka docs