开发手册注意事项
1. POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate toString
时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。
2. 禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx()和 getXxx()方法。
说明:框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到,神坑之一。
3. 浅拷贝和深拷贝的区别,浅拷贝会把对象拷贝进来,修改一个属性,原变量也会修改,是对地址的拷贝 深拷贝会将原对象完整拷贝一份,将其复制一个对象
4. 关于日期的格式引用
日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后 引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年。
正例:表示日期和时间的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
ps:24小时制度是用HH,12小时制度使用hh,大写MM表示月,小写mm表示分钟
5. 获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime()。
说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。
6. 对于年份的天数需要进行动态写入,不能定死为365天,可以使用
// 获取今年的天数
int daysOfThisYear = LocalDate.now().lengthOfYear();
// 获取指定某年的天数
LocalDate.of(2011, 1, 1).lengthOfYear()
7. 使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。
Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list,不可对其进行添加或者删除元素的操作。
反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
8. 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现ClassCastException 错误。
正例:
List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);
说明:使用 toArray 带参方法,数组空间大小的 length,
1) 等于 0,动态创建与 size 相同的数组,性能最好。
2) 大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
3) 等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。
4) 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。
9. 使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。
10. 高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
集合类 Key Value Super 说明
Hashtable 不允许为 null 不允许为 null Dictionary 线程安全
ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 锁分段技术(JDK8:CAS)
TreeMap 不允许为 null 允许为 null AbstractMap 线程不安全
HashMap 允许为 null 允许为 null AbstractMap 线程不安全
反例:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储null 值时会抛出 NPE 异常。
11. 合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。
如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。
12. Set集合去掉List集合中重复元素
public static void main(String[] args) {
//利用set集合 去除ArrayList集合中的重复元素
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("1");
list.add("2");
list.add("2");
list.add("3");
list.add("3");
list.add("4");
list.add("4");
System.out.println("去重前的List集合:"+list);
Set<String> set = new HashSet<>();
set.addAll(list);
System.out.println("Set集合:"+set);
list.clear(); // 清空原有元素 放入被list去重后的元素
list.addAll(set);
System.out.println("去重后的List集合:"+list);
}
运行结果:
去重前的List集合:[1, 1, 2, 2, 3, 3, 4, 4]
Set集合:[1, 2, 3, 4]
去重后的List集合:[1, 2, 3, 4]
13. OOM(java.lang.OutOfMemoryError)内存用完,NPE(java.lang.NullPointerException)空指针
14. SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是 JDK8 的应用,可以使用 Instant(时间戳) 代替 Date,LocalDateTime 代替 Calendar,
DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
15. 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。
尽量在代理中使用 try-finally 块进行回收。
正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
16. 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
RPC的英文全程是Remote Procedure Call,也就是远程过程调用,这里可以把过程理解为方法,也就是远程方法调用。
在分布式系统中,因为每一个服务的边界都很小,很有可能调用别的服务提供的方法,这就会出现服务A调用服务B中一个方法的需求,也就是远程过程调用。
17. 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,
所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,
可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,
像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,
这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,
一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,
所以一般多写的场景下用悲观锁就比较合适。
资金相关的金融敏感信息,使用悲观锁策略。
说明:乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
正例:悲观锁遵循一锁二判三更新四释放的原则
18. HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中注意规避此风险。
19. 在一个 switch 块内,每个 case 要么通过 continue/break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default语句并且放在最后,即使它什么代码也没有。
说明:注意 break 是退出 switch 语句块,而 return 是退出方法体。
20. 所有的类都必须添加创建者和创建日期。
说明:在设置模板时,注意 IDEA 的@author 为`${USER}`,而 eclipse 的@author 为`${user}`,大小写有区别,而日期的设置统一为 yyyy/MM/dd 的格式。
正例:
/**
* @author yangguanbao
* @date 2016/10/31
*/
21. 所有的枚举类型字段必须要有注释,说明每个数据项的用途
用法一:常量定义
public enum ColorEnum {
RED, GREEN, BLANK, YELLOW
}
用法二:switch语句
enum ColorEnum {
GREEN, YELLOW, RED
}
public class ColorTest {
ColorEnum color = ColorEnum.RED;
public void change() {
switch (color) {
case RED:
color = ColorEnum.GREEN;
break;
case YELLOW:
color = ColorEnum.RED;
break;
case GREEN:
color = ColorEnum.YELLOW;
break;
}
}
}
用法三:枚举添加方法
public class EnumTest {
public static void main(String[] args) {
ErrorCodeEnum errorCode = ErrorCodeEnum.SUCCESS;
System.out.println("状态码:" + errorCode.code() +
" 状态信息:" + errorCode.msg());
}
}
enum ErrorCodeEnum {
SUCCESS(1000, "success"),
PARAM_ERROR(1001, "parameter error"),
SYS_ERROR(1003, "system error"),
NAMESPACE_NOT_FOUND(2001, "namespace not found"),
NODE_NOT_EXIST(3002, "node not exist"),
NODE_ALREADY_EXIST(3003, "node already exist"),
UNKNOWN_ERROR(9999, "unknown error");
private int code;
private String msg;
ErrorCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int code() {
return code;
}
public String msg() {
return msg;
}
public static ErrorCodeEnum getErrorCode(int code) {
for (ErrorCodeEnum it : ErrorCodeEnum.values()) {
if (it.code() == code) {
return it;
}
}
return UNKNOWN_ERROR;
}
}
22. 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
23. mybatis-plus中redis的使用
(1) 开启mybatis-plus的二级缓存,application.yml文件进行配置,加入pom文件的依赖
(2) mapper类中加入缓存注解@CacheNamespace
(implementation= MybatisRedisCache.class,eviction= MybatisRedisCache.class)
(3) 编写MybatisRedisCache实现Cache,定义读写锁等
(4) 编写RedisConfiguration 定义序列化规则和缓存方式和时间等自定义事项
(5) 编写Util类,进行调用
24. 后台输送给页面的变量必须加$!{var}——中间的感叹号。
说明:如果 var 等于 null 或者不存在,那么${var}会直接显示在页面上。
25. 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存
26. 事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
27. finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
28. 防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题
29. 在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
30. 好的单元测试必须遵守 AIR 原则。
说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
⚫ A:Automatic(自动化)
⚫ I:Independent(独立性)
⚫ R:Repeatable(可重复)
31. 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
32. 编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
⚫ B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
⚫ C:Correct,正确的输入,并得到预期的结果。
⚫ D:Design,与设计文档相结合,来编写单元测试。
⚫ E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。
33. 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库。
反例:某系统签名大量被恶意修改,即是因为对于危险字符 # --没有进行转义,导致数据库更新时,where后边的信息被注释掉,对全库进行更新。
34. 表单、AJAX 提交必须执行 CSRF 安全验证。
说明:CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。
CSRF(Cross-site Request Forgery)跨站请求伪造(或者缩写为XSRF),也被称为"One Click Attack"或"Session Riding"(曾被列为互联网 20 大安全隐患之一),是一种借助社工对网站身份的恶意利用。
不大流行,但如果被成功利用,危害更大。
CSRF与XSS的区别:
XSS
通过盗取网站内的已有的用户的身份,然后再执行相关操作
CSRF
通过伪装(伪造、更改状态的请求)用户身份(即盗用身份),通过服务器身份认证后,然后发送恶意请求(服务器会认为请求是合法的),但是服务器给出响应肯定是给真实的那个用户,
原理:
在浏览器中cookie在一段时间内是不会过期(不关闭或者退出浏览器),再次访问都会默认登录,这个应该都有体验。如果在cookie存在期间,通过构造csrf脚本或包含csrf脚本的链接发送给用户,得到信息后,再伪造成用户身份,执行相关操作
基本流程:
用户在某网站A进行登录-------->身份验证成功,返回cookie给用户---------->攻击者构建一个网站F,诱使用户使用同一浏览器进入(前提:未退出网站A,一般都会有默认浏览器)------------->网站F收到用户请求后,返回恶意代码给用户,强制他访问网站A---------->用户浏览器在网站A上执行相关操作(以已经持有的cookie)
使用过程
(1) 获取用户使用的修改信息的url
(2) 伪造url,将传入的参数修改
(3) 将url诱骗用户点击
(4) 信息全部被更改
35. 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
正例:aliyun_admin,rdc_config,level3_name
反例:AliyunAdmin,rdcConfig,level_3_name
36. 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。
说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。
37. 小数类型为 decimal,禁止使用 float 和 double。
说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储
38. 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
39. 不得使用外键与级联,一切外键概念必须在应用层解决。
说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
40. 分层领域模型规约:
• DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
• DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
• BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。
• Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。
• VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
41. @EnableScheduling 开启消息任务 @EnableAsync 开启异步调用