一位攻城狮的自我修养,在于良好的编程规范
命名风格
类名使用 UpperCamelCase 风格,但下列情形除外:
DO: Data Object. 与数据库表结构一一对应,通过 DAO 层向上传输数据源对象
BO: Business Object,业务对象. 由 Service 层输出的封装业务逻辑的对象
DTO: Data Transfer Object,数据传输对象. Service 或 Manager 向外传输的对象
VO: View Object,显示对象. 通常是 Web 向模板渲染引擎层传输的对象
AO: Application Object,应用对象. 在 Web 层与 Service 层之间抽象复用的对象模型
PO: POJO 缩写,Plain Ordinary Java Object. 专指只有 setter/getter/toString 的简单类,包括 DO,DTO,BO,VO 等, 禁止使用 xxxPOJO 来命名
UID
方法名,参数名,成员变量,局部变量都统一使用 lowerCamelcase 风格
常量命名全部大写,单词间用下划线隔开, 力求语义表达完整清楚,不要嫌名字长
抽象类命名使用 Abstract 或者 Base 开头
异常类命名使用 Exception 结尾
测试类命名要以要测试的类的名称命名,以 Test 结尾
类型与中括号紧挨来表示数组
POJO 类中布尔类型的变量都不要加 is 前缀,在部分框架中会引起序列化错误
包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词.包名统一使用单数形式.但是类名如果有复数含义,可以使用复数形式
杜绝不规范的缩写,避免望文不知义
为了达到代码自解释的目标,任何自定义的编程元素在命名时,使用尽量完整的单词组合来表达含义
在常量与变量命名的同时,表示类型的名词放在词尾,以提升辨识度
如果模块,接口,类,方法使用了设计模式,在命名时需要体现出设计模式
接口类中的方法和属性不要加任何修饰符号(不要加 public), 保持代码的简洁
尽量不要在接口中定义变量,如果一定要定义变量,一定是与接口方法有关的,并且是整个应用的基础变量
接口方法签名: void commit()
接口基础常量: String COMPANY="Oxford"
接口和实现类:
对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口的区别
如果是形容能力的接口名称,去对应的形容词为接口(-able 的形式)
枚举类带上 Enum 后缀,枚举成员名称需全部大写
枚举类是特殊的类,域成员均为常量,且构造方法被默认强制是私有的
各层命名规范:
Service 或者 DAO 层方法命名规范:
获取单个对象的方法用 get 做前缀
获取多个对象的方法用 list 做前缀 ,复数形式结尾
获取统计值的方法用 count 做前缀
插入方法使用 save 或者 insert 做前缀
删除的方法使用 remove 或者 delete 做前缀
修改的方法使用 update 做前缀
领域模型命名规范:
数据对象: XxxDO,Xxx 为数据表名
数据传输对象: XxxDTO,Xxx 为业务领域相关的名称
展示对象: XxxVO,xxx 一般为网页名称
POJO 为 DO,DTO,BO,VO 的统称,禁止命名成 XxxPOJO
常量定义
不允许任何未经预先定义的常量出现在代码中
在 long 或者 Long 赋值时,数值后使用大写的 L, 不能是小写的 l. 因为小写容易和数字 1 混淆,造成误解
不要使用一个常量类维护所有常量,要按常量的功能进行归类,分开维护
大而全的常量类杂乱无章,使用查找功能才能定位到修改的常量,不利于理解和维护
常量的复用层次有五层:
跨应用共享常量: 放置在二方库中,通常是 client.jar 中的 constant 目录下
应用类共享常量 放置在一方库中,通常是子模块中的 constant 目录下
子工程内共享常量 在当前子工程的 constant 目录下
包内共享常量 在当前包的 constant 目录下
类内共享常量 直接在类内部 private static final 定义
如果变量值仅在一个固定范围内变化,使用 enum 类型定义
如果存在名称之外的延伸属性应使用 enum 类型,比如季节,表示一年中第几个季节:
代码格式
大括号的使用约定:
如果大括号内为空,则简洁地写成 { } 即可,不需要换行
如果是非空代码块:
左大括号前不换行
左大括号后换行
右大括号前换行
右大括号后如果还有 else 则不换行
表示终止的右大括号后必须换行
小括号的使用约定:
左小括号和字符之间不要出现空格
右小括号和字符之间也不要出现空格
左大括号之前需要空格
if,for,while,switch,do 等保留字与括号之间都必须加空格
任何二目,三目运算符左右两边都需要加一个空格
运算符包括:
赋值运算符 :=
逻辑运算符 :&&
加减乘除符号
采用 4 个空格进行缩进
注释的双斜线与注释内容之间有且仅有一个空格
方法参数在定义和传入时,多个参数逗号后面必须加空格
单个方法的总行数不要超过 80 行:
除注释之外的方法签名,左右大括号,方法内代码,空行,回车及任何不可见字符的总行数不超过 80 行
代码逻辑分清红花和绿叶,个性和共性:
绿叶逻辑单独出来成为额外的方法,使主干代码更加清晰
共性逻辑抽取成共性方法,便于复用和维护
不需要增加若干空格来使某一行的字符与上一行对应位置的字符对齐
不同逻辑,不同语义,不同业务代码之间只需要插入一个空行分割来提升可读性即可
OPP 规约
避免通过一个类的对象引用访问类的静态变量和静态方法,这会增加编译器的解析成本,直接使用类名访问即可
所有的覆写方法,必须加 @Override
相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免可变参数使用 Object 类型
可变参数必须放置在参数列表的最后, 建议尽量不要用可变参数编程
外部正在调用的或者二方库依赖的接口,不允许修改方法签名(方法名和参数列表),避免对接口的调用方产生影响 .接口过时必须加上 ==@Deprecated== 注解,并清晰地说明采用的新接口和新服务是什么
不能使用过时的类或方法:
接口的提供方既然明确是过时接口,那么有义务提供新接口
作为调用方,有义务考证过时方法的新实现是什么
Object 的 equals 方法容易抛出空指针异常,应使用常量或者确定有值的对象来调用 equals
"test".equals(Object)
推荐使用 java.util.objects
所有相同类型的包装类对象之间的值的比较,全部使用 equals 方法比较
对于 Integer var = ? 在-128 至 127 范围内赋值时 ,Integer 对象是在 IntegerCache.cache 中产生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断
但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,所以推荐使用 equals 方法进行比较
任何货币金额,均以最小货币单位且整型类型来进行存储
浮点数之间的等值判断:
基本类型不能用 == 来比较
包装类型不能使用 equals 来判断
浮点数采用尾数+阶码的编码方式,类似与科学计数法有效数字+指数的表示方式. 二进制无法精确表示大部分十进制小数
为了避免出现问题,所有的浮点数都使用 BigDecimal 定义
定义数据对象 DO 类时,属性类型要与数据库字段类型相匹配
数据库字段的 bigint 必须与类属性 Long 类型相对应
禁止使用构造方法 BigDecimal(double) 的方式将 double 值转化为 BigDecimal 对象:
BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中会导致业务逻辑异常
推荐使用入参为 String 的构造方法
或者使用 BigDecimal 的 valueOf 方法: 此方法内部执行了 Double 的 toString,实际能表达的精度对尾数进行了截断
基本类型和包装类型的使用标准:
所有的 POJO 类属性必须使用包装类数据类型
RPC 方法的返回值和参数必须使用包装数据类型
所有的局部变量使用基本数据类型
定义 DO,DTO,VO 等 POJO 类时,不要设定任何属性默认值
序列化类新增属性时,不能修改 serialVersionUID 字段,这样会导致反序列化失败;如果完全不兼容升级,避免反序列化混乱,可以修改 serialVersionUID 值.在 serialVersionUID 不一致时会抛出序列化运行时异常
构造方法中禁止加入任何业务逻辑,如果有初始化逻辑,要放在 init 中
POJO 类必须写 toString 方法.如果继承了一个 POJO 类,需要在前面添加 super.toString
这样在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印属性值,便于排查问题
禁止在 POJO 类中,同时存在对应属性 Xxx 的 isXxx() 和 getXxx() 方法
框架在调用属性 Xxx 的获取方法时,不能确定哪个方法一定是被优先调用到的
使用索引访问用 String 的 split 方法得到的数组时,需要做最后一个分隔符后有无内容的检查, 否则会有 IndexOutofBoundsException 异常
当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读
类内方法定义的顺序依次为:
公有方法或者保护方法
公有方法是类调用者或者维护最频繁使用的方法,最好首先展示
保护方法尽管是子类需要的方法,但也可能是模板设计模式中的核心方法
私有方法
私有方法外部一般不需要关心,是一个黑盒实现
getter 或者 setter 方法
所有 Service 和 DAO 的 getter 或者 setter 方法都放在类的最后
setter 方法中,参数名称要和类成员变量名称一致 ,this.成员名=参数名.
在 getter 或者 setter 方法中,不要增加业务逻辑
循环体内,字符串的类连接方式,使用 StringBuilder 的 append 方法进行扩展
否则会导致每次循环都会 new 一个新的 StringBuilder 对象
然后再进行 append 操作
最后通过 toString 方法返回 String 对象,造成资源浪费
final 可以声明类,成员变量,方法,以及本地变量. 使用 final 的情况:
不允许被继承的类
String
不允许修改的引用的域对象
不允许被重写的方法
POJO 中的 setter 方法
不允许运行过程中重新赋值的局部变量
避免上下文重复使用一个变量,使用 final 描述可以强制重新定义,方便更好地进行重构
不要使用 Object 的 clone 方法拷贝对象:
对象的 clone 方法默认是浅拷贝
若想实现深度拷贝需要重写 clone 方法实现域对象的深度遍历拷贝需要重写 clone 方法实现域对象的深度遍历拷贝
类成员与方法访问控制规约:
如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private
工具类不允许有 public 或者 default 构造方法
类非 static 成员变量并且与子成员共享,必须是 protected
类非 static 成员变量并且仅在本类中使用,必须是 private
类 static 成员变量如果仅在本类中使用,必须是 private
若是 static 成员变量,考虑是否为 final
类成员方法只供类内部调用时,必须是 private
类成员方法只对继承类公开时,限制使用 protected
日期时间
日期格式化时,传入 pattern 中表示年份统一使用小写的 yyyy
日期格式化时:
yyyy 表示当天所在的年
YYYY 表示当天所在的周属于的年份,一周从周日开始,至周六结束.如果本周跨年,返回的 YYYY 就是下一年
在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 的含义:
表示月份的是大写的 M
表示分钟的是小写的 m
24 小时的是大写的 H
12 小时的是小写的 h
获取当前的毫秒数 :System.currentTimeMillis()
如果想要获取更加精确的纳秒级的时间值,使用 System.nanoTime()
针对统计时间的场景,推荐使用 Instant
不要使用 java.sql 中的相关时间方法
不要在程序中写死一年的为 365,避免在公历闰年时出现日期转换错误或程序逻辑错误
使用 LocalDate 方法
使用 Calendar 中的枚举值来指代月份
如果使用数字,要注意 Date,Calendar 等日期相关类的月份 month 的值在 0 - 11 之间
集合处理
hashCode 和 equals 的处理:
只要重写 equals, 就必须重写 hashCode
Set 中存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法
如果自定义对象作为 Map 的键,必须重写 hashCode 和 equals
String 重写了 hashCode 和 equals 方法所以可以使用 String 对象作为 key 来使用
ArrayList 的 subList 结果不可以强转成 ArrayList,否则会抛出 ClassCastException 异常:
subList 返回的是 ArrayList 的内部类 SubList, 并不是 ArrayList, 而是 ArrayList 的一个视图.对于 SubList 子列表的所有操作最终会反映到原列表上
在 subList 场景中,要注意对原集合元素的增加或者删除,都会导致子列表的遍历,增加和删除产生 ConcurrentModificationException 异常
使用 Map 的方法:
keySet()
values()
entrySet()
返回集合对象时,不可以进行添加元素的操作,否则会抛出 UnsupportedOperationException 异常
Collections 类返回的对象不可以进行添加或者删除操作:
如果查询无结果,则返回 Collection.emptyList()空集合对象. 调用方一旦进行了添加元素操作,就会触发 UnsupportedOperationException 异常
使用集合转数组的方法,必须使用集合的 toArrary(T[] array), 传入的是类型完全一样的数组,数组的大小就是 list.size()
使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组的地址;
如果数组元素个数大于实际所需,下标为[list.size()] 的元素的数组元素将被置为 null,其余数组元素保持原值
因此最好将方法入参数组大小定义为与集合元素个数一致
在使用 Collection 接口任何实现类的 addAll() 方法时,都要对输入集合参数进行 NPE 判断
使用工具类 Arrays.asList()将数组转换成集合时,不能使用这个相关的修改集合的方法,这个集合的 add, remove, clear 方法会抛出 UnsupportedOperationException 异常
asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法
Arrays.asList 体现的是适配器模式,只是转换接口,后台数据依旧是数组
泛型通配符 <? extends T> 来接收返回的数据,这种写法的泛型集合不能使用 add 方法 ;<? super T> 不能使用 get 方法,作为接口调用赋值时会出错
PECS(Producer Extends Consumer Super)原则:
频繁往外读取内容,适合使用<? extends T>
经常往里插入的,适合使用<? super T>
不要在 foreach 循环里进行元素的 remove 或者 add 操作
remove 元素要使用 Iterator 方式,如果是并发操作,要对 Iterator 对象加锁
在 JDK 7 以后的版本中 ,Comparator 实现要满足三个条件,否则 Arrays.sort, Collections.sort 会出现 IllegalArgumentException 异常:
x, y 的比较结果和 y, x 的比较结果相反
x > y, y > z, 则 x > z
x = y, 则 x, z 比较结果和 y, z 比较结果相同
在 JDK 7 以后的版本中,给集合的泛型定义时,使用全省略,即直接使用 <> 来指定前边已经指定的类型
集合初始化时,指定集合初始值大小
HashMap 使用 HashMap(int initialCapacity) 初始化
initalCapacity = (需要存储的元素个数 / 负载因子) + 1. 注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值的大小,设为为默认值 16
使用 entrySet 遍历 Map 类集合 kv, 而不是使用 keySet 方式进行遍历
如果使用 keySet 方式遍历,其实是遍历了两次:
一次转换为 Iterator 对象
一次从 hashMap 中取出 key 所对应的 value
entrySet 只是遍历一次就把 key 和 value 都放到了 entry 中,效率更高
如果是 JDK 8 以后的版本,使用 Map.foreach 方法
示例:
values()返回的是 V 值集合,是一个 list 集合对象
keySet()返回的是 K 值集合,是一个 Set 集合对象
entrySet()返回的是 K-V 值组合集合
要注意 Map 类集合中的 K-V 能不能存储 null 值的情况:
由于 HashMap 的干扰,误以为 ConcurrentHashMap 可以置入 null 值,其实这样会抛出 NPE 异常
合理利用集合的有序型 - sort 和集合的稳定性 - order, 避免集合的无序性 - unsort 和不稳定性 - unorder 带来的负面影响
有序性是指遍历的结果按照某种比较规则依次排列的
稳定性是指集合每次遍历的元素次序是一定的
ArrayList, HashMap, TreeSet
利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作
避免使用 List 的 contains 方法进行遍历,对比,去重操作
并发处理
获取单例对象需要保证线程安全,其中的方法也要保证线程安全
资源驱动类, 工具类, 单例工厂类都需要注意
创建线程或者线程池时要指定有意义的线程名称,方便出错时回溯
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题
线程池不允许使用 Executors 创建,要通过 ThreadPoolExecutors 创建,这样可以让人更加明确线程池的运行规则,规避资源消耗的风险
Executors 返回线程池对象存在以下问题:
FixedThreadPool 和 SingleThreadPool:
允许请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,导致 OOM
CachedThreadPool 和 ScheduledThreadPool:
允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,导致 OOM
SimpleDateFormat 是线程不安全类,不要定义为 static 变量.如果定义为 static,必须加锁,或者使用 DateUtils 工具类
注意线程安全,使用 DateUtils,可以进行如下处理:
在 JDK 8 中,可以使用:
Instant 代替 Date
LocalDateTime 代替 Calendar
DateTimeFormatter 代替 SimpleDateFormat
必须回收自定义的 ThreadLocal 变量:
尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,会影响后续业务逻辑和造成内存泄漏的问题
尽量在代理中使用 try - finally 块进行回收
高并发时,同步调用应该考量锁的性能损耗.
能用无锁数据结构,就不要用锁
能用锁区块,就不要锁整个方法体
能用对象锁,就不要用类锁
尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法
对多个资源, 数据库表, 对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
如果线程一需要对 A, B, C 依次全部加锁后才可以进行更新操作
那么线程二的加锁顺序也必须是 A, B, C.否则可能会出现死锁
在使用阻塞等待获取锁的方式中:
必须在 try 代码块之外
如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常 ,导致在 finally 代码块中 ,unlock 对未加锁的对象解锁,会调用 AQS 的 tryRelease 方法,抛出 IlleagalMonitorStateException 异常
必须在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁
如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法获取锁
在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,导致 unlock 对未加锁的对象解锁,会调用 AQS 的 tryRelease 方法,抛出 IlleagalMonitorStateException 异常
在使用尝试机制来获取锁的方式中:
进入业务代码之前,必须先判断当前线程是否持有锁
锁的释放规则与锁阻塞等待的方式相同
Lock 对象的 unlock 方法在执行时,会调用 AQS 的 tryRelease 方法,如果当前线程不持有锁, 则抛出 IllegalMonitorStateException 异常
并发修改同一记录时,避免更新丢失,需要加锁:
在应用层加锁
在缓存加锁
在数据库中加锁
使用 version 作为更新依据
如果每次访问概率小于 20%, 推荐使用乐观锁
否则的话,使用悲观锁
乐观锁的重试次数不得小于 3 次
多线程并行处理定时任务时:
Timer 运行多个 TimerTask 时只要其中之一没有捕获抛出的异常,任务便会自动终止运行
使用 ScheduleExecutorService 则没有这个问题
悲观锁遵循一锁二判三更新四释放的原则
使用 CountDownLatch 进行异步转同步操作:
每个线程退出前必须调用 countDown 方法
线程执行代码注意 catch 异常,确保 counDown 方法被执行到
避免主线程无法执行至 await 方法,直到超时才返回结果
子线程抛出的异常堆栈,不能在主线程 try-catch 得到异常
避免 Random 实例被多线程使用,共享该实例是线程安全的,但是会因为竞争同一个 seed 导致性能下降
Random 实例:
java.util.Random 的实例
Math.random() 的方式
在 JDK 7 后,可以直接使用 ThreadLoalRandom
在并发的场景下,通过双重检查锁 double-check locking 实现延迟初始化来优化问题隐患:
将目标属性声明为 volatile 型
volatile 用于解决多线程内存不可见问题:
对于一写多读,可以解决变量同步问题
对于多写,无法解决线程安全问题
对于 count++操作,使用如下的类实现:
在 JDK 8 后,推荐使用 LongAdder 对象,比 AtomicLong 性能更好,因为可以减少乐观锁的重试次数
HashMap 在容量不够进行 resize 操作时会由于高并发可能出现死锁,导致 CPU 增加:
使用其它的数据结构
加锁
ThreadLocal 无法解决共享对象的更新问题,建议要使用 static 进行修饰:
这个变量是针对一个线程内所有操作共享的
因此设置为静态变量,所有的此类实例共享此静态变量
即这个变量在类第一次被使用时装载,只分配一块内存空间,只要这个线程内定义的所有此类的对象都可以操作这个变量
控制语句
在一个 switch 块内:
每个 case 要通过 break 或者 return 来终止
或者注释说明程序将继续执行到哪一个 case 为止
必须包含一个 default 语句并且放在最后,即使是空代码
当 Switch 括号内的变量类型为 String 并且此变量为外部参数时,必须进行 null 判断
在 if, else, for, while, do 语句中必须使用大括号,即使只有一行代码,避免采用单行编码模式
三目运算符: condition ? 表达式 1 : 表达式 2 要注意表达式 1 和表达式 2 在类型对齐时,可能因自动拆箱导致 NPE 异常
触发类型对齐的拆箱操作:
表达式 1 或者表达式 2 只要有一个原始类型
表达式 1 或者表达式 2 类型不一致,会强制拆箱升级成表示范围更大的那个类型
在高并发的场景中,避免使用 “等于” 判断作为中断或者退出的条件
因为如果并发控制没有处理好,容易产生等值判断被 “击穿” 的情况 .要使用大于或者小于区间判断条件来代替
示例: 判断剩余数量等于 0 时,当数量等于 0 的过程中,由于并发处理错误导致数量瞬间变成了负数,这样的话,处理无法终止
表达异常的分支时,不要使用 if - else 方式,改写为
对于超过 3 层的 if - else 的逻辑判断代码可以使用卫语句,策略模式,状态模式等实现
除常用的方法 :getXxx, isXxx 等,不要在条件判断中执行复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性
很多 if 语句内的逻辑相当复杂,需要分析表达式的最终结果,才能明确什么样的条件执行什么样的语句
不要在其它表达式中(尤其时条件表达式),插入赋值语句
循环体中的语句要考量性能,以下操作尽量移动至循环体外处理:
定义对象,变量
获取数据库连接
进行不必要的 try - catch 操作(考虑这个 try - catch 操作是否可以移动至循环体外)
避免使用取反逻辑运算符
取反逻辑运算符不利于快速理解
取反逻辑写法必然存在对应的正向逻辑写法
接口入参保护: 这种场景常见的是用作批量操作的接口
参数校验:
需要进行参数校验的情形:
调用频次低的方法
执行时间开销很大的方法
此情形中,参数校验的时间几乎可以忽略不计
但是如果因为参数错误导致中间执行被退回,或者错误,就得不偿失
需要极高稳定性和可用性的方法
对外提供开放接口,无论是 RPC, API, HTTP 接口
敏感权限入口
不需要进行参数校验的情形:
极有可能被循环调用的方法. 但是在方法说明里必须注明外部参数的检查要求
底层调用频度比较高的方法
被声明成 private 只会被自己代码所调用的方法.如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数
注释规约
类, 类属性, 类方法的注释必须使用 Javadoc 规范,使用/** xxx */格式,不允许使用// xxx 方式
所有抽象方法, 包括接口中的方法, 都必须使用 Javadoc 注释,除了返回值, 参数, 异常说明外,还必须指出该方法做了什么事情,实现什么功能. 对子类的实现要求以及调用的注意事项需要一并说明
所有的类都必须添加创建者和创建日期
方法内部注释:
单行注释: 在被注释语句上方另起一行,使用 // 注释
多行注释: 使用 /* */ 注释,注意与代码对齐
所有枚举类型字段必须要有注释,说明每个数据项的用途
当水平足够高时,应当使用英文注释. 否则就用中文把问题说清楚,只要将专有名词与关键字保持英文原文即可
代码修改的同时,注释也要进行相应的修改,尤其是参数, 返回值, 异常, 核心逻辑等. 要保持代码与注释更新同步
谨慎注释代码:
注释的代码要进行详细的说明,而不是简单的注释
如果无用,则应该删除
注释的要求:
能够准确反映设计思想和代码逻辑
能够描述业务含义,能够迅速了解到代码背后的信息
好的命名,代码结构是自解释的,注释保证精简准确,表达到位
特殊的注释标记,需要注明标记人与标记时间.注意及时处理这些标记,通过标记扫描,经常清理此类标记.线上故障有时候就源于这些标记处的代码
待办事宜 TODO : (标记人, 标记时间, [预处理时间])
表示要实现,但目前尚未实现的功能.这实际上是一个 Javadoc 的标签.只能应用于类, 接口, 方法
错误,不能工作 FIXME : (标记人, 标记时间, [预处理时间])
在注释中用 FIXME 标记某段代码是错误的,而且不能工作,需要及时纠正情况
前后分离
前后端交互的 API,需要明确协议,域名,路径,请求方法,请求内容,状态码,响应体:
协议: 生产环境必须使用 HTTPS
路径: 每一个 API 需要对应一个路径,表示 API 具体的请求地址
代表资源,只能为名词,推荐使用复数,不能为动词,因为请求方法已经表达动作含义
URL 路径不能使用大写,单词如果需要分割,统一使用下划线
路径禁止携带请求内容类型的后缀 : ".json",".xml", 通过 accept 头表达即可
请求方法: 对具体操作的定义
GET: 获取
POST: 新增
PUT: 修改
DELETE: 删除
请求内容:
URL 带的参数必须无敏感信息或者符合安全要求
body 里带参数时必须设置 Content-Type
响应体: 响应体 body 可以放置多种数据类型,由 Content-Type 头来确定
前后端数据列表相关的接口返回时,如果为空,则返回空数组 [ ] 或者空集合 { }
服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码, errorCode, errorMessage, 用户提示信息四个部分:
HTTP 状态码: 浏览器
200 OK : 表明该请求被成功完成, 所请求的资源发送到客户端
401 Unauthorized : 请求要求身份验证, 通常是需要登录而用户未登录的情况
403 Forbidden : 服务器拒绝请求, 通常是机密信息或复制其余登录用户链接访问服务器的情况
404 Not Found : 服务器无法取得所请求的网页. 请求的资源不存在
500 Internal Server Error: 服务器内部错误
errorCode: 前端开发
errorMessage: 错误排查人员
用户提示信息: 用户. 要求简短清晰,提示友好,引导用户进行下一步操作或者解释错误原因,上下文环境,推荐操作
errorMessage 是前后端错误追踪机制的体现,可以在前端输出到 type="hidden" 的文字类控件或者用户端的日志中,这样能够快速地定位问题
对于需要使用超大整数的场景,服务端一律使用 String 字符串返回类型,禁止使用 Long 类型
Java 服务端如果直接返回 Long 整型数据给前端 ,JS 会自动转换为 Number 类型:
Number 类型: 双精度浮点数,表示原理和取值范围等同于 Java 中的 Double
Long 类型: 表示的最大值为 2^63^ -1. 超过 2^53^(9007199254740992) 的数值转化为 JS 的 Number 时,有些数值会有精度损失
在 Long 取值范围内,任何 2 的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题
如果浮点数尾数位与指数位空间不限,则可以精确表示任何整数.但是双精度浮点数的尾数位只有 52 位
示例: 通常在订单号或者交易号大于等于 16 位,大概率会出现前后端单据不一致的情况. 比如后端的 362909601374617692 到前端则是 362909601374617660
HTTP 请求通过 URL 传递参数时,不能超过 2048 个字节:
不同浏览器对于 URL 的最大长度限制略有不同,并且对超出最大长度的处理逻辑也有差异. 2048 字节是取所有浏览器的最小值
HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错:
Nginx 默认限制是 1MB
Tomcat 默认限制是 2MB
当确实有业务需要传较大内容时,可以通过调大服务器端的限制
在分页场景中,用户输入参数小于 1, 则前端返回第一页参数给后端. 后端发现用户输入的参数大于总页数,直接返回最后一页
服务器内部重定向必须使用 forward. 外部重定向地址必须使用 URL 统一代理模块生成,否则会因为线上采用 HTTPS 协议而导致浏览器提示 "不安全", 并且还会带来 URL 维护不一致的问题
服务器返回信息必须被标记是否可以缓存,如果缓存,客户端可能会重用之前的请求结果
缓存有利于减少交互次数,减少交互的平均延迟
示例: http 1.1 中 ,s-maxage 通知服务器进行缓存,时间单位为秒:
response.setHeader("Cache-Control", "s-maxage=" + cacheSeconds)
服务端返回的数据,使用 JSON 格式而非 XML :
HTTP 支持使用不同的输出格式,例如纯文本,JSON,CSV,XML,RSS 以至 HTML
在使用面向用户的服务,应该选择 JSON 作为通信中使用的标准数据交换格式,包括请求和响应
application/JSON 是一种通用的 MIME 类型,具有实用,精简,易读的特点
前后端的时间格式统一为 "yyyy-MM-dd HH:mm:ss", 统一为 GMT
其它注意
在使用正则表达式时, 利用好预编译功能,可以有效加快正则匹配速度
不要在方法体内定义
二方库中可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象
velocity 调用 POJO 类的属性时,直接使用属性名取值即可,模板引擎会自动按规范调用 POJO 的 getXxx(), 如果是 boolean 基本类型变量 ,boolean 命名不要加 is 前缀, 会自动调用 isXxx 方法.如果是 Boolean 包装类对象,优先调用 getXxx() 方法
后台输送给页面变量必须加上 $ ! {var},注意中间的感叹号
如果 var 等于 null 或者不存在,那么 ${var}会直接显示在桌面上
注意 Math.random() 这个方法返回是 double 类型,取值范围 0 <= x <1(能够取到零值,注意除零)
如果获取整数类型的随机数,不需要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法
获取当前秒数 System.currentTimeMillis(), 不是使用 new Date().getTime()
如果想获取更加精确的纳秒级时间值,使用 System.nanoTime() 的方式
在 JDK 8 以后,针对统计时间等常景,需要使用 Instant
不要在视图模版中加入任何复杂逻辑,根据 MVC 理论,视图的职责是展示,不要有模型和控制器的代码逻辑
任何数据结构的构造和初始化,都应指定大小,避免数据结构无限增长吃光内存
及时清理不再使用的代码段或配置信息
对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余
对于暂时被注释掉,后续可能恢复使用的代码片段,在注释代码的上方,统一规定使用三个斜杠/// 来说明注视掉代码的理由
版权声明: 本文为 InfoQ 作者【攻城狮Chova】的原创文章。
原文链接:【http://xie.infoq.cn/article/691ffa7ca27fee0a8422f39a1】。文章转载请联系作者。
评论