Java Dev Guide
Java Dev Guide
愿景:码出高效,码出质量。
此篇规范可以作为自己的编写规范。也可留作日后 team 的开发规范。
以后所有项目,直接引入这一篇规范。
编程风格
命名风格
【强制】命名不可以
_
/$
为开始或者结束。【强制】禁止拼音与英文混淆。建议纯英文。
【强制】类名遵循 UpperCamelCase。除却 DO/BO/DTO/VO/AO
【强制】方法名、参数名、成员变量、局部变量统一使用 lowerCamelCase
【强制】常量命名(枚举)全部大写,
_
分隔。力求完整清晰。【强制】抽象类名以 Abstract/Base 开头。异常类以 Exception 结尾。测试类以类名开始,Test 结尾。
这是一种约定,可以快速分类出类属于哪一部分。比如约定工具类都以 Util
结尾。
【强制】POJO 类布尔变量,不要加 is。否则部分框架解析会导致序列化错误。
【强制】包名统一小写。
.
分隔符之间仅有一个自然语义的英文单词。包名统一单数形式。类名可用复数。【强制】杜绝不规范的缩写。
英文名过长,可进行对应缩写。约定对应缩写规范。
【推荐】为了代码自解释。任意元素命名,尽量为完整语义。
【推荐】若模块、接口、类、方法使用了设计模式,可以直接命名时体现。
【推荐】接口方法/属性不要加任何修饰符。(public 也不应该,因为默认就是。)
接口与实现
【强制】Service/DAO 类,基于 SOA 理念。
暴露接口,实现为 Impl 后缀与之区分。
【推荐】形容能力的接口,(-able) 的形式。
【推荐】枚举使用 Enum 后缀,常量使用 Constant 后缀。
Service/DAO 命名规约
觉得原来的文档设计的不够好。可以从区分 DAO/Service 的角度来命名。其他可参考原文档。
DAO: insert/delete/update/select 与 SQL 保持一致。
Service: save/remove/edit/query 等与服务语义保持一致。(可自行约定)
- 领域模型命名规范
数据对象。xxxDO, xxx 为表名
数据传输对象。xxxDTO, xxx 为业务领域相关名称
展示对象。xxxVo, xxx 一般为网页名称。
POJO 是以上统称,禁止命名 xxxPOJO。
常量定义
【强制】代码中禁止直接出现魔法值。(未经定义的常量)
【强制】Long 变量初始化时,后缀应使用大写 L。
【推荐】常量类应该按照功能归类,分开维护。
【推荐】常量的复用广度不同,可分为以下 5 层:
1、跨应用共享变量。放在二方库中。如 client.jar/constant
2、应用内共享变量。放在一方库中。如 modules/constant
3、子工程内共享变量。当前子工程 constant 下。
4、包内共享常量。当前包下单独的 constant 下。
5、类内共享常量。类内部:private static final。
- 【推荐】枚举优于常量就不多言了。
可以多看看《Effective Java》之类的书。
代码风格
【强制】大括号,默认编辑器格式。
【强制】左右括号之间与字符之间不应该有空格。
正:if (a == b)
反: if ( a == b )
【强制】if/for/while/switch/do 等保留字与括号之间必须加空格。
【强制】任何二目、三目运算符的左右两边都需要加一个空格。
【强制】采用 4 空格缩进,禁止使用 tab。
IDE 设置 tab 为 4 个空格时,请不要勾选 Use tab character;
【强制】注释的
//
与注释内容之间,有且仅有一个空格。【强制】单行字符数不超过 120。(IDE 有个分割线)
- 换行后。第二行缩进 4 个空格。第三行则不用继续缩进。
【强制】多个参数定义传入时,多个参数
,
后必须加空格。【强制】IDE 编码统一为
UTF-8
,换行符统一为Unix
,而不是Windows
。【推荐】没有必要增加若干空格使得字符对齐。
【推荐】不同的语义之间,插入一行空格分隔即可。相同逻辑和语义不需要。
OOP
【强制】避免通过类的对象引用调用访问其静态方法/变量(会增加编译器解析成本),直接使用 ClassName 访问。
【强制】所有方法覆写,添加
@Override
注解。【强制】尽量避免使用可变参数编程
有些场景就挺合适的,比如 Arrays.asList(...)
- 【强制】外部正在调用或者二方库依赖的接口,禁止修改方法签名。应使用
@Depretched
废弃掉,并注明最新的使用替代方案。
Java 的这个注解没有 .Net 的设计的好,无法注解中指定新的方法是什么。
【强制】请勿使用过时的方法/类
【强制】比较时,尽量使用确定的值和变量对比。推荐使用 Objects.equals();
正:
"test".equals(obejct)
- 【强制】相同类型的包装类值比较,必须使用 equals();
Integer val = ? (-128~127) 会复用已有对象,大坑。
- 【强制】所有 POJO 都必须为包装数据类型,且不要设置默认值。
保证有些值入库可以为 null,所有校验由使用者保证。
【推荐】所有局部变量使用基本数据类型。
【强制】序列化类,不要修改
serialVersionUID
,避免反序列化失败
看过一本书,应该是 《Thinking in Java》。类中其实不需要这个变量,个人建议不要有此属性。
【强制】构造方法中禁止有业务代码。如果有初始化逻辑,放在 init() 中
【强制】POJO 必须添加 toString() 方法。为了日志更容易追踪。
个人建议使用 Commons-lang3 反射实现 ToString(); 自己写项目,为了简单可以使用 lombok。
有一天,系统需要对有些字段进行脱敏。如用户名,手机号,密码。统一使用 Commons-lang3 性能消耗不高,可以统一生成。
对某些字段添加字段,就可脱敏,方便统一管理。当然日志脱敏也是可行的。
- 【推荐】类中的方法应该按照名称顺序等,便于阅读的放在一起。
建议顺序:public > protect > private > getter/setter > toString() > equals()/hashcode()
- 【推荐】getter/setter 方法要保证纯粹。
这一点很好,比如最近我想在 get 的时候,如果没有就默认给个初始值。是在 get 方法中加的业务逻辑,检讨。
个人想法:在获取到之后,在对属性进行默认值初始化等处理。
【推荐】 String 循环体字符串的拼接,使用 StringBuilder.append();
【推荐】类成员/方法的访问控制从严:
仅仅本类使用,必须为 private;
考虑与子类共享,则为 protect;
不允许 new 来创建,则构造器必须 private;
一句话:尽量将访问级别控制在最低。
集合处理
- 【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
只要重写equals,就必须重写hashCode。
因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的 对象必须重写这两个方法。
如果自定义对象做为Map的键,那么必须重写hashCode和equals。
【强制】ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException 异常,即java.util.Random
【强制】在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、 删除均会产生ConcurrentModificationException 异常。
子列表是个大坑,建议对原来的列表进行复制成一个新列表。
【强制】使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全 一样的数组,大小就是 list.size()。
【强制】使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方 法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
- 【强制】PECS(Producer Extends Consumer Super)原则:
第一、频繁往外读取内 容的,适合用。
第二、经常往里插入的,适合用
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
【推荐】集合初始化时,指定集合初始值大小。
这个涉及到 HashMap 的原理。
HashMap使用HashMap(int initialCapacity) 初始化,
正例: initialCapacity = (需要存储的元素个数 / 负载因子) + 1
。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
也就是说,当数量较多时,应该设置初始值。为:initialCapacity = (num / 0.75)+1;
- 【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
DESC
values() 返回的是 V 值集合,是一个 list 集合对象; keySet() 返回的是 K 值集合,是 一个 Set 集合对象; entrySet() 返回的是 K-V 值组合集合。
并发处理
【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
- SimpleDateFormat 是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。
DESC
如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,
官方给出的解释:simple beautiful strong immutable thread-safe。
- 【强制】尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
类似的。在事务中尽量不要有 bean 的构建,外部的调用。这些尽量放在事务外面。
【强制】并发修改同一记录时,避免更新丢失,需要加锁。如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
【强制】多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获 抛出的异常,其它任务便会自动终止运行,使用
ScheduledExecutorService
则没有这个问题。【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。
在 JDK7 之后,可以直接使用 API ThreadLocalRandom。
【推荐】在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优 化问题隐患(可参考 The "Double-Checked Locking is Broken" Declaration),
推荐解决方案中较为简单一种(适用于 JDK5 及以上版本),将目标属性声明为 volatile 型。【参考】volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
如果是 count++ 操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);
如果是 JDK8,推 荐使用 LongAdder
对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
- 【参考】ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static 修饰。
这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
控制语句
- 【推荐】除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复 杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
建议声明一个方法,并起一个好名字。
【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。
【参考】下列情形,需要进行参数校验:
调用频次低的方法。
执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。
需要极高稳定性和可用性的方法。
对外提供的开放接口,不管是RPC/API/HTTP接口。
敏感权限入口。
一般,所有对外接口都认为别人是恶意调用,必须入参校验。快速失败。
注释规约
【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用
/**内容*/
格式,不得使用// xxx
方式。【强制】所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
【强制】所有的类都必须添加创建者和创建日期
便于日后甩锅。。。
- 【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途。
所有的常量和 POJO 也应该如此。
【推荐】与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
【参考】谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。
有 VCS 的出现,代码中应该杜绝被注释掉的代码,直接删掉。
- 【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
// todo 这个是很有用的代码。不过 Eclipse 下有些开发者,经常清理这些代码。
其他
【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x 中增加映射,是必须的。 在MyBatis Generator生成的代码中,需要进行对应的修改。
【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需 要定义;反过来,每一个表也必然有一个与之对应。
说明:配置映射关系,使字段与 DO 类解耦,方便维护。
可以为不同的查询制定一个 XXXResult, 但是维护量较大。
-【强制】sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。
-【强制】iBATIS自带的queryForList(String statementName,int start,int size)不推荐使用。
说明:其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取 start,size 的子集合。
正例:
Map map = new HashMap(); map.put("start", start);
map.put("size", size);
- 【强制】不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出。
说明:resultClass=”Hashtable”,会置入字段名和属性值,但是值的类型不可控。 ——禁止用于商业用途,违者必究——
- 【强制】更新数据表记录时,必须同时更新记录对应的 gmt_modified 字段值为当前时间。
这里可以再 mySQL 中设置,让这个字段当记录被修改时就会自动变化。
【推荐】不要写一个大而全的数据更新接口。传入为 POJO 类,不管是不是自己的目标更新字 段,
都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。
执行 SQL 时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。【参考】@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需 要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
【参考】中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时带 上此条件;表示不为空且不为 null 时执行;表示不为 null 值时 执行。