一、工程结构
1、应用分层
Client: 主要提供对外交互的API,各种Cmd或者Qry以及Response,并且遵循sofa的相关规范。
App层主要负责获取输入,组装context,做输入校验,发送消息给领域层做业务处理,监听确认消息,如果需要的话使用MetaQ进行消息通知;
Domain层主要是通过领域服务(Domain Service),领域对象(Domain Object)的交互,对上层提供业务逻辑的处理,然后调用下层Repository做持久化处理;
Infrastructure层主要包含Repository,Config和Common,Repository负责数据的CRUD操作,这里我们借用了盒马的数据通道(Tunnel)的概念,通过Tunnel的抽象概念来屏蔽具体的数据来源,来源可以是MySQL,NoSql,Search,甚至是HSF等;Config负责应用的配置;Common是一写工具类;负责message通信的也应该放在这一层
分层领域模型规约:
DO(Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。 一般来说对外提供的Command和Query都继承自DTO;
BO(Business Object):业务对象。由 Service 层输出的封装业务逻辑的对象。
AO(Application Object):应用对象。在 Web 层与 Service 层之间抽象的复用对象模型, 极为贴近展示层,复用度不高。
VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止 使用 Map 类来传输。
相应各层Java文件命名规范参见如下文件:
2、配置文件管理;
配置文件目录层次遵循maven的最佳实践:
代码块
Java
src
\-- main
| \-- java
| |-- resources
| \-- spring
| | \-- xxx-common-pigeon-client.xml
| | |-- xxx-common-mybatis.xml
| | |-- xxx-common-cache.xml
| | |-- xxx-pigeon-service.xml
| | |-- xxx-mvc-servlet.xml
| |-- sqlmap
| | \-- dbName(db名称)
| | \-- ActivityInfoDOMapper.xml
| | |-- gmktacitivy
| |-- log4j2.xml
| |-- mybatis-generator.xml
|-- test
\-- resources
3、项目依赖管理;
此处的依赖关系,主要管理提供给外部调用方的API、依赖第三方的外部API。需要说明的有以下几点
1)团队的所有maven构件的groupId定义要求以com.sankuai.web.trade(待定)为前缀,子业务通过artifactId来区分,不再在groupId后增加内容;
2) maven的version定义:{主版本号}.{次版本号}.{修订版本号}。
当增加了新的功能,或做了不兼容的改进时,一般需要更改主版本号;
当做了向下兼容的功能迭代(新增接口、类等)时,一般需要更改次版本号;
当保持API的方法签名不变,仅做了参数字段、枚举常量的增添,或者一些小的bug fix时,一般需要更改修订版本号;
说明:注意起始版本号必须为:1.0.0,而不是 0.0.1 正式发布的类库必须先去中央仓库进 行查证,使版本号有延续性,正式版本号不允许覆盖升级。如当前版本:1.3.3,那么下一个 合理的版本号:1.3.4 或 1.4.0 或 2.0.0
3) 线上应用不要依赖 SNAPSHOT 版本(安全包除外),内部的禁用,外部依赖请推动其发布正式包。
4) 在对外提供API的Jar时,建议仅包含API接口相关的声明类等,不要添加任何的第三方依赖,以防止给调用方带来一些不必要的传递依赖;对于API层的sofa依赖,请只依赖sofa-common;
5) 所有依赖的version声明应在父POM里面通过<dependencyManagement>来进行管理,而实际依赖的引入则在子POM的<dependencies>里面完成;
6) 公司基础组件请使用inf-bom,团队内部对于第三库的依赖请使用 trade-framework-bom;
4、服务器环境规范;
1)文献【3】详细定义了各个环境,以及北京、上海两地的环境对应关系,请参考。
2)JVM参数规范? 是固定还是可修改,还是部分固定,部分可修改?待定;
二、编码规范
1、编码风格
1) 代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。 说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式 也要避免采用。
注意:sankuai、meituan、dianping等通用名称可以视为英文;
2) 类名使用 UpperCamelCase(驼峰) 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO 等。
3) 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从 驼峰形式。
4) 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长
5) 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类 命名以它要测试的类名开始,以 Test 结尾。
6) 类型与中括号紧挨相连来定义数组。
正例:定义整形数组 int[] arrayDemo; 反例:在 main 参数中,使用 String args[]来定义。
7) 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用 单数形式,但是类名如果有复数含义,类名可以使用复数形式。
正例:应用工具类包名为 com.sankuai、类名为 MessageUtils(此规则参考 spring 的框架结构)
8) 如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式。 说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
9) 接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁 性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是 与接口方法相关,并且是整个应用的基础常量。
正例:接口方法签名 void f(); 接口基础常量 String COMPANY = "meituan";
反例:接口方法定义 public abstract void f();
11) DAO 层方法命名规约
a) 获取单个对象的方法用 get 作前缀。
b) 获取多个对象的方法用 list 作前缀。
c) 获取统计值的方法用 count 作前缀。
d) 插入的方法用 save/insert 作前缀。
e) 删除的方法用 remove/delete 作前缀。
f) 修改的方法用 update 作前缀。
13) boolean类型变量命名不要加'is','has'等前缀;
2、常量定义
1) long 或者 Long 初始赋值时,使用大写的 L,不能是小写的 l,小写容易跟数字 1 混 淆,造成误解。
2) 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
反例:String key = "Id#taobao_" + tradeId;
cache.put(key, value);
3) 不要使用一个常量类维护所有常量,按常量功能进行归类,分开维护。
4) 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包 内共享常量、类内共享常量。
a) 跨应用共享常量:放置在二方库中,例如: trade-framework-client.jar 中的 constant 目录下。
b) 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。
c) 子工程内部共享常量:即在当前子工程的 constant 目录下。
d) 包内共享常量:即在当前包下单独的 constant 目录下。
e) 类内共享常量:直接在类内部 private static final 定义。
3、代码风格
1) 大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果 是非空代码块则:
a) 左大括号前不换行。
b) 左大括号后换行。
c) 右大括号前换行。
d) 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。
2) 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格。
3)if/for/while/switch/do 等保留字与括号之间都必须加空格
4)任何二目、三目运算符的左右两边都需要加一个空格
5)采用 4 个空格缩进,禁止使用 tab 字符。
6)注释的双斜线与注释内容之间有且仅有一个空格。
正例: //_这是示例注释,请注意在双斜线之后有一个空格
7)单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
a) 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进。
b) 运算符与下文一起换行。
c) 方法调用的点符号与下文一起换行。
d) 方法调用时,多个参数,需要换行时,在逗号后进行。
e) 在括号前不要换行,见反例
代码块
Java
正例:
StringBuffer sb = new StringBuffer();
// 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
sb.append("zi").append("xin")...
.append("huang")...
.append("huang")...
.append("huang");
反例:
StringBuffer sb = new StringBuffer();
// 超过 120 个字符的情况下,不要在括号前换行
sb.append("zi").append("xin")...append
("huang");
// 参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
method(args1, args2, args3, ...
, argsX);
8) 方法参数在定义和传入时,多个参数逗号后边必须加空格。
9) IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符使用 Unix 格式, 不要使用 Windows 格式。
4、OOP规范
1) 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成 本,直接用类名来访问即可。
2) 所有的覆写方法,必须加@Override 注解。请注意idea自动生成代码对于JDK <= 1.5的时候不自动增加override直接;
3)相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。
4)外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生 影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。
5)不能使用过时的类或方法。
6)Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
正例:"test".equals(object);
反例:object.equals("test");
7) 所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
8) 关于基本数据类型与包装数据类型的使用标准如下:
a) 所有的 POJO 类属性必须使用包装数据类型。
b) RPC 方法的返回值和参数必须使用包装数据类型。
c) 所有的局部变量使用基本数据类型。
9) 序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如 果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值
10) 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
11) POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。 说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排 查问题。
5、集合相关;
1)关于 hashCode 和 equals 的处理,遵循如下规则:
a) 只要重写 equals,就必须重写 hashCode。
b) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的 对象必须重写这两个方法。
c) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals。 说明:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象 作为 key 来使用。
2)ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException 异常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList.
说明:subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
3)在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、 删除均会产生 ConcurrentModificationException 异常。
4)使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全 一样的数组,大小就是 list.size()
5)使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方 法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
6)泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方 法,而不能使用 get 方法,作为接口调用赋值时易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内 容的,适合用。第二、经常往里插入的,适合用。
7)不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
8)在 JDK7 版本及以上,Comparator 要满足如下三个条件,不然 Arrays.sort, Collections.sort 会报 IllegalArgumentException 异常。 说明:三个条件如下
a) x,y 的比较结果和 y,x 的比较结果相反。
b) x>y,y>z,则 x>z。
c) x=y,则 x,z 比较结果和 y,z 比较结果相同。
9)集合初始化时,指定集合初始值大小。
6、并发;
1)获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
2)创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
3)线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
4)线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
说明:Executors 返回的线程池对象的弊端如下:
a)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
b)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
5)SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
6)对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造 成死锁。
7)并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加 锁,要么在数据库层使用乐观锁,使用 version 作为更新依据
8)多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获 抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。
9)使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行 至 await 方法,直到超时才返回结果。
10) 避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。
11)在高并发场景中,避免使用”等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间 判断条件来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变 成了负数,这样的话,活动无法终止。
7,控制语句
1)在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程 序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且 放在最后,即使空代码。
2)在 if/else/for/while/do 语句中必须使用大括号。即使只有一行代码,避免采用 单行的编码方式:if (condition) statements;
8,注释
1) 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用 // xxx 方式。
2)所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。
3)所有的类都必须添加创建者和创建日期。
4) 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释 使用/* */注释,注意与代码对齐。
5) 所有的枚举类型字段必须要有注释,说明每个数据项的用途。
三、数据库缓存规范
1、建表
1)表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint ( 1 表示是,0 表示否)。
2) 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只 出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
3) 表名不使用复数名词。
4)禁用保留字,如 desc、range、match、delayed 等,
5)主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名
6)小数类型为 decimal,禁止使用 float 和 double。
7) 如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
8)varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长 度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索 引效率。
9)表必备三字段:id, add_time, update_time
2、索引规则;
1)业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。
2)超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致;多表关联查询时, 保证被关联的字段需要有索引。
3)在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度即可。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分 度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度 来确定。
4)页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
3、sql语句;
1) 不要使用 count(列名)或 count(常量)来替代 count(*),count(*)是 SQL92 定义的 标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
2)count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
3)当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题。
4)使用 ISNULL()来判断是否为 NULL 值
5)在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
6) 不得使用外键与级联,一切外键概念必须在应用层解决。
说明:以学生和成绩的关系为例,学生表中的 student_id是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为 级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻 塞,存在数据库更新风暴的风险;外键影响数据库的插入速度
7)禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
8)数据订正(特别是删除、修改记录操作)时,要先 select,避免出现误删除,确认 无误才能执行更新语句。
4、ORM映射;
1)在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
2)POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行 字段与属性之间的映射。
3)不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需 要定义;反过来,每一个表也必然有一个与之对应。
4)sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。
5)不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出。
6)更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。
四、安全
1. 隶属于用户个人的页面或者功能必须进行权限控制校验。
2. 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。
说明:个人手机号码显示为:158****9119,隐藏中间 4 位,防止隐私泄露
3. 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入, 禁止字符串拼接 SQL 访问数据库。
4. 用户请求传入的任何参数必须做有效性验证。
5. 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据,防止XSS等漏洞攻击;
6 表单、AJAX 提交必须执行 CSRF 安全过滤。
7 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制, 如数量限制、疲劳度控制、验证码校验,避免被滥刷导致资损。
五、异常日志;
1、异常不要用来做流程控制,条件控制
2、catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。 对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
3、捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请 将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的 内容。
4、有 try 块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回 滚事务。
5、finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
6、不要在 finally 块中使用 return。说明:finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
7、应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
8、在提供 HTTP Web 接口的 Controller 层,或者提供Pigeon接口的 Service 层,强制需要捕获住 Exception,并将其转换成调用方可理解的内容
9、在应用内部的方法调用之间,如果有异常,推荐直接抛出给调用方,如果调用方无法处理,继续向上抛出;在应用对外提供的 Pigeon 接口,建议统一包装成 errorCode、errorMsg 的方式给调用方;
10、在记录日志的时候,必须包括上下文信息(因为日志是为了在系统出问题,帮助快速定位问题的第一有效手段),在一个方法层次调用链中记录日志时,尤其需要注意尽量在每次记录日志时带有一个唯一性的业务标识;
六、关于代码检查插件;
参考文献: