序
之前的所谓代码规范大部分是根据语言书的一些默认规则,我感觉基本就是习惯问题,不要太离谱即可。现在也做了一些项目,对规范的东西感触更深,好看好读的代码才是好代码。虽然阿里的开发手册不能说适用于业界所有Java项目,但是实际上来说,阿里现在的Java技术积累已经是很强了,这些开发者愿意来做这件事也是于业界有益的,很感谢他们。
在这里只记录个人之前没有意识到的点。毕竟里面有些规定也成了自己的习惯。
编程规约
命名风格
强制
- 使用语意准确的英文变量名。包括潜在共识的名词(例如Alibaba)
- 避免使用歧视性词汇(blackList->blockList/whiteList->allowList/slave->secondary),估计是与时俱进了
- 类名一律首字母大写驼峰式
- 方法名、参数名、成员变量、局部变量一律首字母小写驼峰式
- 常量名全大写,
_
分隔。要注意语意,不怕长 - 抽象类命名以Abstract/Base开头;异常类以Exception结尾;测试类以测试的类名称开始,Test结尾
- 类型和
[]
紧挨来表示数组(int[] i) - POJO类中的任何布尔变量不使用
isXXX
的形式,会引起一些框架的序列化错误。MySql的是否变量采用is_xxx
命名方式,在中要设置 xxx
和is_xxx
的映射关系 - 包名统一小写,点分隔符之间只写一个自然语意的单词。包名统一用单数形式。类名如果有复数含义可以用复数
- 子父类中的局部变量,非getter/setter方法的参数命名尽量不与成员变量的命名相同,子父类的成员变量最好不出现同名
- 对于Service和DAO类,基于SOA理念,暴露出的一定是接口,其内部实现类用Impl的后缀与接口区别(CacheServiceImpl和CacheService)
推荐
- 命名时使用尽量完整的单词组
- 常量与变量的命名中,类型放到最后(nameList/TERMINATE_THREAD_COUNT)
- 将设计模式体现在类、模块、方法、接口的命名中
- 接口类中所有方法不加修饰符(保持简洁),加上JavaDoc注释。尽量不在接口中定义变量,如果非要这样,要确定这个变量与接口方法有关,并且是整个应用的基础常量,JDK8中允许接口有默认实现的方法,此方法必须是对所有实现类有价值的默认实现
- 功能性接口的命名最好使用对应的形容词作为接口名
- 枚举类名要加上Enum后缀,枚举类的成员名全部大写,单词之间加上
_
(枚举就是特殊的常量类,构造方法默认是私有的)
参考
- Web开发中各层命名规约
* Service和DAO层方法命名规约
* 获取单个对象的方法用get做前缀
* 获取多个对象的方法用list做前缀
* 获取统计值的方法用count做前缀
* 插入方法用save/insert做前缀(个人感觉insert更好)
* 删除方法用remove/delete做前缀(个人感觉delete更好)
* 修改方法用update做前缀
* 领域模型命名
* 数据对象,xxxDO
,xxx为表名
* 数据传输对象,xxxDTO
,xxx为业务领域相关名称
* 展示对象,xxxVO
,xxx一般为网页名称
* POJO是DO/DTO/BO/VO的统称,切忌使用xxxPOJO
来命名
常量定义
强制
- 不允许魔法值直接出现在代码里(所有未经定义的字面量)
- long的数字定义要加
L
而非l
推荐
- 按常量的功能划分为多个常量类来保存常量
- 常量的复用层次(跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量)
* 跨应用:二方库,通常在client.jar中的constant目录,专门做一个jar包来存储(注:一方库是本工程内模块和包依赖的;二方库是同一公司内部开发的库;三方是公共库)
* 应用内:一方库,子模块下的constant目录,模块级别
* 子工程内:当前子工程的constant目录
* 包内:包下独立的constant目录
* 类内:private static final - 如果变量值在一个范围内使用enum类型来定义(如四季)
代码格式
强制
- 采用4空格缩进,不适用tab
- 注释符号和内容间有且只有1个空格
- 强制类型转换的右括号和被转换变量之间没有空格
- 单行字符不超过120,超出需换行,换行原则
* 第二行较第一行换行4个空格,后续不再继续缩进
* 运算符与下文一起换行
* 方法调用符号与下文一起换行
* 方法调用的参数个数多,要在逗号后换行而不是连逗号一起换行 - IDE的text file encoding设置为UTF-8,IDE中文件的换行符使用Unix而不是Windows
推荐
- 单个方法行数不超过80行(注释除外),核心在于分清方法的红花和绿叶逻辑,注意方法的拆分,便于复用和维护
- 不同逻辑、不同语义、不同业务的代码之间插入一个空行来分割提高可读性
OOP规约
强制
- 避免通过对象来访问类的静态方法(无谓增加编译器解析成本),直接用类名访问
- 所有覆写方法要加
@Override
注解(在抽象类中改了方法签名,覆写方法会马上报错) - 相同参数类型、相同业务含义才可以使用Java的可变参数,避免用Object
- 外部或者二方库正在调用的接口不允许修改方法签名,过时的接口必须要加
@Deprecated
注解,并清楚说明新的接口或服务 - 不能使用过时方法(接口提供方标明了过时接口就有义务提供新接口的说明,接口调用方有义务查明新方法的实现)
- Object的
equals
方法容易抛空指针异常,宜用常量或有确定值的对象来调用equals
方法(推荐JDK7提供的java.util.Object#equals(Object o1, Object o2)
) - 所有整型包装类对象之间值的比较,全用
equals
方法(Integer var = value;
在-128-127的数值之间的赋值,Integer对象是在IntegerCache.cache
中产生的,会复用已有对象,此时直接用==
比较没有问题。但是这个区间之外的所有数据,都会在堆上产生,并不会复用对象) - 任何货币金额,都用最小货币单位并以整数存储(不用BigDecimal是因为精度问题--出问题几率大,数额巨大可以使用
BigInteger
) - 浮点数之间的等值判断,基本类型不能用
==
,包装类型不能用equals
(尾数+阶码的表示方式无法精确表示大部分小数),解决方法:
1. 定义误差值,在此误差内即算相等
2. 使用BigDecimal
类 BigDecimal
的对象比较要用compareTo()
方法而不是equals
- DO类中,成员属性和数据库字段的类型要对应
- 禁止使用
BigDecimal
类的BigDecimal(double)
构造器来构建对象,这样有损失精度的风险,建议使用BigDecimal(String)
来构造对象,或者用valueOf(double)
方法,但是这个方法也是调用了Double
的toString()
方法再转换 - 基本数据类型和包装类型的使用说明
* POJO类中的属性必须用包装类型
* RPC方法返回值和参数类型必须用包装类型(包装类型的null值可以携带更多信息,例如调用失败,异常退出等) - 定义DO/DTO/VO等POJO类时不要设定任何属性默认值(例如创建日期字段默认为
new Date()
,更新别的字段会附带更新了这个字段,那么创建日期变成了当前日期) - 序列化类新增属性时,不要修改
serialVersionUID
字段,避免反序列化失败。如果是完全不兼容升级,那么修改serialVersionUID
(serialVersionUID
不一致会抛出运行时异常) - 构造方法中不写任何业务逻辑,所有的初始化逻辑要放到
init()
方法中 - POJO类中必须写
toString()
方法,如果有继承的父类,要调用super.toString()
(便于查错) - 进制再POJO类中写同一个属性xxx的两个方法
isXxx()
和getXxx()
(框架在调用属性xxx的提取方法时,并不能确定哪一个是被优先调用的)--不完全理解
推荐
- 所有局部变量使用基本数据类型
- 使用
String
的split(String)
时,要做最后一个分割符后是否有内容的检查,否则可能后续会出现数组越界的情况 - 构造方法、多个同名方法最好放在一起显示
- 类内方法顺序:公有方法或保护方法->私有方法->getter/setter
- setter方法中,参数名和属性名相同。不在getter/setter中加入业务逻辑
- 循环体内的字符串拼接使用
StringBuilder
的append()
方法 - 使用
final
的情况
* 不允许被继承的类
* 不允许被修改的域对象(POJO类的域变量)
* 不允许被覆写的方法(POJO中的setter)
* 不允许运行过程中重新复制的局部变量
* 避免上下文使用同一个变量,final可以强制你创建新变量(这是不是有点过了) - 慎用Object的
clone
方法来拷贝对象(浅拷贝,实现神拷贝需要覆写clone
方法,对域对象的深度遍历式拷贝) - 类成员和方法访问控制从严
1. 如果不允许外部直接使用new
来创建对象,构造器要使用private
2. 工具类不允许有public
或default
构造方法
3. 类非静态成员变量并且与子类共享,必须是protected
4. 类非静态成员变量仅在本类使用,必须是private
5. 类静态成员变量仅在本类使用,必须是private
6. 若是静态变量,考虑其是否为final
7. 类成员方法只供内部调用,必须是private
8. 类成员方法只对继承类公开,必须是protected
日期时间
强制
- 日期格式化的pattern中的年要用小写的
y
(大写Y在JDK7之后引用,代表的是当日所在当周所在的年份,一周为周六到周日,所以只要本周跨年那么返回的就是下一年) - 分清H、h、M、m的含义,H是24小时制的时间,h是12小时制的时间,M是月份,m是分钟
- 获取当前毫秒数使用
System.getCurrentTimeMillis()
而不是new Date().getTime()
(纳秒可以用System.nanoTime
的方式,JDK8中统计时间相关推荐用Instant
类) - 不允许在程序任何地方使用
java.sql.Date
java.sql.Time
java.sql.TimeStamp
(第一个不记录时间、第二个不记录日期,第三个构造方法super((time/1000)*1000),在Timestamp属性fastTime
和nanos
分别存储秒和纳秒信息 - 不在程序中将一年写死为365天(避免在公历闰年中出现日期转换错误或程序逻辑错误)
推荐
- 避免公历闰年2月问题,闰年2月是29天,那么它的下一年2月一定不是29天
- 使用枚举值来表示月份,如果使用数字,在Date、Calendar等类中月份是0-11
集合处理
强制
hashCode
和equals
的处理,遵循以下原则
1. 只要覆写equals
,就要覆写hashCode
2. 因为Set存储的是不相同的对象,根据hashCode
和equals
方法来判断,所以Set存储的对象必须覆写这两个方法
3. 如果自定义对象作为Map的键,那么必须覆写hashCode
和equals
- 判断集合为空使用
isEmpty()
而不是x.size() == 0
(某些集合中前者时间复杂度为1,且可读性更好) - 在使用
java.util.stream.Collectors
类中的toMap()
方法转为Map集合时,一定要使用含有参数类型为BinaryOperator
,参数名为mergeFunction
的方法,否则当出现相同key
值时会抛出IllegalStateException
异常(正常应该是保留最后一个键值对) - 在使用
java.util.stream.Collectors
类的toMap()
方法转为Map
集合时,一定要注意当value
为null
时会抛NPE
异常。(HashMap
的merge
方法中有:if (value == null || remappingFunction == null) throw new NullPointerException()😉 ArrayList
的subList
结果不能转为ArrayList
,它的类型就是内部类SubList
,对于SubList的所有操作会映射到原List上- 使用Map的
keySet()
values()
entrySet()
方法时不能向其中添加元素 Collections
类返回的对象都是immutable
的,如emptyList()/singletonList()
,不能对其做添加删除元素的操作subList
场景中,注意对父集合元素的增加和删除,均会导致子列表的遍历、增加和删除报错(ConcurrentModificationException
)- 使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入的是类型完全一致、长度为0的空数组(直接用无参方法会返回Object[]
,再转数组类型会报错) - 在使用
Collection
接口任何实现类的addAll()
方法时,都要对输入的集合参数进行NPE
判断(在ArrayList#addAll
方法的第一行代码即Object[] a = c.toArray()
,如果c--传入的集合参数为null,那么会报错) - 使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear
方法会抛出UnsupportedOperationException
异常(asList
体现的是适配器模式,内部存储的数据还是数组(Array的内部类)) - 泛型通配符
<? extends T>
来接收返回的数据,此写法的泛型集合不能使用add
方法,而<? super T>
不能使用get
方法,两者在接口调用赋值的场景中容易出错(扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用<? extends T>
。第二、经常往里插入的,适合用<? super T>
)(标记一下,泛型是需要补习的知识点,这里看不懂) - 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行
instanceof
判断,避免抛出ClassCastException
异常(毕竟泛型是在 JDK5 后才出现,考虑到向前兼容,编译器是允许非泛型集合与泛型集合互相赋值)(同上,这里的意思是转换之后每个元素用之前要进行instanceof
判断) - 不要在
foreach
循环里进行元素的remove/add
操作。remove
元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁(foreach
语句内不对集合进行增删操作,使用迭代器方式可以删除元素) - 在 JDK7 版本及以上,
Comparator
实现类要满足如下三个条件,不然Arrays.sort
,Collections.sort
会抛IllegalArgumentException
异常
1. x,y比较结果和y,x的比较结果相反
2. x > y,y > z则x > z
3. x = y则x,z的比较结果跟y,z的比较结果相同
推荐
- 集合泛型定义时,在JDK7及以上,使用diamond(菱形泛型,左右括号)语法或全省略,在定义时不需要再写一遍泛型类型
- 集合初始化时,指定集合初始值大小(HashMap 使用
HashMap(int initialCapacity)
初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可,initialCapacity = (需要存储的元素个数 / 负载因子) + 1
。注意负载因子(即loader factor)默认为 0.75) - 使用
entrySet
遍历Map
类集合KV,而不是keySet
方式进行遍历(JDK8之后使用Map.forEach()
方法) - 高度注意 Map 类集合 K/V 能不能存储 null 值的情况
集合类 | Key | Value | Super | 说明 |
---|---|---|---|---|
Hashtable | 不允许 | 不允许 | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许 | 不允许 | AbstractMap | 锁分段技术(JDK8-CBS) |
TreeMap | 不允许 | 允许 | AbstractMap | 线程不安全 |
HashMap | 允许 | 允许 | AbstractMap | 线程不安全 |
参考
- 合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响(有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的)(遍历结果始终相同)
- 利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作
并发处理 (目前还不能完全理解,多线程也需要详细补习)
强制
- 获取单例对象需要保证线程安全,其中的方法也要保证线程安全(资源驱动类、工具类、单例工厂类都需要注意)(应该是要保证对象在各个线程中的一致性)
- 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程(线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题)(线程池中的线程对象是用完返还的,所以提高了资源利用率并降低系统开销)
- 线程池不允许使用
Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。Executors 返回的线程池对象的弊端如下:
1.FixedThreadPool
和SingleThreadPool
:允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
2.CachedThreadPool
:允许的创建线程数量为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM SimpleDateFormat
是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils
工具类(不理解)(如果是JDK8的应用,可以使用Instant代替Date,LocalDateTime代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutablethread-safe)
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
使用DateUtils
建议如上处理,待学习过再来复习
- 必须回收自定义的
ThreadLocal
变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal
变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally
块进行回收(不理解) - 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁(尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法)(不理解)
- 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁(这个锁解了情况下才能给另一个加锁,另一个相同情况)
- 在使用阻塞等待获取锁的方式中,必须在
try
代码块之外,并且在加锁方法与try
代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally
中无法解锁(代码执行逻辑要保证锁资源释放),三种情况说明:
1. 如果在lock
方法与try
代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁
2. 如果lock
方法在try
代码块之内,可能由于其它方法抛出异常,导致在finally
代码块中,unlock对未加锁的对象解锁,它会调用AQS
的tryRelease
方法(取决于具体实现类),抛出IllegalMonitorStateException
异常
3. 在Lock
对象的lock
方法实现中可能抛出unchecked
异常,产生的后果与说明二相同 - 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同(不理解)
- 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据 (概念不清楚,不理解)(如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次)
- 多线程并行处理定时任务时,
Timer
运行多个TimeTask
时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService
则没有这个问题
推荐
- 资金相关的金融敏感信息,使用悲观锁策略(乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。)(悲观锁遵循一锁、二判、三更新、四释放的原则)(概念不清楚,不理解)
- 使用
CountDownLatch
进行异步转同步操作,每个线程退出前必须调用countDown
方法,线程执行代码注意catch
异常,确保countDown
方法被执行到,避免主线程无法执行至await
方法,直到超时才返回结果(注意,子线程抛出异常堆栈,不能在主线程try-catch
到)(不理解) - 避免
Random
实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed
导致的性能下降(Random
实例包括java.util.Random
的实例或者Math.random()
的方式)(在JDK7之后,可以直接使用APIThreadLocalRandom
,而在 JDK7 之前,需要编码保证每个线程持有一个单独的Random
实例) - 通过双重检查锁(double-checked locking)(在并发场景下)存在延迟初始化的优化问题隐患(可参考 The "Double-Checked Locking is Broken" Declaration),推荐解决方案中较为简单一种(适用于 JDK5 及以上版本),将目标属性声明为
volatile
型,比如将 helper 的属性声明修改为private volatile Helper helper = null;
(不理解)
参考(机制需要参考源码实现理解)
volatile
解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题(如果是 JDK8,推荐使用LongAdder
对象,比AtomicLong
性能更好(减少乐观锁的重试次数))HashMap
在容量不够进行resize
时由于高并发可能出现死链,导致CPU飙升,在开发过程中注意规避此风险ThreadLocal
对象使用static
修饰,ThreadLocal
无法解决共享对象的更新问题(这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量)
控制语句
强制
- 在一个
switch
块内,每个case
要么通过continue/break/return
等来终止,要么注释说明程序将继续执行到哪一个case
为止;在一个switch
块内,都必须包含一个default
语句并且放在最后,即使它什么代码也没有( break 是退出 switch 语句块,而 return 是退出方法体) - 当
switch
括号内的变量类型为String
并且此变量为外部参数时,必须先进行null
判断 - 三目运算符condition? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的NPE异常.以下两种场景会触发类型对齐的拆箱操作:
1. 表达式 1 或表达式 2 的值只要有一个是原始类型
2. 表达式 1 或表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型 - 在高并发场景中,避免使用”等于”判断作为中断或退出的条件(如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替)(击穿即瞬间变为负数,那么等于0的判断会出问题)
推荐
- 表达异常的分支时,少用
if-else
方式。如果非使用if()...else if()...else...
方式表达逻辑,避免后续代码维护困难,请勿超过3层(超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现) - 除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性
- 不要在其它表达式(尤其是条件表达式)中,插入赋值语句
- 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的
try-catch
操作(这个try-catch
是否可以移至循环体外) - 避免采用取反逻辑运算符
- 公开接口需要进行入参保护,尤其是批量操作的接口(文档中规定了参数大小,但是接口没有做任何保护,容易出问题)
参考
- 参数校验的情况
1. 调用频次低的方法
2. 执行时间开销很大的方法。(校验的性能浪费可以忽略,否认得不偿失)
3. 需要极高稳定性和可用性的方法
4. 对外提供的开放接口,不管是RPC/API/HTTP接口(不信任方式)
5. 敏感权限入口 - 不需要参数校验的情况
1. 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查(性能问题)
2. 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题
3. 被声明成private
只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数
注释规约
强制
- 类、类属性、类方法必须使用JavaDoc注释,使用
/** ... */
的格式,不允许普通注释(提供阅读代码效率) - 所有抽象方法(接口内)必须用JavaDoc注释,除了参数、返回值、异常说明以外,方法的用途也要说清楚
- 所有类必须添加创建者和创建日期(责任制)
- 方法内注释在被注释语句上一行,多行注释使用
/* */
,要注意对齐 - 所有枚举类型字段必须要有注释,说明每个数据项的用处
推荐
- 用中文注释把问题说清楚比强行用英文更强。专有名词可直接用(一切为了生产效率)
- 代码修改的同时,注释也要同步更新(参数、返回值、异常说明、核心逻辑)
- 在类中删除没有使用过的字段、方法、内部类。方法中删除没有使用过的任何参数和变量
参考
- 谨慎注释代码。如果后续要用则详细说明,否则直接删除
- 注释要求
1. 设计思想和代码逻辑讲清楚
2. 业务含义说清楚 - 好的命名、代码结构是自解释的,避免过多过滥的注释
- 特殊注释标价(TODO(是JavaDoc标签,只能修饰类、接口、方法)、FIXME(要标明标记人,标记时间,待办人))
前后端规约
强制
- 前后端交互的API,需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体
1. 协议:生产必须用HTTPS
2. 路径:每个API对应一个路径,表示API的具体请求地址
* 代表一种资源,只能为名词,推荐使用复数,不能为动词(请求方法已经表达此含义)
* URL路径不能使用大写,单词如果需要分割,统一用下划线
* 路径进制携带请求内容类型的后缀,通过accept头表达即可
3. 请求方法
* GET:从服务器取资源
* POST:从服务器新建一个资源
* PUT:在服务器更新资源
* DELETE:从服务器删除资源
4. 请求内容:URL带的参数必须无敏感信息(符合安全要求);body里带参数时必须设置Content-Type
5. 响应体:响应体body中可放置多种数据类型,由Content-Type
头来确定 - 前后端数据列表相关的接口返回,如果为空则返回空数组或空集合(避免前端的判null行为)
- 服务端发生错误时,返回给前端的响应信息必须包含HTTP状态码,errorCode,errorMessage,用户提示信息四个部分(四个部分分别对应浏览器、前端开发、错误排查人员、用户)
- 前后端交互的JSON格式数据,所有的key必须是小写开头的驼峰写法
- errorMessage是前后端错误追踪机制的体现,可以在前端输出到
type="hidden"
文字类控件中或用户端的日志中,帮助快速定位问题 - 使用超大整数的场景,服务端一律返回字符串(String),禁止使用
Long
类型(服务器端的Long
类型数在JS中是Number
类型(双精度浮点数、表示原理和取值范围等同于Java的Double),转换中有概率损失精度) - HTTP请求通过URL传递参数不能超过2048字节(所有浏览器的最小值)
- HTTP请求通过body来传递内容时,必须控制长度,超出最大长度后后端解析会出错(nginx默认限制是1Mb,tomcat默认为2Mb,如果确实有此需求,可以调高服务器端的限制)
- 翻页场景中,用户输入参数小于1,则前端返回第一页参数给后端;后端发现用户输入参数大于总页数,直接返回最后一页
- 服务器内部重定向必须用
forward
;外部重定向地址必须使用URL统一代理模块生成,否则会因线上采用HTTPS协议而导致浏览器提示“不安全”,还会带来URL维护不一致问题(外部重定向不理解,后续补足知识)
推荐
- 服务器返回信息必须被标记是否可以缓存,如果缓存,客户端可能会重用之前的请求结果(http1.1中,
s-maxage
告诉服务器进行缓存,时间单位为秒) - 服务端返回的数据使用JSON而非XML(application/JSON是一种通用的MIME类型,实用、精简、易读)
- 前后端的时间格式统一为“yyyy-MM-dd HH:mm:ss”,统一为GMT
参考
- 接口路径中不要加入版本号,版本控制在HTTP头信息中体现,有利于向前兼容(用户在低版本和高版本之间反复切换工作,导致迁移复杂度高,存在数据错乱风险)
其他
强制
- 使用正则表达式时,利用好预编译功能(
Pattern.compile("regex")
) - 避免使用Apache Beanutils进行属性的copy(性能较差,可以使用Spring BeanUtils、Cglib BeanCopier,这些都是浅拷贝)
- velocity调用POJO类的属性时,直接使用属性名取值即可,模板引擎会自动按照规范调用POJO的
getXxx()
,如果是boolean基本类型(命名不需要加is前缀),会自动调用isXxx()
方法(如果是Boolean包装类对象,优先调用getXxx()
方法) - 后台输送给页面的变量必须加
$!(var)
--中间的感叹号(如果var为空或不存在,那么$(var)
会直接显示在页面上) Math.random()
方法返回的是double
类型,取值范围是0<=x<1
,如果要获得整数类型的随机数,使用Random
对象的nextInt
或nextLong
方法
推荐
- 不要在视图模板中加入任何复杂逻辑(展示职责)
- 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存
- 及时清理不再使用的代码段或配置信息(避免程序臃肿。在注释掉的之后可能会用的代码加三斜杠
///
注释来说明注释掉代码的理由)
异常日志
错误码
强制
- 错误码的制定原则:快速溯源,沟通标准化,对错误码的理解如下
1. 错误码必须快速知晓错误来源(谁的错)
2. 错误码必须能清晰比对(在代码中容易equals
3. 错误码有利于团队对错误的原因快速达成一致 - 错误码不提供版本号和错误等级(错误码叠加生成(新换老)而不是版本覆盖,错误等级由日志和错误码释义共同判断
- 全部正常不得不填充错误码,以00000代替(阿里内部原则)
- 错误码为字符类型,单字母+4位数字(字母ABC,分别指代用户的错误,业务逻辑错误,第三方服务错误。错误大类之间间隔数字100)(阿里内部原则)
- 编号以先到先得的方式,一经确定,不再变更
- 错误码使用者尽量避免定义新的错误码(成熟业务-技术团队的要求)
- 错误码不能直接输出给用户作为信息展示
推荐
- 错误码之外的独特业务信息由
error_message
携带 - 在获取第三方服务错误码时,向上抛出允许系统转义(C->B)且说明错误详情并带原错误码
参考
- 错误码分三级宏观错误码(即A0001 B0001 C0001),在不清楚用什么具体错误码时可以直接用宏观码(阿里内部原则)
- 错误码的后三位和Http状态码没有关系(设定错误码不和其余信息因素挂钩)
- 错误码有利于和不同文化背景的同事交流信息(国际化)
- 使用纯数字来编码错误码不符合人性(估计是研究过,人对纯数字序列没有直接的拆解解读行为)
异常处理
强制
- Java类库中定义的可以通过预检查方式规避的
RuntimeException
异常不应该通过catch的方式来处理(NullPointerException
IndexOutOfBoundsException
等) - 异常捕获后不要用来做流程控制,条件控制(效率比分支语句低很多)
- catch是分清稳定代码和非稳定代码。对于非稳定代码尽量区分异常类型,再做对应的异常处理
- 捕获异常是为了处理,如果不想处理就抛出给调用者,异常必须处理,且要转化为用户可以理解的内容
- 事务场景中,抛出异常被catch后,如果需要回滚,一定要注意手动回滚事务
- finally块必须对资源对象、流对象进行关闭,有异常也要做
try-catch
(JDK7以上可以使用try-with-resources
方式) - 不要再finally块中使用return(try块中的return语句执行成功后不会马上返回,而finally块中如果有return语句则直接返回,并且丢弃掉try块中的返回点)
- 捕获异常和抛异常,必须完全匹配,或者捕获异常是抛异常的父类
- 调用RPC、二方包、或动态生成类的相关方法时,捕获异常必须使用
Throwable
类来进行拦截(通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException
。什么情况会抛出NoSuchMethodError
呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError
)(没有应用经历,不很理解)
推荐
- 方法的返回值可以为null,不强制返回空集合或空对象等。必须添加注释说明什么情况下会返回null
- 防止NPE是程序员的基本素养,注意NPE产生的场景
* 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱可能会NPE
* 数据库的查询结果可能是null
* 集合里的元素即使isNotEmpty
,取出的数据元素也可能为null
* 远程调用返回对象时,一律要求空指针判断,防止NPE
* Session中获取的数据,建议进行NPE检查,避免空指针
* 级联调用obj.getA().getB().getC()
,易产生NPE(JDK8,使用Optional
来防止NPE) - 定义时区分受检/非受检异常,避免直接抛出
new RuntimeException()
,更不允许直接抛出Exception()
,应使用有业务含义的自定义异常,推荐业界定义过的自定义异常(DAOException
/ServiceException
等)
参考
- 对于公司外的http/api开放接口必须使用errorCode;应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装
isSuccess()
方法、errorCode、errorMessage;应用内部直接抛出异常即可(关于RPC调用使用Result方式的理由(对象):1.如果抛出异常,调用方没有捕获就会产生运行时错误(捕获难度不稳定)2.如果不加栈信息,只有自定义异常和自己理解的errorMessage,对于调用端解决问题意义不大。如果加了栈信息,频繁调用出错,数据序列化和传输的性能损耗也是问题)
日志规约
强制
- 应用中不能直接使用日志系统中的API(Log4J/Logback),应依赖使用日志框架(SLF4J,JCL-jakarta
Commons Logging)的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一
// 日志框架推荐使用方式
// 使用 SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
// 使用 JCL:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);
- 所有日志文件至少保存15天,因为有些异常具备以“周”为频次发生的特点。当天日志,以“应用名.log”来保存,保存在
/home/admin/appName/logs/
目录下,过往日志格式为{logname}.log.{保存日期}
,日期格式为yyyy-MM-dd - 国家法律规定,网络运行状态、网络安全事件、个人敏感信息操作等相关记录留存日志不得少于6个月
- 应用中的扩展日志(打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log
,logName是日志描述,这样可以通过名字就看出日志属于什么应用、类型、目的。推荐对日志分类(例如分为错误日志和业务日志) - 日志输出时,字符串变量之间的拼接使用占位符的方式(直接字符串拼接使用的是StringBuilder的append方式,有一定性能损耗)(占位符:
logger.debug("lalala {} {}", id, symbol)
) - 对于
trace/debug/info
级别的日志输出,必须进行日志级别的开关判断(主要是为了节省调用日志方法时,参数中可能会出现的字符串拼接或者方法调用的性能损失,应使用if(logger.isDebugEnable())
方式来做开关) - 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置
additivity=false
- 生产环境禁止使用
System.out/System.err
输出日志或使用e.printStackTrace()
打印异常堆栈(标准输出和标准错误输出文件每次JBoss(容器)重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超出系统大小限制) - 异常信息应该包括两类信息,如果不处理,那么通过
throws
往上抛
* 案发现场信息(调用方法参数信息等)
* 异常堆栈信息(异常堆栈) - 日志打印时禁止直接使用Json工具将对象转为String(调用其
toString()
方法)(如果对象中某些get()方法被覆写,存在抛出异常的情况)
推荐
- 谨慎记录日志,生产环境禁止输出debug日志;有选择输出info日志。如果使用warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,记得及时删除这些观察日志
* 这条日志有人看吗?
* 看到这条日志能干什么?
* 能不能帮助排查问题 - 可以使用warn日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景中打印error级别,避免频繁报警(error级别只记录系统逻辑出错、异常或更重要的错误信息)
- 尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清,可以使用中文描述,防止产生歧义(国际化团队问题)
单元测试
强制
- 好的单元测试必须遵守AIR原则
A:自动化(automatic)
I:独立性(independent)
R:可重复(repeatable) - 单元测试应该是全自动执行的,并且非交互式的。测试用例通常定期执行,执行过程完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不允许使用
System.out
人工查看结果。需要使用assert
来验证 - 单元测试的独立性。单元测试用例之间绝不能互相调用,也不能依赖执行的先后顺序(单元测试稳定可靠易维护)
- 单元测试是可以重复执行的,不受外界环境影响(单元测试通常会放到持续集成中,每次有代码check in时单元测试会被执行。如果对外部(网络、服务、中间件)有依赖,容易导致持续集成机制不可用)(为了不受外界环境影响,设计代码时就把SUT的依赖改成注入,测试时用Spring这样的DI框架注入一个本地(内存)实现或者Mock实现(这段话理解不能,在学习单元测试和Spring之后再来解读))
- 单元测试要保证测试粒度足够小,有助于精确定位问题。单元粒度至多类级别,一般是方法级别
- 核心业务、核心应用、核心模块的增量代码一定要保证能通过单元测试
- 单元测试代码必须要写在如下工程目录
src/test/java
,不允许写在业务代码目录下(源码编译会跳过这个目录,单元测试框架默认扫描这个目录)
推荐
- 单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率要到100%(工程规约的应用分层中提到的DAO层、Manager层、重用度高的Service,都需要单元测试)(理解不深,以后有类似经验再来细读)
- 编写单元测试代码遵守BCDE原则,保证测试模块的交付质量
* B border 边界值测试,循环边界、特殊取值、特殊时间点、数据顺序等
* C correct 正确的输入,得到预期的结果
* D design 与设计文档结合,来编写单元测试
* E error 强制错误信息输入(非法数据、异常流程、业务允许外等),得到预期结果 - 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,要使用程序插入或者导入数据的方式来准备数据
- 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识
- 对于不可测的代码在适当时机做必要的重构,使代码可测,避免为了达到测试要求而书写不规范测试代码
- 设计评审阶段,开发人员要和测试人员一起确定单元测试范围,最后能覆盖所有测试用例
- 单元测试作为一种质量保障手段,应在项目提测之前完成,不建议项目发布后补充单元测试用例
参考
- 为了更方便进行单元测试,业务代码应避免以下情况
* 构造方法做的事情过多(主要测方法)
* 存在过多全局变量和静态方法(独立性)
* 存在过多外部依赖(独立性)
* 过多条件语句(测试条件暴多) - 不要对单元测试存在以下误解
* 测试同学干的(开发也要承担责任)
* 单元测试代码使多余的(系统的整体功能与各单元部件测试正常是强相关的)
* 单元测试代码不需要维护(时间长了,代码可能就完全不能用了)
* 单元测试和线上故障没有辩证关系(测试代码好坏会直接影响线上状态)
安全规约
强制
- 隶属于用户个人的页面或者功能必须进行权限控制校验(防止没做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信)(横向越权--用户希望攻击相同权限的其他用户,水平权限校验即防止此问题)
- 用户敏感数据禁止直接显示,必须对展示的数据脱敏
- 用户输入的SQL参数严格使用参数绑定或者METADATA字段值限定,防止SQL注入,禁止字符串拼接访问数据库
- 用户请求传入的任何参数必须做有效性验证。忽略参数校验可能会发生
* page size过大导致内存溢出
* 恶意order by
导致数据库慢查询
* 缓存击穿(使用不存在的key进行高并发查询,使得缓存无法命中,频繁击穿到数据库做查询)
* SSRF(Server-Side Request Forgery:服务器端请求伪造。主要是攻击与外网隔绝的内部系统)
* 任意重定向
* SQL注入,Shell注入,反序列化注入(后续再回来细看)
* 正则输入源串拒绝服务ReDoS(后续回来细看)(Java代码用正则来验证用户输入,如果是攻击人员设置的特殊输入,有可能会进入死循环) - 禁止向HTML页面输出未经安全过滤或未正确转义的用户数据
- 表单,AJAX提交必须执行CSRF安全验证(Cross-site request forgery--跨站请求伪造。对于存在CSRF漏洞的应用/网站,攻击者可以实现构造URL,只要受害者用户访问,那么后台可以在用户不知道的情况下对数据库中用户参数进行相应修改)
- URL外部重定向传入的目标地址必须执行白名单(allowList--这不就又歧视了?)
- 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷
推荐
- 发帖、评论、发送即时信息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略
MySQL数据库
建表规约
强制
- 表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是
unsigned tinyint
(1表示是,0表示否)(任何非负字段都要用unsigned
)(POJO类中的布尔变量命名之前有提及,不能用isXxx
命名) - 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑(MySQL在windows下不区分大小写,Linux下区分)
- 标明不使用复数名词(名字只表示实体内容,不表示数量)
- 禁用保留字,如
desc
range
match
delayed
等,即MySQL官方保留字 - 主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名为idx_字段名
- 小数类型为
decimal
,禁止用float和double(float和double都有精度损失的问题,可能在比较值的时候,得不到正确结果。如果存储数超过了decimal的范围,建议将数据拆成整数和小数分开存储) - 如果存储的字符串长度几乎相等,使用char定长字符串类型
- varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果大于此,建议定义为text类型,独立出来一张表用主键来对应,避免影响其它字段索引效率
- 表必备三字段(id-bigint unsigned/单表时自增/步长为1,create_time-datetime,update_time-datetime)
推荐
- 表的命名最好遵循“业务名称_表的作用”(alipay_task/force_project)
- 库名与应用名称尽量一致
- 如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释
- 字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
* 不是频繁修改的字段
* 不是唯一索引的字段
* 不是varchar超长字段,更不能是text
(正例:各业务线冗余存储商品名称--方便查询) - 单表行数超过500万行或单表容量超过2G,推荐分库分表(预计三年后数据量达不到这个级别,那么不要在创建表时就分库分表)
参考
- 合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度(结合字段的实在意义来判断取值范围,然后选取字段类型)
索引规约
强制
- 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引(建立索引影响的insert速度损耗可以忽略,提高查询速度很明显)(如果没有唯一索引,根据墨菲定律,即使在应用层面做了再完善的校验控制,也一定会产生脏数据)(墨菲定律--如果有两种方法做一件事,有一种方法会产生灾难,那么一定有人去采用这种方法,即无论概率多小,都有可能发生)
- 超过三个表禁止join,需要join的字段,数据类型保持绝对一致;多表关联查询,必须保证被关联的字段有索引(双表join也要注意索引、SQL性能)
- varchar字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度(索引长度和区分度是矛盾体,一般对字符串类型数据,长度为20的索引,区分度能到90%。可以使用
count(distinct left(列名,索引长度)/count(*)
的区分度来确定)(后续再回来细看) - 页面搜索严禁左模糊或全模糊,如果需要走搜索引擎解决(索引文件具有B-Tree的最左前缀匹配特性,如果左边值不确定,无法使用此索引)
推荐
- 如果有
order by
的场景,注意利用索引的有序性,order by
最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort
的情况,影响查询性能(正例:where a=? and b=? order by c利用了索引a_b_c。反例:where a > 10 order by b,索引如果包含范围查询,那么索引有序性无法利用,这种情况索引a_b无法排序) - 利用覆盖索引来进行查询操作,避免回表(没用过,后续再理解)(覆盖索引只是查询的一种效果,用explain的结果,extra列会出现,using index)
- 利用延迟关联或者子查询优化超多分页场景(MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写)(需要再回来细看)
- SQL性能优化的目标(至少达到range级别,要求是ref级别,最好是consts级别)
1. consts单表中最多只有一个匹配行(主键或唯一索引),优化阶段即可读到数据
2. ref指使用普通索引
3. range对索引进行范围检索
(反例:explain表的结果,type=index,索引物理文件全扫描,速度非常慢) - 建组合索引时,区分度最高的放在最左边(在等号和非等号混合判断条件情况下,建索引时把等号条件的列前置(此时不管区分度高低))
- 防止因字段类型不同造成的隐式转换,导致索引失效
参考
- 创建索引时避免有如下极端误解
* 索引宁滥勿缺,认为一个查询就需要一个索引
* 吝啬索引创建,认为索引会消耗空间,拖慢记录更新和行的新增速度
* 抵制唯一索引,认为唯一索引一律需要在应用层通过先查后插的方式解决
SQL语句
强制
- 不要使用count(列名)或count(常量)来替代count(*),count(*)是SQL92定义的标准统计行数的语法,和数据库无关,和NULL 非NULL无关(count(*)会统计值为NULL的行,而count(列名)不会统计)
- count(distinct col)计算该列除NULL以外的不重复行数,注意count(distinct col1, col2)`,如果其中一列全是NULL,那么即使另一列有不同值,也返回0
- 当一列值全是NULL,count(col)的返回结果是0,但sum(col)的返回结果为NULL,使用sum()时要注意NPE问题
- 使用
ISNULL()
来判断是否为NULL值(NULL值与任何值比较都为NULL)
1. NULL<>NULL 返回NULL
2. NULL=NULL 返回NULL
3. NULL<>1 返回NULL - 代码中写分页查询逻辑,若count为0应直接返回,避免执行后面的分页语句
- 不得使用外键与级联,一切外键概念必须在应用层解决(外键和级联更新适用于单机低并发。高并发场景下,级联更新是强阻塞,存在数据库更新风暴风险;外键影响数据库插入性能)
- 禁止使用存储过程(难以调试和扩展,没有移植性)
- 数据订正(删除或修改记录操作)时,要先select,避免误删除,确认无误才能执行更新
- 对于数据库中表记录的查询和变更,只要涉及多表,都要在列名前加表的别名(表名)来限定
推荐
- SQL语句中表的表明前加as,并且以t1 t2 t3顺序依次命名
- in操作能避免则避免,实在避免不了,需要评估in后边的集合元素数量,控制在1000个之内
参考
- 国际化需要,所有字符存储和表示均采用utf8字符集,那么字符计数方法要注意(存储表情选择utf8mb4进行存储,注意与utf8的区别)
- TRUNCATE TABLE比DELETE速度快,且使用系统和事务资源少,但是无事务且不触发trigger,容易造成事故,不建议在开发代码中使用此语句
ORM映射
强制
- 表查询中一律不要使用 * 作为查询的字段列表,需要的字段要明确写明。缺点:
1. 增加查询分析器解析成本
2. 增减字段容易与resultMap配置不一致
3. 无用字段增加网络消耗,尤其是text字段 - POJO类的布尔变量不能加is,数据库字段必须加is_,要求在resultMap中进行字段和属性的映射(重复再三了)
- 不要用resultClass当返回参数,即使所有类属性名与数据库字段一一对应,也要定义
,反过来每个表也必然有一个 与之对应(字段与DO解耦,方便维护) - sql.xml配置参数使用 #{} #param#,不要使用 ${},这种方式容易出现SQL注入(没碰到过,标记一下)
- iBATIS自带的
queryForList(String statemenName, int start, int size)
不推荐使用(其实现方式是在数据库取到statementName对应的SQL语句的所有记录,再通过subList取start、size的子集) - 不允许直接拿HashMap与Hashtable作为查询结果集的输出(这里反例是出现了数据转换问题,正例没有著名,待学习iBATIS框架之后再细看)
- 更新数据表记录时,必须同时更新记录对应的update_time为当前时间
推荐
- 不要写一个大而全的数据更新接口。传入POJO类,无论是改哪个字段都全部update。执行SQL,不更新无需改动的字段(易出错;效率低;增加binlog存储)
参考
- @Transactional事务不要滥用,影响数据库的QPS,使用事务的地方需要考虑各方面的回滚方案(缓存回滚、搜索引擎回滚、消息补偿、统计修正)(数据库事务要单开一篇学习)
中的comparaValue是与属性值对比的常量,一般是数字,表示相等时带上此条件; 表示不为空且不为NULL时执行 表示不为null值时执行(学了框架再来细看,标记)
工程结构
应用分层
推荐
- 根据业务架构实践,结合业界分层规范与流行技术架构分析,推荐分层结构:
默认上层依赖下层,箭头表示可以直接依赖
- 开放API层:可直接封装Service接口暴露成RPC接口;通过Web封装成http接口;网关控制层
- 终端显示层:各个端的模板渲染并执行显示的层
- Web层:对访问控制进行转发,各类基本参数校验,或不复用的业务简单处理
- Service层:具体的业务逻辑层
- Manager层:通用业务处理层,特征如下
1. 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口(对外部的封装,给上层提供通用接口)
2. 对Service层通用能力的下沉,如缓存方案、中间件通用处理
3. 与DAO层交互,对多个DAO组合复用 - DAO层:数据访问层,与底层各种数据库进行数据交互
- 第三方服务:其它部门的RPC服务接口,基础平台,其它公司的HTTP接口
- 外部数据接口:外部(应用)数据存储服务提供的接口(多见于数据迁移场景中)
参考
- 分层异常处理规约
* DAO层异常类型很多,无法细粒度的异常进行catch,所以使用catch(Exception e)
方式,并且抛出throw new DAOException(e)
,不需要打印日志,因为Service/Manager层一定会捕获并且打印日志
* Service层出现异常,必须记录出错日志到磁盘,尽可能带上参数信息(保护案发现场)
* Manager层和Service同机部署,日志方式与DAO层处理一致(上抛),如果单独部署则同Service层
* Web层绝不应继续往上抛异常,因为已经处于顶层。如果这个异常导致渲染页面出错,应该转到友好错误页面并提供友好的错误提示信息
* 开放接口层要将异常处理成错误码和错误信息方式返回 - 分层领域模型规约
* DO(Data Object):与数据库表结构一一对应
* DTO(Data Transfer Object):Service或Manager层传输的对象
* BO(Business Object):封装业务逻辑的,由Service层输出的对象
* Query:数据查询对象,各层接收上层的请求。超过2个参数的查询封装不允许使用Map类传输(标记,数据类型转换问题?应该使用字符串?)
* VO(View Object):显示层对象
二方库依赖
强制
- GAV遵从以下规则
* GroupID:com.{公司/BU}.业务线[.子业务线] 最多4级
* ArtifactID:产品线名-模块名。语意不重复不遗漏
* Version:阿里内部设计 - 二方库版本号命名方式:主版本号.次版本号.修订号
* 主版本号:产品方向改变,大规模API不兼容,架构不兼容升级
* 次版本号:相对兼容性,增加主要功能特性,影响范围极小的API不兼容修改
* 修订号:完全兼容性,修复BUG,新增次要功能 - 版本号起始必须是
1.0.0
- 线上应用不要依赖
SNAPSHOT
版本(安全包除外)(保持幂等性(对于同一个系统,同样的条件,一次请求和多次请求的影响必须相同),加快编译时的打包构建) - 二方库的新增或升级,保持除功能点之外的其它jar包仲裁结果不变,如果改变,必须明确评估和验证(看不懂,标记一下,再回来细看)
- 二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的POJO对象
- 依赖于一个二方库群时,必须定义一个统一的版本变量,避免版本号不一致
- 禁止在子项目的pom依赖中出现相同的GroupID,ArtifactID,但是不同的Version(在本地调试时会使用各子项目指定的版本号,但是合并成一个 war,只能有一个版本号出现在最后的lib 目录中。曾经出现过线下调试是正确的,发布到线上却出故障的先例)
推荐
- 底层基础技术框架、核心数据管理平台、近硬件端系统谨慎引入第三方实现(那得有能力自己实现啊)
- 所有pom文件中的依赖声明放在
语句块中,所有版本仲裁放在 语句块中( 里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖,version 和 scope 都读取自父 pom。而 所有声明在主 pom 的 里的依赖都会自动引入,并默认被所有的子项目继承) - 二方库不要有配置项,最低限度不要再增加配置项(直接使用)
- 不要使用不稳定的工具包或Utils类(无法做到向下兼容的包,编译通过,运行时异常)
参考
- 避免应用二方库的依赖冲突问题,二方库发布者应遵循以下原则
1. 精简可控(移除一切不必要的API和依赖,只包含Service API、必要的领域模型对象、Utils类、常量、枚举等。如果依赖其它二方库,尽量是provided引入,让二方库使用者去依赖具体版本号;无log具体实现,依赖框架)
2. 稳定可追溯(每个版本变化需要记录,由谁维护,源码在哪。除非用户主动升级,否则公共二方库的行为不应再发生变化)
服务器
推荐
- 高并发服务器建议调小TCP协议的time_wait超时时间(默认240秒)(在 linux 服务器上请通过变更/etc/sysctl.conf 文件去修改该缺省值 net.ipv4.tcp_fin_timeout = 30(s))
- 调大服务器所支持的最大文件句柄数(File Descriptor,简写为 fd)(主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd。主流的linux服务器默认所支持最大fd数量为1024,当并发连接数很大时很容易因为fd不足而出现“open too many files”错误,导致新的连接无法建立。建议将 linux 服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关))
- JVM环境参数设置
-XX:+HeapDumpOnOutOfMemoryError
,JVM碰到OOM场景时输出dump信息 - 线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力
参考
- 服务器内部重定向必须使用forward;外部重定向必须使用
URL Broker
生成,否则因线上采用HTTPS协议而导致浏览器提示”不安全“,此外还会带来URL维护不一致的问题(不知道什么时URL Broker,标记一下以后细看)
设计规约
就是UML的实际应用,感觉这个不是自己能引导的东西,需要团队有这个素质
强制
- 存储方案和底层数据结构的设计获得评审一致通过,并沉淀成为文档(有缺陷的底层数据结构容易导致系统风险上升,可扩展性下降,重构成本也会因历史数据迁移和系统平滑过渡而陡然增加,所以,存储方案和数据结构需要认真地进行设计和评审,生产环境提交执行后,需要进行 double check)(评审内容包括存储介质选型、表结构设计能否满足技术方案、存取性能和存储空间能否满足业务发展、表或字段之间的辩证关系、字段名称、字段类型、索引等;数据结构变更(如在原有表中新增字段)也需要进行评审通过后上线)
- 在需求分析阶段,如果与系统交互的 User 超过一类并且相关的 User Case 超过 5 个,使用用例图来表达更加清晰的结构化需求
- 如果某个业务对象的状态超过 3 个,使用状态图来表达并且明确状态变化的各个触发条件(状态图的核心是对象状态,首先明确对象有多少种状态,然后明确两两状态之间是否存在直接转换关系,再明确触发状态转换的条件是什么)(例:淘宝订单)
- 如果系统中某个功能的调用链路上的涉及对象超过 3 个,使用时序图来表达并且明确各调用环节的输入与输出(时序图反映了一系列对象间的交互与协作关系,清晰立体地反映系统的调用纵深链路)
- 如果系统中模型类超过 5 个,并且存在复杂的依赖关系,使用类图来表达并且明确类之间的关系(类图像建筑领域的施工图,如果搭平房,可能不需要,但如果建造蚂蚁 Z 空间大楼,肯定需要详细的施工图)
- 如果系统中超过 2 个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活动图来表示(活动图是流程图的扩展,增加了能够体现协作关系的对象泳道,支持表示并发等)
推荐
- 系统架构设计时明确以下目标:
* 确定系统边界。确定系统在技术层面上的做与不做。
* 确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。
* 确定指导后续设计与演化的原则。使后续的子系统或模块设计在一个既定的框架内和技术方向上继
续演化。
* 确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等。 - 需求分析与系统设计在考虑主干功能的同时,需要充分评估异常流程与业务边界(反例:用户在淘宝付款过程中,银行扣款成功,发送给用户扣款成功短信,但是支付宝入款时由于断网演练产生异常,淘宝订单页面依然显示未付款,导致用户投诉)
- 类在设计与实现时要符合单一原则(单一原则最易理解却是最难实现的一条规则,随着系统演进,很多时候,忘记了类设计的初衷)(切记单一职责,而非无脑在旧类上扩写)
- 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现(不得已使用继承的话,必须符合里氏代换原则,此原则说父类能够出现的地方子类一定能够出现,比如,“把钱交出来”,钱的子类美元、欧元、人民币等都可以出现)
- 系统设计阶段,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护(低层次模块依赖于高层次模块的抽象,方便系统间的解耦)
- 系统设计阶段,注意对扩展开放,对修改闭合(极端情况下,交付的代码是不可修改的,同一业务域内的需求变化,通过模块或类的扩展(接口的扩展)来实现)
- 系统设计阶段,共性业务或公共行为抽取出来公共模块、公共配置、公共类、公共方法等,在系统中不出现重复代码的情况,即 DRY 原则(Don't Repeat Yourself)(说明:随着代码的重复次数不断增加,维护成本指数级上升。随意复制和粘贴代码,必然会导致代码的重复,在维护代码时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化)
- 避免如下误解:敏捷开发 = 讲故事 + 编码 + 发布(说明:敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摒弃传统的审批流程,但核心关键点上的必要设计和文档沉淀是需要的)
- 设计文档的作用是明确需求、理顺逻辑、后期维护,次要目的用于指导编码(说明:避免为了设计而设计,系统设计文档有助于后期的系统维护和重构,所以设计结果需要进行分类归档保存)
- 可扩展性的本质是找到系统的变化点,并隔离变化点(世间众多设计模式其实就是一种设计模式即隔离变化点的模式)(极致扩展性的标志,就是需求的新增,不会在原有代码交付物上进行任何形式的修改--这可太难了)
参考(企业开发人员高级素养)
- 设计的本质就是识别和表达系统难点(识别和表达完全是两回事,很多人错误地认为识别到系统难点在哪里,表达只是自然而然的事情,但是大家在设计评审中经常出现语焉不详,甚至是词不达意的情况。准确地表达系统难点需要具备如下能力: 表达规则和表达工具的熟练性。抽象思维和总结能力的局限性。基础知识体系的完备性。深入浅出的生动表达力)
- 代码即文档的观点是错误的,清晰的代码只是文档的某个片断,而不是全部(代码的深度调用,模块层面上的依赖关系网,业务场景逻辑,非功能性需求等问题是需要相应的文档来完整地呈现的)(个人对此持保留意见,不过代码即文档确实理想化)
- 在做无障碍产品设计时,需要考虑到:(怕是碰不到)
* 所有可交互的控件元素必须能被 tab 键聚焦,并且焦点顺序需符合自然操作逻辑。
* 用于登录校验和请求拦截的验证码均需提供图形验证以外的其它方式。
* 自定义的控件类型需明确交互方式。
(正例:用户登录场景中,输入框的按钮都需要考虑 tab 键聚焦,符合自然逻辑的操作顺序如下,“输入用户名,输入密码,输入验证码,点击登录”,其中验证码实现语音验证方式。如果有自定义标签实现的控件设置控件类型可使用 role 属性)