跳至主要內容

0A.编码风格

trydofor原创神翼规范大约 14 分钟

0A.编码风格

Wings项目实践中,主张防御式编程,秉承以下价值观和哲学,

  • 静态优于动态 - 能编码的,就不要反射
  • 强类型优于弱类型 - 能class或enum,就不要map或const
  • 编译时优于运行时 - 能编译时解决的,就不要到运行时处理
  • IDE优于Editor - 有上下文语法的,就不要字符串查找
  • 奥卡姆剃刀 - 能简单的就不要复杂,能明示的就不要暗示
  • 命名必须明确 - 明确业务语义,可读性优先,不怕长,不怕怪
  • 边界外参数不可信 - 边界层做验证和转换,边界内则都可信

防御式编程,处处考虑边界和异常,May The false Be With You !

0A.1.Java代码可读性优先

遵循标准的java规范(Idea提示即可),但可读性优先。

  • static final 不必全大写。如logLOG可读性好
  • BIG_SNAKE可使用PascalNaming,因为大写单词不如小写易读
  • 全大写名词(缩写或专有)只首字母大写驼峰法。Json,Html,Id
  • 前缀,后缀及缩写,必须2个字母起,建议3个字母(驼峰法)
  • 英文无法明确的行业黑话,可使用中文,但不建议用拼音。
  • 要求4-8字母的单词都记住,命名采用动宾或副词结构。
  • 以Empty消除null,Set/List/Array/Map用empty
  • 显示标注@NotNull,@Nullable,@Contract,声明null约束
  • 正向的业务语义,使用正向的词汇,参数等,如true
  • 使用@Param.InOut/Out及相应的后缀注解可被修改参数

提高编码质量,升级编程技能,请左转至攻城狮的Java代码审查open in new window

0A.2.Sql命名snake_case

即全小写,下划线分割。因实践中发现,小写词比大写容易识别。

  • 数据库,表名,字段名,全小写。
  • SQL关键词,内置词等建议大写,以区别。
  • indexix_,uq_,ft_,pk_区分索引类型。
  • trigger(ai|au|db)__表示触发的时机。

wings主张业务表SQL化,即使用SQL管理表及数据,而GUI或对象映射都是辅助功能。 SQL脚本可以很好的编辑,比较,文档化,包括业务表的分层,编号及注释格式。

  • 编号/名字:解释 - 105/常量枚举:自动生成enum类
  • 字段注释/解释:选项1|选项2 - 验证账号/身份辨识:邮箱|手机|union_id|api_key

编号由业务层规划,如10x为系统,11x为应用,12x为用户,13x为权限,2xx为商品,3xx为订单等。

0A.3.配置类properties优先

尽量使用properties格式,因yml的缩进在局部编辑时,容易出现困扰。

  • 一组有关联的属性,放在一个properties,分成文件便于版本管理
  • wings-enabled.properties用于ConditionalOnProperty配置
    • 统一使用wings.enabled.xxx=true|false格式
    • xxx 可以是 Class/Method 命名规则
  • spring-*放置spring官方配置key
  • wings-*放置wings配置key,
    • 带有工程或模块代号,如wings.slardar.*
    • 提供默认配置,使用-79序号
  • 推荐kebab-caseae命名,即key全小写,使用-分割
  • 说明类型的注释,使用两个单号注释,如Properties中##
  • 功能关闭类的注释,使用单个单行注释,如Properties中#

0A.4.spring注入注意事项

  • 优先使用constructor注入,用lombok@RequiredArgsConstructor
  • 次之使用setter注入,用lombok@Setter(onMethod_ = {@Autowired})kotlin@Autowired lateinit var
  • 尽量避免使用Field注入,坏处自己搜一搜
  • 通常required时constructor注入,optional时setter注入
  • 但注入过多,使参数列表过长,影响理解和使用时, 使用setter注入,加上afterPropertiesSet检查

使用@Resource@Inject@Autowired,有细微差别,

  • Resource由CommonAnnotationBeanPostProcessor处理, 查找顺序为①BeanName ②BeanType ③Qualifier
  • Autowired和Inject由AutowiredAnnotationBeanPostProcessor处理, 查找顺序为①BeanType ②Qualifier ③BeanName
  • type优先用Autowired和Inject,name优先用Resource(细粒度,难控制)
  • 在spring体系下推荐@Autowired,考虑兼容性用@Inject

继承父类时的注入规定(类无法得知是否被继承)

  • 父类中有@Setter注入时,字段以protected替代private
  • 不希望子类覆盖时,需要final setter,避免父类无法注入
  • 继承时,一旦父类有setter,请不要override,除非确保DI无碍
  • 继承时,不希望父类DI,可子类override,并自行注入

0A.5.RequestMapping风格

Url命名主要是场景化的前缀,参考RestHalf

  • 在方法上写全路径@RequestMapping("/a/b/c.html")
  • 在controller上写版本号@RequestMapping("/v1")
  • 不要相写相对路径,这样才可以通过URL直接搜索匹配
  • 不要使用prefix拼接路径(view,url),避免无意义的碎片
  • 不管REST还是其他,url必须有扩展名,用来标识MIME和过滤

0A.6.Service和Dto约定

interface上使用annotation时,遵循以下规则,

  • @Component类注解,不要放在接口上,放在具体实现上
  • 功能约定类,放在接口上,如@Transactional

Service定义为接口,Service中的Dto,定义为内类,作为锲约。 Dto间的转换和复制,使用工具类生成Helper静态对拷属性。 禁止使用反射,不仅是因为性能损失,主要是动态性,脱离了编译时检查。

直接单向输出的model对象,可以使用map,否则一定强类型的class。

public interface TradeService {

    @Getter
    @RequiredArgsConstructor
    enum Err implements CodeEnum {
        RateFailed("fedex.rate.unknown", "Fedex查询价格错误"),
        ;

        private final String code;
        private final String hint;
    }

    @Data
    class TradeInfo {
        private long orderId;
        private BigDecimal amountOrder;
        // others
    }

    /* docs */
    void transfer(@NotNull MoneyInfo ai, @NotNull TradeInfo ti, @NotNull Journal journal);
}

0A.7.枚举类和code/const值

因强类型原则,所有code和const都应该变成enum,在业务层传递。

  • 在service层,通过自动java模板生成enum,通过*EnumUtil,转换
  • 在db层,以基本类型(int,varchar)读取和写入
  • 在用户层,以多国语形式显示枚举内容
  • 不能enum的魔法值,使用@MagicConstant标注

0A.8.maven管理的约定

  • 多模块有主工程(parent|packaging=pom)和子工程(module|packaging=jar)
  • 主工程在dependencyManagement定义lib,不管理具体dependency
  • 子工程自己管理dependency,不可以重新定义版本号
  • 依赖冲突时,maven遵循路径最短原则,所以在就近工程重新定义

0A.9.Api测试及文档约定

wings默认开启swagger,访问路径为/swagger-ui/index.html

因swagger注解容易使doc部分冗长,且SpringDoc做了比较智能的推导, 所以在能够表述清楚时,建议简化注解,参考以下注解。

  • @Operation,以tag,summary,description等表述清楚
  • @Schema,输入或输出对象
  • @Parameter, 输入参数
  • @ApiResponse,必要时使用

在description中,支持Markdown,辅助jsdoc,可使文档更加清晰。

使用swagger时,不可使用弱口令,在正式服上可通过以下属性关闭。

  • springdoc.api-docs.enabled=true
  • springdoc.swagger-ui.enabled=true

0A.A.resource结构

文件或包,一般以wings或项目代号为前缀。前缀表示统一服务,项目代号为项目特有。

src/main/resources
├── META-INF - spring 自动配置入口等
│   └── spring.factories - EnableAutoConfiguration入口
├── extra-conf/ - 非自动加载的其他配置
├── wings-conf/ - wings自动加载配置 xml|yml|yaml|properties
├── wings-flywave/ - flywave数据库版本管理,
│   ├── branch/* - 分支脚本,如维护,功能
│   └── master/* - 主线脚本,上线中
└── wings-i18n/ - wings自动加载 bundle
│   ├── base-validator_en.properties - 英文版
│   └── base-validator_ja.properties - 日文版
└── application.properties - spring 默认配置,用于覆盖wings

0A.B.自动配置结构

wings对spring/bean包有特殊处理,可以自动 @ComponentScan

src/**/spring - spring有个配置
├── bean/ - 自动扫描,产生可被Autowired的Bean
│   └── WingsLightIdConfiguration.java - 内部用项目前缀,对外使用Wings前缀
├── boot/ - spring boot 配置用,不产生Bean
│   └── WingsAutoConfiguration.java - 兼容IDE和starter的配置入口
├── conf/ - 配置辅助类Configurer, AutoConfiguration
├── help/ - 工具辅助类
└── prop/ - 属性类,自动生成spring-configuration-metadata.json
    └── FacelessEnabledProp.java - 开关类

@Configuration类中配置@Bean时,对其依赖的注入,遵循以下原则,

  • 优先使用Constructor+final形式
  • 使用Bean声明方法的参数
  • 可使用Config的Field注入
  • 避免使用Config的Setter注入,因为不能及时暴露依赖错误
  • 一般方法的Autowired用于工具类初始化

0A.C.常见的命名约定

  • 接口默认实现为Default*
  • 适配器类为*Adapter

常用命名组合,单词顺序和词义尽量保持一致,可读性优先。

  • Ins/Out
  • Query/Reply
// Service中Journal 枚举类
enum Jane {
    Create, // 新建
    Modify, // 修改
    Remove, // 逻辑删除
    Delete, // 物理删除
}

0A.D.Event同步内部优先

  • 内部Event,内部Publish,内部Listen
  • 能内部Listen的,就不用外部的Subscribe
  • 能同步的,就不用异步

0A.E.有关过度设计和技术债务

因为需求的渐进明细,外部的环境变化,几乎所有业务系统的开发都是演进式。 基于以上事实,在实际交付中,力求完美很容易误人误事,应该遵循以下规则:

  • 仅做高出能力的10%的挑战,小于20%的远见
  • 任何技术或方案的妥协都不得牺牲质量,做事不可糊弄
  • 每次迭代,偿还10%-20%的技术债务

0A.F.时间是神奇的类型

系统内有2种时间系统时间本地时间,数据库和java类型映射上,

  • 日期时间,以DATETIMEDATETIME(3)LocalDateTime存储
  • 日期,以DATELocalDate存储
  • 时间,以TIMETIME(3)LocalTime存储
  • 时区,以VARCHAR(40)INT(11)存储
  • 特别场景,以BIGINT(20)VARCHAR(20)存储

字段命名规则,存在以下后缀约定,

  • _ldt - LocalDateTime,如 order_ldt
  • _odt - OffsetDateTime,如 order_odt
  • _zdt - ZonedDateTime,如 order_zdt
  • _zid - ZoneId,如 order_zidorder_*的时区

以跨境电商场景为例,服务器群采用UTC时区(系统时间),中国用户Asia/Shanghai(用户时间), 纽约NY商家America/New_York(数据时间),洛杉矶LA商家America/Los_Angeles(数据时间)。

本地日时,必须有时区配合,又分为用户时间数据时间,命名后缀如下,

  • 时区 - 以_tz_zid为后缀,内容为ZoneId的字符串名字
  • 日时 - 系统/用户/数据,分别以_dt/_udt/_ldt结尾
  • 日期 - 系统/用户/数据,分别以_dd/_udd/_ldd结尾
  • 时间 - 系统/用户/数据,分别以_tm/_utm/_ltm结尾

举例,北京时间2020-08-09 01:00:00,中国用户C1,分别在NY和LA商家下单。

  • Sys_dt(UTC) = 2020-08-08 17:00:00
  • C1_udt(Asia/Shanghai, UTC+8) = 2020-08-09 01:00:00
  • NY_ldt(America/New_York, UTC-4) = 2020-08-08 13:00:00
  • LA_ldt(America/Los_Angeles, UTC-7) = 2020-08-08 10:00:00

哎,不对啊,记得纽约是西五区啊,应该UTC-5啊,怎么时间不对呢? 系统中不要使用UTC-5,所以需要city标志zoneid, 因为同一经线上国家很多,并且时区本非按经线换分,有些区域存在夏令时

系统时区,推荐为核心用户所在时区,要考虑UTC是否为最优解。

于是,以下场景时,我们会用到不同的时间,

  • 当跟踪系统日志时,我们使用Sys_dt,可以保证统一的时间线
  • 当统计北美商家上午的营运报表时,我们使用*_ldt
  • 当追求用户体验,用户不关心时区时,用户看到的所有时间都是C1_udt
  • 有些行业惯例(航空,物流)使用本地时间,我们使用*_ldt

按数据的读写比例,在时间存储时,要考虑,

  • 统计类业务,通常写入时转化,存入用户本地时间(和时区),读取时不转换
  • 协作类业务,通常写入时,使用系统时间,读取时转换

如果需要转换时间,需要在用户界面统一(如controller)处理。

对应java7过来的选手,参考以下替代关系。

  • Instant 代替 Date
  • LocalDateTime 代替 Calendar
  • DateTimeFormatter 代替 SimpleDateFormat

0A.G.非科学家别用浮点型

wings中不应该有浮点类型float/double,而只有整数(int/long),小数用BigDecimal, 他们对应的数据库类型分别为 INT/BIGINT/DECIMAL。

但在实践过程中,因科普不到位,一些外部惯性未被消除而污染wings代码,尤其在js体系中更为明显。

  • 0.1 + 0.2 = 0.30000000000000004
  • 0.12 - 0.02 = 0.099999999999999

其根本原因在于IEEE754格式,浮点型不适合非科学计算场景,除科学家外普通人慎用。 Effective Java是java从业人员必备知识,在此不做赘述,参考以下章节: "Avoid Float and Double If Exact Answers Are Required"

0A.H.业务数据类型

Wings中推荐使用以下类型,主要考虑了sql和json的兼容性,sql主要是mysql和h2database

JAVAMYSQL/H2JS/JSON
Boolean/booleanTINYINT(1)/BOOLEANboolean
Integer/intINT/INT(11)number
Long/longBIGINT/BIGINT(20)BigInt/bigint
BigDecimalDECIMAL(M,D)string
StringCHAR(N)/VARCHAR(N)/TEXTstring
LocalDateTimeDATETIME(3)string
LocalDateDATEstring
LocalTimeTIME(3)string
byte[]BLOBstring(base64_urlsafe)

0A.I.实际中如何优雅的消除null

如同【攻城狮朋友圈】代码的坏味道所讲,wings工程实际,基本上以empty取代了null。

  • 若null是业务有效值,需要首先做业务判断。
  • 若null是业务无效值,应该采用PreCheck或以@NotNull及empty取代
  • 业务方法,一般提供get()和get(boolean)两类方法
    • get()用于获取NotNull的业务对象,等同于get(true),肯定语义
    • get(false)用于获取可以为null的业务对象,即否定语义

分情况讲,尽管我们都主张避免使null变成业务有效值,但有时系统外的因素不可控。 常见的数据库,API,JNI,都可能导致null进入数据流。此时,应该在进入业务流之前拦截, 或显示的做null判断,比如 Objects.equalsfoo == null等。

需要注意的是,业界流传一种『高级』秘籍,流行到被视为高级程序猿标配。

  • !"foo".equals(bar) 可以安全的处理,bar是null的情况
  • null != foo,null前置,变成左值。

这两个小技巧在工程中很容易挖坑,应当引起警觉或避免,大概的不好之处如下。

  • equals和hashCode的实现,有基本要求的,并非equals都对null友好。
  • 混淆了逻辑,容易搞丢逻辑分支,!=null!=foo是两个分支。
    • 若null是业务值,应该采用Objects.equals显示的合并分支;
    • 否则应assert或PreCheck,null进入业务逻辑,就意味着沦陷了。
  • null变左值,破坏一致性,好比Junit中expected和actual互换,攻城狮应该维护一致性。

理论归理论,实际中都有取舍和无奈,要尊重历史,遵守团队约定。在wings中,这样做,

  • EmptyValueEmptySugar,在业务中确立了empty值及工具类
  • Collection,Map,Array等集合或容器类型,都需要以Empty返回
  • Null类,定义了用来代替null的类型和检查方法,包括enum等
  • 方法签名尽量使用@NotNull注解,是IDE辅助检查,编译时解决
  • AssertArgsAssertState进行业务assert,支持多国语

0A.J.类型系统的逆变/协变/PECS

// ① 字段使用具体类型,还是抽象类型
private List<E> field1 = new ArrayList<>();
private ArrayList<E> field2 = new ArrayList<>();

// ② 方法返回值
public Map<String, ?> provide1();
public TreeMap<String, Object> provide2();

// ③ 方法输入参数
public void consume1(List<String> list);
public void consume2(Collection<? extends CharSequence> list);

// ④ Map的方法签名
replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

Wings在编码中鼓励,在保证兼容性(主要是行为特性)的情况下,接口(以嘴做比喻)

  • 吃的时候 - 输入项尽量抽象,尽量吃的更广
  • 吐的时候 - 输出项尽量具体,尽量嚼的更碎
  • 内部东西 - 保持原样,用的人知道特征

以Map举例来说,输入时,使用Map+superK+extendsV来, 输出时,不要抹杀特征,比如是否SortedMap及RandomAccess

  • TreeMap是字典序
  • LinkedHashMap是插入序
  • HashMap是乱序等

0A.K.类和方法的泄露(副作用)

Wings把非以下特征的方法,统称为泄露或者副作用

  • Pure functions(纯函数)
  • referential transparency(透明引用)
  • Side Effects(副作用)

编程中,尽量避免方法泄露,规避隐式变量

0A.L.增强后的自注入 ThisLazy

ThisLazy模式,在Bean内,可调用被Spring增强方法,如@Transactional, @Cacheable, @Async

以下方式,使用thisLazyAwarePostProcessor 完成了自动注,

  • extends ThisLazy<T> - 子类中直接使用 thisLazy
  • implements ThisLazyAware<T> - 实现接口。

以下代码,为手动设置初始化和注入thisLazy

@Setter(onMethod_ = {@Autowired, @Lazy})
protected RuntimeConfServiceImpl thisLazy = this;