0%

线程运行状态的几个方法

start方法

用来启动一个线程,当调用start方法的时候,系统才会开启一个新的线程来执行用户定义的子任务,并为线程分配需要的资源


run方法

run()方法不需要用户调用,当通过start启动一个线程的时候,当线程或得了CPU执行时间,便会自动进入到run方法去执行具体的任务。*继承Thread类必须要重写run方法,在run中定义具体要执行的任务。


sleep方法

1
2
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) // 第一个参数为毫秒,第二个参数为纳秒

sleep相当于让线程睡眠,交出CPU,去执行其他任务。

但是sleep方法不会释放锁,某个对象加锁,则即使调用sleep方法,其他线程也无法访问这个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Test {

private int i = 10;
private Object object = new Object();

public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread1 = test.new MyThread();
MyThread thread2 = test.new MyThread();
thread1.start();
thread2.start();
}


class MyThread extends Thread{
@Override
public void run() {
synchronized (object) {
i++;
System.out.println("i:"+i);
try {
System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
Thread.currentThread().sleep(10000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
i++;
System.out.println("i:"+i);
}
}
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
i:11
线程Thread-0进入睡眠状态
线程Thread-0睡眠结束
i:12
i:13
线程Thread-1进入睡眠状态
Disconnected from the target VM, address: '127.0.0.1:13332', transport: 'socket'
线程Thread-1睡眠结束
i:14

*从上面的输出结果可以看出,当Thread0进入睡眠状态之后,Thread1并没有去执行具体的任务,只有当Thread0执行完成后,此时Thread0释放了对象锁,Thread1才开始执行


yield方法

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体交出的CPU时间,另外yield方法只能让拥有相同优先级的线程获取CPU执行时间的机会。

* 调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点和sleep方法不一样的


join方法

1
2
3
join()
join(long millis) //参数为毫秒
join(long millis,int nanoseconds) //第一个参数为毫秒,第二个参数为纳秒

假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread方法执行完毕,如果调用的是指定时间join方法,则等待一定的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {

public static void main(String[] args) throws IOException {
System.out.println("进入线程"+Thread.currentThread().getName());//执行顺序①
Test test = new Test();
MyThread thread1 = test.new MyThread();
thread1.start();
try {
System.out.println("线程"+Thread.currentThread().getName()+"等待");//执行顺序②
thread1.join();
System.out.println("线程"+Thread.currentThread().getName()+"继续执行");//执行顺序⑤
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

class MyThread extends Thread{
@Override
public void run() {
System.out.println("进入线程"+Thread.currentThread().getName());//执行顺序③
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕");//执行顺序④
}
}
}

输出结果如下:

1
2
3
4
5
①进入线程main
②线程main等待
③进入线程Thread-0
④线程Thread-0执行完毕
⑤线程main继续执行

可以看出,当调用thread1.join()方法后,main线程会进入等待,然后等待thread1执行完之后再继续执行。


interrupt方法

中断方法,单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就是说,它可以用来中断一个正处在阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Test {

public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {

}
thread.interrupt();
}

class MyThread extends Thread{
@Override
public void run() {
try {
System.out.println("进入睡眠状态");
Thread.currentThread().sleep(10000);
System.out.println("睡眠完毕");
} catch (InterruptedException e) {
System.out.println("得到中断异常");
}
System.out.println("run方法执行完毕");
}
}
}

输出结果如下:

1
2
3
进入睡眠状态
得到中断异常
run方法执行完毕

从上面的结果输出可以看出:

当线程调用interrupt的时候,中断处于阻塞状态的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {

public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {

}
thread.interrupt();
}

class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}

上面的程序会一直执行下去,执行到Int的最大值,说明了一点interrupt方法不能中断正在运行中的线程

但是如果配合isInterrupted()能够中断正在运行的线程,因为调用interrupt方法相当于将中断标志置为true,那么可以通过调用isInterrupted()判断中断标志是否被置为中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.xydtech.controller.prdman.prdconfig;

import java.io.IOException;

public class Test {

public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {

}
thread.interrupt();
}

class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(!isInterrupted()&&i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}

上面这段代码中,设置thread为interrupt之后,内部while循环的时候查询到不满足条件了,自然退出了循环,但是一般不建议通过这种方式来中断线程,一般会在Thread中添加一个属性isStop来指标是否结束while循环,然后再在while循环中判断isStop的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public class Test {

public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
thread.setStop(true);
} catch (InterruptedException e) {

}
}

class MyThread extends Thread{
boolean isStop = false;
@Override
public void run() {
int i = 0;
while(!isStop && i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}

public void setStop(boolean stop) {
isStop = stop;
}
}


stop方法destroy方法
这两个方法基本上被废弃了,基本上不会被使用到

线程相关属性的方法

getId() 得到线程的ID
getName()setName() 用来得到或者设置线程的名称
getPrioritysetPriority 用来获取和设置线程的优先级
setDaemonisDaemon用来设置线程为守护线程和判断线程是否为守护线程。

*重点:守护线程和用户线程的却别是,守护线程依赖于创建它的线程,而用户线程不依赖于创建它的线程,当mian方法运行完毕后,守护线程会随之消亡。但是用户线程不会消失,直至其运行完毕。在JVM中,垃圾收集器线程就是守护线程。
Thread类中有一个比较常用的静态类方法currentThread用来获取当前线程。

方法调用和Thread的生命周期关系

业务说明:项目中经常会出现将一个庞大的业务拆分成多个小部分,独自在分内运行,但前后任务之间又需要一定的执行顺序,所以就用到了标题中的“主线程执行过程中等待子线程都异步执行完成后才继续执行的问题”

第一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {

public static void main(String[] args){
Test test = new Test();
for(int i=0;i<10;i++){
MyThread myThread = test.new MyThread();
myThread.start();
}
System.out.println("主线程已经执行结束了!");
}

class MyThread extends Thread{

@Override
public void run() {
try {
System.out.println("子线程在执行!");
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

上面这段代码输出如下结果,很明显,这个是主线程执行完成之后,子线程又执行的,这个先后顺序不确定,因为有可能若干个子线程执行完后,主线程才执行到执行结束

1
2
3
4
5
6
7
8
9
10
11
主线程已经执行结束了!
子线程在执行!
子线程在执行!
子线程在执行!
子线程在执行!
子线程在执行!
子线程在执行!
子线程在执行!
子线程在执行!
子线程在执行!
子线程在执行!

能解决问题的第一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Test {

public static void main(String[] args){
Test test = new Test();


try {
for(int i=0;i<10;i++){
MyThread myThread = test.new MyThread();
myThread.start();
myThread.join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程已经执行结束了!");
}

class MyThread extends Thread{

@Override
public void run() {
try {
System.out.println("子线程在执行!");
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

上面这段代码是能够实现子线程执行完毕之后,再执行主线程,但是存在一个问题就是子线程是一个一个顺序执行的,也就没有了意义,所以可以看下下面的异步执行的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test {

public static void main(String[] args){
Test test = new Test();
long beginTime = System.currentTimeMillis();
Vector<MyThread> myThreadVector = new Vector<>();
for(int i=0;i<10;i++){
MyThread myThread = test.new MyThread();
myThread.start();
myThreadVector.add(myThread);
}

for(MyThread tempMyThread : myThreadVector){
try {
tempMyThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("一共执行了"+(System.currentTimeMillis() - beginTime)+"ms");
System.out.println("主线程已经执行结束了!");
}

class MyThread extends Thread{
@Override
public void run() {
try {
System.out.println("子线程开始执行!");
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

简单解析一下上面的代码:
首先上面的代码输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
子线程开始执行!
子线程开始执行!
子线程开始执行!
子线程开始执行!
子线程开始执行!
子线程开始执行!
子线程开始执行!
子线程开始执行!
子线程开始执行!
子线程开始执行!
Disconnected from the target VM, address: '127.0.0.1:14413', transport: 'socket'
一共执行了1027ms
主线程已经执行结束了!

首先创建一个Vector,然后通过创建Thread塞入到Vector中,然后再遍历Vector进行join动作,这样就可以不需要考虑子线程中sleep,动态的启动所有的线程

版权说明:本文档摘抄自《阿里巴巴Java开发手册(详尽版)》

摘抄不意味着单纯的Copy,而是对别人知识汲取和吸收的同时添加自己的个人理解

Java 编码规则手册

版本号 更新时间 备注
V1.0 2018-11-22 初始化版本

一、编程规约

(一)命名风格

  1. 【强制】代码中的命名不能以下划线或美元符号开始,也不能以下划线或美元符号结束。

    反例:_name/__name/$name/name_/name$/name__

  2. 【强制】代码中命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式

    说明:正确的英文拼写和语法可以让阅读者易于理解,便面歧义

    反例:DaZhePromotion()[打折]/getPingFenByName()[获取评分]/int 某变量=3

  3. 【强制】类名使用UpperCameCase风格,但以下情况例外DO/BO/DTO/VO/AO/PO/UID

    正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion

    反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion

  4. 【强制】方法名、参数名、成员变量、局部变量都统一使用lowerCameCase风格,必须遵从驼峰形式。

    正例:localValue / getHttpMessage() / inputUserId

  5. 【强制】常量命名全部大写,单词间用下划线隔开,尽量将语义表达清楚,不要害怕变量长度太长。

    正例:MAX_STOCK_COUNT

    反例:MAX_COUNT

  6. 【强制】抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾

  7. 【强制】类型与中括号紧挨相连来表示数组

    正例:定义整形数组 int[] arrayDemo

    反例:在 main 参数中,使用 String args[]来定义

  8. 【强制】POJO 类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误

    反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted()RPC框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常

  9. 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式

    正例:应用工具类包名为 com.alibaba.ai.util、类名为 MessageUtils

  10. 【强制】杜绝完全不规范的缩写,避免望文不知义

    反例:AbstractClass“缩写”命名成 AbsClasscondition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。

  11. 【强制】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意

    正例:JDK中,表达原子更新的类名为:AtomicReferenceFieldUpdater

    反例:变量 int a 的随意命名方式

  12. 【推荐】如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式

    说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念

    正例:public class OrderFactory;, public class LoginProxy;,public class ResourceObserver;

  13. 【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量

    正例:接口方法签名 void commit();接口基础常量 String COMPANY = "alibaba";

    反例:接口方法定义 public abstract void f();

    说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现

  14. 接口和实现类的命名有两套规则

    1)【强制】对于 Service DAO类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用Impl的后缀与接口区别

    正例:CacheServiceImpl 实现 CacheService接口

    2)【推荐】如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形式)

    正例:AbstractTranslator 实现 Translatable 接口

  15. 【参考】枚举类名建议带上 Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开

    说明:枚举其实就是特殊的类,域成员均为常量,且构造方法被默认强制是私有

    正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON

  16. 【参考】

    • Service/DAO层方法命名规则
      • 获取单个对象的方法用 get 做前缀
      • 获取多个对象的方法用 list 做前缀,复数形式结尾如:listObjects
      • 获取统计值的方法用 count 做前缀
      • 插入的方法用 save/insert 做前缀
      • 删除的方法用 remove/delete 做前缀
      • 修改的方法用 update 做前缀
    • 领域模型命名规约
      • 数据对象:xxxDOxxx 即为数据表名
      • 数据传输对象:xxxDTOxxx 为业务领域相关的名称
      • 展示对象:xxxVOxxx 一般为网页名称
      • POJODO/DTO/BO/VO的统称,禁止命名成 xxxPOJO

(二)常量定义

  1. 【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中

    反例:

    1
    2
    3
    4
    //Magic values, except for predefined, are forbidden in coding.
    if (key.equals("Id#taobao_1")) {
    //...
    }

    正例:

    1
    2
    3
    4
    String KEY_PRE = "Id#taobao_1";  
    if (KEY_PRE.equals(key)) {
    //...
    }
  2. 【强制】在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字1 混淆,造成误解

    说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2?

  3. 【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护

    说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解和维护

    正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 ConfigConsts

  4. 【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量

    • 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下
    • 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下

    反例:易懂变量也要统一定义成应用内共享常量,两位攻城师在两个类中分别定义了表示“是”的变量

    ​ 类 A 中:public static final String YES = "yes";

    ​ 类 B 中:public static final String YES = "y";

    A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致线上问题

    • 子工程内部共享常量:即在当前子工程的 constant 目录下
    • 包内共享常量:即在当前包下单独的 constant目录下
    • 类内共享常量:直接在类内部 private static final 定义
  5. 【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义(扩展enmu的使用方法

    说明:如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节

    正例:

    1
    2
    3
    4
    5
    6
    7
    8
    public enum SeasonEnum {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
    private int seq;

    SeasonEnum(int seq) {
    this.seq = seq;
    }
    }

(三)代码格式

  1. 【强制】大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块则

    • 左大括号前不换行
    • 左大括号后换行
    • 右大括号前换行
    • 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行
  2. 【强制】左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格;而左大括号前需要空格。详见第 5 条下方正例提示

    反例:if (空格 a == b 空格)

  3. 【强制】if/for/while/switch/do 等保留字与括号之间都必须加空格

  4. 【强制】任何二目、三目运算符的左右两边都需要加一个空格

    说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等

  5. 【强制】采用 4 个空格缩进,禁止使用 tab 字符

    说明:如果使用 tab 缩进,必须设置 1 个 tab 为 4 个空格。IDEA 设置 tab 为 4 个空格时,请勿勾选 Use tab character扩展IDEA设置按TAB时输入四个空格的方法);而在 eclipse 中,必须勾选 insert spaces for tabs

    正例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static void main(String[] args) {
    // 缩进 4 个空格
    String say = "hello";
    // 运算符的左右必须有一个空格
    int flag = 0;
    // 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
    if (flag == 0) {
    System.out.println(say);
    }

    // 左大括号前加空格且不换行;左大括号后换行
    if (flag == 1) {
    System.out.println("world");
    // 右大括号前换行,右大括号后有 else,不用换行
    } else {
    System.out.println("ok");
    // 在右大括号后直接结束,则必须换行
    }
    }
  6. 【强制】注释的双斜线与注释内容之间有且仅有一个空格

    正例:

    1
    2
    // 这是示例注释,请注意在双斜线之后有一个空格
    String ygb = new String();
  7. 【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则

    • 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例
    • 运算符与下文一起换行
    • 方法调用的点符号与下文一起换行
    • 方法调用中的多个参数需要换行时,在逗号后进行
    • 在括号前不要换行,见反例

    正例:

    1
    2
    3
    4
    5
    6
    StringBuffer sb = new StringBuffer();
    // 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
    sb.append("zi").append("xin")
    .append("huang")
    .append("huang")
    .append("huang");

    反例:

    1
    2
    3
    4
    5
    6
    7
    StringBuffer sb = new StringBuffer();
    // 超过 120 个字符的情况下,不要在括号前换行
    sb.append("zi").append("xin")...append
    ("huang");
    // 参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
    method(args1, args2, args3, ...
    , argsX);
  8. 【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格

    正例:下例中实参的args1,后边必须要有一个空格

    method(args1, args2, args3);

  9. 【强制】IDEtext file encoding 设置为 UTF-8; IDE 中文件的换行符使用 Unix 格式,不要使用 Windows格式

  10. 【推荐】单个方法的总行数不超过 80 行

    说明:包括方法签名、结束右大括号、方法内代码、注释、空行、回车及任何不可见字符的总行数不超过 80 行

    正例:代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加清晰;共性逻辑抽取成为共性方法,便于复用和维护

  11. 【推荐】没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐

    正例:

    1
    2
    3
    4
    int one = 1;
    long two = 2L;
    float three = 3F;
    StringBuffer sb = new StringBuffer();

    说明:增加 sb 这个变量,如果需要对齐,则给 a、b、c 都要增加几个空格,在变量比较多的情况下,是非常累赘的事情

  12. 【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性

    说明:任何情形,没有必要插入多个空行进行隔开

(四)OOP规约

  1. 【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可

  2. 【强制】所有的覆写方法,必须加@Override 注解

    说明:getObject()get0bject()的问题。一个是字母的 O,一个是数字的 0,加@Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错

  3. 【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object

    说明:可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)

    正例:

    1
    public List<User> listUsers(String type, Long... ids) {...}
  4. 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么

    正例:

    1
    2
    3
    4
    @Deprecated
    public static String toJson(Object obj) {
    return toJson(getAttributes(obj));
    }
  5. 【强制】不能使用过时的类或方法

    说明:java.net.URLDecoder 中的方法 decode(String encodeStr) 这个方法已经过时,应该使用双参数 decode(String source, String encode)。接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么

  6. 【强制】Objectequals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals

    正例:"test".equals(object);

    反例:object.equals("test")

    说明:推荐使用 java.util.Objects#equalsJDK7 引入的工具类)

  7. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较

    说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断

  8. 关于基本数据类型与包装数据类型的使用标准如下

    • 【强制】所有的 POJO 类属性必须使用包装数据类型
    • 【强制】RPC 方法的返回值和参数必须使用包装数据类型
    • 【推荐】所有的局部变量使用基本数据类型

    说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE(空指针异常)问题,或者入库检查,都由使用者来保证

    正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险

    反例:比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出

  9. 【强制】定义 DO/DTO/VOPOJO 类时,不要设定任何属性默认值

    反例:POJO 类的 gmtCreate 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间

  10. 【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID

    说明:注意 serialVersionUID 不一致会抛出序列化运行时异常

  11. 【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中

  12. 【强制】POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate toString时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString

    说明:在方法执行抛出异常时,可以直接调用 POJOtoString()方法打印其属性值,便于排查问题

  13. 【强制】禁止在 POJO 类中,同时存在对应属性 xxxisXxx() getXxx()方法

    说明:框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到

  14. 【推荐】使用索引访问用 Stringsplit 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险

    说明:

    1
    2
    3
    4
    String str = "a,b,c,,";
    String[] ary = str.split(",");
    // 预期大于 3,结果是 3
    System.out.println(ary.length);
  15. 【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读,此条规则优先于第 16 条规则

  16. 【推荐】类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法

    说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为承载的信息价值较低,所有 ServiceDAOgetter/setter 方法放在类体最后

  17. 【推荐】setter 方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名。在getter/setter 方法中,不要增加业务逻辑,增加排查问题的难度

    反例:

    1
    2
    3
    4
    5
    6
    7
    public Integer getData() {
    if (condition) {
    return this.data + 100;
    } else {
    return this.data - 100;
    }
    }
  18. 【推荐】循环体内,字符串的连接方式,使用 StringBuilderappend 方法进行扩展

    说明:下例中,反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费(扩展Java中的String,StringBuilder,StringBuffer三者的区别)

    反例:

    1
    2
    3
    4
    String str = "start";
    for (int i = 0; i < 100; i++) {
    str = str + "hello";
    }
  19. 【推荐】final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字

    • 不允许被继承的类,如:String
    • 不允许修改引用的域对象
    • 不允许被重写的方法,如:POJO 类的 setter 方法
    • 不允许运行过程中重新赋值的局部变量
    • 避免上下文重复使用一个变量,使用 final 描述可以强制重新定义一个变量,方便更好地进行重构
  20. 【推荐】慎用 Objectclone方法来拷贝对象

    说明:对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现域对象的深度遍历式拷贝

  21. 【推荐】类成员与方法访问控制从严

    • 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private
    • 工具类不允许有 publicdefault 构造方法
    • 类非 static 成员变量并且与子类共享,必须是 protected
    • 类非 static 成员变量并且仅在本类使用,必须是 private
    • static 成员变量如果仅在本类使用,必须是 private
    • 若是 static 成员变量,考虑是否为 final
    • 类成员方法只供类内部调用,必须是 private
    • 类成员方法只对继承类公开,那么限制为 protected

    说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的

(五)集合处理

  1. 【强制】关于 hashCodeequals 的处理,遵循如下规则

    • 只要重写 equals,就必须重写 hashCode
    • 因为 Set 存储的是不重复的对象,依据 hashCodeequals 进行判断,所以 Set 存储的对象必须重写这两个方法
    • 如果自定义对象作为 Map 的键,那么必须重写 hashCodeequals

    说明:String 重写了 hashCodeequals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用

  2. 【强制】ArrayListsubList结果不可强转成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()

    说明:使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致

    正例:

    1
    2
    3
    4
    5
    List<String> list = new ArrayList<String>(2);
    list.add("guan");
    list.add("bao");
    String[] array = new String[list.size()];
    array = list.toArray(array);

    反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误

  5. 【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常

    说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组

    1
    2
    String[] str = new String[] { "you", "wu" };
    List list = Arrays.asList(str)

    第一种情况:list.add("yangguanbao"); 运行时异常

    第二种情况:str[0] = "gujin"; 那么 list.get(0)也会随之修改

  6. 【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法,而<? super T>不能使用 get 方法,作为接口调用赋值时易出错

    说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用<? extends T>。第二、经常往里插入的,适合用<? super T>

  7. 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁

    正例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    List<String> list = new ArrayList<>();
    list.add("1");
    list.add("2");
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
    String item = iterator.next();
    if (删除元素的条件) {
    iterator.remove();
    }
    }

    反例:

    1
    2
    3
    4
    5
    for (String item : list) {
    if ("1".equals(item)) {
    list.remove(item);
    }
    }

    说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗

  8. 【强制】JDK7 版本及以上,Comparator扩展Java] 用 Comparator 实现排序) 实现类要满足如下三个条件,不然 Arrays.sortCollections.sort 会报 IllegalArgumentException 异常

    说明:三个条件如下

    • x,y 的比较结果和 y,x 的比较结果相反
    • x>y,y>z,则 x>z
    • x=y,则 x,z 比较结果和 y,z 比较结果相同

    反例:下例中没有处理相等的情况,实际使用中可能会出现异常

    1
    2
    3
    4
    5
    6
    new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
    return o1.getId() > o2.getId() ? 1 : -1;
    }
    };
  9. 【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略

    说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型

    正例:

    1
    2
    3
    4
    // <> diamond 方式
    HashMap<String, String> userCache = new HashMap<>(16);
    // 全省略方式
    ArrayList<User> users = new ArrayList(10);
  10. 【推荐】集合初始化时,指定集合初始值大小

    说明:HashMap 使用 HashMap(int initialCapacity) 初始化

    正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能

  11. 【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历

    说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value。而 entrySet 只是遍历了一次就把 keyvalue 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.foreach 方法

    正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合

  12. 【推荐】高度注意 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 异常

  13. 【推荐】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响

    说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。如:ArrayListorder/unsortHashMapunorder/unsortTreeSetorder/sort

  14. 【推荐】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List contains 方法进行遍历、对比、去重操作

(六)并发处理

  1. 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全

    说明:资源驱动类、工具类、单例工厂类都需要注意

  2. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯

    正例:

    1
    2
    3
    4
    5
    6
    public class TimerTaskThread extends Thread {
    public TimerTaskThread() {
    super.setName("TimerTaskThread");
    ...
    }
    }
  3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程(扩展Java并发编程:线程池的使用

    说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题

  4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

    说明:Executors 返回的线程池对象的弊端如下

    • FixedThreadPoolSingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
    • CachedThreadPoolScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
  5. 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类

    正例:注意线程安全,使用 DateUtils。亦推荐如下处理:

    1
    2
    3
    4
    5
    6
    private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
    return new SimpleDateFormat("yyyy-MM-dd");
    }
    };

    说明:如果是 JDK8 的应用,可以使用 Instant 代替 DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe

  6. 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁

    说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法

  7. 【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁

    说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁

  8. 【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据

    说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次

  9. 【强制】多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService则没有这个问题

  10. 【强制】禁止在循环中调用synchronized(同步)方法

    说明:方法的同步需要消耗相当大的资源,不要在循环中调用同步方法

    反例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import java.util.Vector;
    public class SYN {
    public synchronized void method (Object o) {
    }
    private void test () {
    for (int i = 0; i < vector.size(); i++) {
    method (vector.elementAt(i)); // violation
    }
    }
    private Vector vector = new Vector (5, 5);
    }

    正例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import java.util.Vector;
    public class SYN {
    public void method (Object o) {
    }
    private void test () {
    synchronized{//在一个同步块中执行非同步方法
    for (int i = 0; i < vector.size(); i++) {
    method (vector.elementAt(i));
    }
    }
    }
    private Vector vector = new Vector (5, 5);
    }
  11. 【强制】禁止在循环中使用表达式

    说明:在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快

    反例:

    1
    2
    3
    4
    5
    6
    7
    import java.util.Vector;
    class CEL {
    void method (Vector vector) {
    for (int i = 0; i < vector.size (); i++) // Violation
    ; // ...
    }
    }

    正例:

    1
    2
    3
    4
    5
    6
    7
    class CEL_fixed {
    void method (Vector vector) {
    int size = vector.size ()
    for (int i = 0; i < size; i++)
    ; // ...
    }
    }
  12. 【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果

    说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到

  13. 【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降(扩展多线程下生成随机数

    说明:Random 实例包括 java.util.Random 的实例或者 Math.random()的方式

    正例:JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个实例

  14. 【推荐】在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患(可参考 The “Double-Checked Locking is Broken" Declaration),推荐解决方案中较为简单一种(适用于 JDK5 及以上版本),将目标属性声明为 volatile

    反例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class LazyInitDemo {
    private Helper helper = null;
    public Helper getHelper() {
    if (helper == null) synchronized(this) {
    if (helper == null)
    helper = new Helper();
    }
    return helper;
    }
    // other methods and fields...
    }

(七)控制语句

  1. 【强制】在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使空代码

  2. 【强制】在高并发场景中,避免使用”等于”判断作为中断或退出的条件

    说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替

    反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止

  3. 【强制】if/else/for/while/do语句中必须使用大括号。即使只有一行代码,避免采用单行的编码方式:if (condition) statements

  4. 【推荐】表达异常的分支时,少用 if-else 方式,这种方式可以改写成

    1
    2
    3
    4
    5
        if (condition) {
    ...
    return obj;
    }
    // 接着写 else 的业务逻辑代码;

    说明:如果非得使用 if()...else if()...else...方式表达逻辑,【强制】避免后续代码维护困难,请勿超过 3 层

    正例:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句示例如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void today() {
    if (isBusy()) {
    System.out.println(“change time.”);
    return;
    }
    if (isFree()) {
    System.out.println(“go to travel.”);
    return;
    }
    System.out.println(“stay at home to learn Alibaba Java Coding Guidelines.”);
    return;
    }
  5. 【推荐】除常用方法(如getXxx/isXxx等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性

    说明:很多 if 语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢

    正例:

    1
    2
    3
    4
    5
    // 伪代码如下
    final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
    if (existed) {
    ...
    }

    反例:

    1
    2
    3
    if ((file.open(fileName, "w") != null) && (...) || (...)) {
    ...
    }
  6. 【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)

  7. 【推荐】避免采用取反逻辑运算符

    说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法

    正例:使用if (x < 628)来表达 x 小于 628

    反例:使用 if (!(x >= 628)) 来表达 x 小于 628

  8. 【推荐】接口入参保护,这种场景常见的是用作批量操作的接口

  9. 【推荐】下列情形,需要进行参数校验:

    • 调用频次低的方法
    • 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失
    • 需要极高稳定性和可用性的方法
    • 对外提供的开放接口,不管是 RPC/API/HTTP 接口
    • 敏感权限入口
  10. 【推荐】下列情形,不需要进行参数校验

    • 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求
    • 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般DAO层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略
    • 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数

(八)注释规约

  1. 【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用/*内容\/格式,不得使用// xxx 方式

    说明:IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率

  2. 【强制】所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。

    说明:对子类的实现要求,或者调用注意事项,请一并说明

  3. 【强制】所有的类都必须添加创建者和创建日期

  4. 【强制】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐

  5. 【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途

  6. 【推荐】与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可

    反例:“TCP 连接超时”解释成“传输控制协议连接超时”,理解反而费脑筋

  7. 【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改

    说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义

  8. 【推荐】谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除

    说明:代码被注释掉有两种可能性:1)后续会恢复此段代码逻辑。2)永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉(代码仓库保存了历史代码)

  9. 【推荐】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码

    • 待办事宜(TODO):( 标记人,标记时间,[预计处理时间])

      表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc
      还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc 标签)

    • 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])

      在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。

  10. 【推荐】核心代码注释量不得低于文件总行数的30%,非核心代码注释量不得低于文件总行数的20%

(九)其他

  1. 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度

    说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”)

  2. 【强制】velocity 调用 POJO 类的属性时,建议直接使用属性名取值即可,模板引擎会自动按规范调用 POJOgetXxx(),如果是 boolean 基本数据类型变量(boolean 命名不需要加 is前缀),会自动调用 isXxx()方法

    说明:注意如果是 Boolean 包装类对象,优先调用 getXxx()的方法

  3. 【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法

  4. 【强制】获取当前毫秒数 System.currentTimeMillis(); 而不是 new Date().getTime()

    说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime()的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类

  5. 【强制】删除未引用类,避免使用以”.*”结尾的引用,引用的顺序如下:

    • JDK标准包(例如:import java.io.IOException等)
    • 第三方的包(例如:import org.dom4j.Node等)
    • 项目的公共包
  6. 【强制】禁止在循环体中生命并实例化变量

    说明:在循环体中声明并实例化临时变量将会增加内存消耗,增加垃圾回收的次数;尤其是大对象,严格禁止在循环体内声明并实例化对象

    反例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import java.util.Vector;
    public class LOOP {
    void method (Vector v) {
    for (int i=0;i < v.size();i++) {
    Object o = new Object(); // 不建议
    o = v.elementAt(i);
    }
    }
    }

    正例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import java.util.Vector;
    public class LOOP {
    void method (Vector v) {
    Object o;
    for (int i=0;i<v.size();i++) {
    o = v.elementAt(i);
    }
    }
    }
  7. 【推荐】不要在视图模板中加入任何复杂的逻辑

    说明:根据 MVC 理论,视图的职责是展示,不要抢模型和控制器的活

  8. 【推荐】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存

  9. 【推荐】及时清理不再使用的代码段或配置信息

    说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余

    正例:对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由

二、异常日志

(一)异常处理

  1. 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException异常不应该通过
    catch 的方式来处理,比如:NullPointerExceptionIndexOutOfBoundsException 等等

    说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,不得不通过 catch NumberFormatException 来实现

    正例:if (obj != null) {...}

    反例:try { obj.method(); } catch (NullPointerException e) {…}

  2. 【强制】异常不要用来做流程控制,条件控制

    说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多

  3. 【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理

    说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现

    正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户

  4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容

  5. 【强制】有 try 块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回滚事务

  6. 【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch

    说明:如果 JDK7 及以上,可以使用 try-with-resources 方式(扩展Java进阶知识点3:更优雅地关闭资源 - try-with-resource及其异常抑制

  7. 【强制】不要在 finally 块中使用 return

    说明:finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句

  8. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类

    说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况

  9. 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值

    说明:本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null 的情况

  10. 【强制】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景

    • 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE

      反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE

    • 数据库的查询结果可能为 null

    • 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null

    • 远程调用返回对象时,一律要求进行空指针判断,防止 NPE

    • 对于 Session 中获取的数据,建议 NPE 检查,避免空指针

    • 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE

      正例:使用 JDK8Optional 类来防止 NPE 问题。(扩展Java8 如何正确使用 Optional

  11. 【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException

  12. 【推荐】对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”

    说明:关于 RPC 方法返回方式使用 Result 方式的理由

    • 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误
    • 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题
  13. 【推荐】避免出现重复的代码(Don’t Repeat Yourself),即 DRY 原则

    说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化

    正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
    private boolean checkParam(DTO dto) {...}

(二)日志规约

  1. 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

    1
    2
    3
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    private static final Logger logger = LoggerFactory.getLogger(Abc.class);
  2. 【强制】日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点

  3. 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式

    appName_logType_logName.log

    logType:日志类型,如 stats/monitor/access 等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找

    正例:mppserver 应用中单独监控时区转换异常,如:

    mppserver_monitor_timeZoneConvert.log

    说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控

  4. 【强制】trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式

    说明:logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,会执行 toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印

    正例:(条件)建设采用如下方式

    1
    2
    3
    if (logger.isDebugEnabled()) {
    logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
    }

    正例:(占位符)

    1
    logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
  5. 【强制】避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false

    正例:<logger name="com.taobao.dubbo.config" additivity="false">

  6. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出

    正例:logger.error(各类参数或者对象 toString() + "_" + e.getMessage(), e);

  7. 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从

三、安全规约

  1. 【强制】用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库

  2. 【强制】用户请求传入的任何参数必须做有效性验证

    说明:忽略参数校验可能导致

    • page size 过大导致内存溢出
    • 恶意 order by 导致数据库慢查询
    • 任意重定向
    • SQL 注入
    • 反序列化注入
    • 正则输入源串拒绝服务 ReDoS

    说明:Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果

  3. 【强制】禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据

  4. 【强制】表单、AJAX 提交必须执行 CSRF 安全验证

  5. 【强制】在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机
    制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。

四、MySQL数据库

(一)建表规约

  1. 【强制】表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigned tinyint(1表示是,0表示否)

    说明:任何字段如果是非负数,必须是unsigned

    注意:POJO类中任何布尔类型的变量,都不要加is前缀,所以,需要在设置从is_xxxXxx的映射关系。数据库表示是与否的值,使用tinyint类型,坚持is_xxx的命名方式是为了明确其取之含义与取值范围

    正例:表达逻辑删除的字段名is_deleted,1表示删除,0表示未删除

  2. 【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。

    说明:MySQLWindows下不区分大小写,单在Linux下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。

    正例:aliyun_adminrdc_configlevel3_name

    反例:AliyunAdminrdConfiglevel_3_name

  3. 【强制】表名不适用复数名词

    说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于DO类名也是单数形式,符合表达习惯。

  4. 【强制】禁用保留字,如descrangematchdelayed等,请参照MySQL官方保留字

  5. 【强制】主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名为idx_字段名

    说明:pk_primary keyuk_unique key;idx_index的简称。

  6. 【强制】小数类型为decimal,禁止使用floatdouble

    说明:floatdouble在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过decimal的范围,建议将数据拆成证书和小数部分分开存储。

  7. 【强制】如果存储的字符串长度几乎相等,使用char定长字符串类型

  8. 【强制】varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为text,独立出一张表,用主键来对应,避免影响其他字段索引效率。

  9. 【强制】表必备三个字段:idgmt_creategmt_modified

    说明:其中id必为主键,类型为bigint unsigned、表单时自增,步长为1。gmt_creategmt_modified的类型均为datetime类型,前者现在时表示主动创建,后者过去分词表示被动更新

  10. 【推荐】表的命名最好是“业务名称_表的作用”

    正例:alipay_task/force_project/trade_config

  11. 【推荐】库名尽量与应用名保持一致

  12. 【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:

    • 不是频繁修改的字段
    • 不是varchar超长字段,更不能是text字段

    正例:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存储类目名称,避免关联查询。

  13. 【推荐】单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表

    说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。

  14. 【参考】合适的字符存储长度,不但节约数据库表空间,节约索引存储,更重要的是提升检索速度

    正例:如下表,其中无符号值可以避免误存负数,且扩大了表示范围

    对象 年龄区间 类型 字节 表示范围
    150岁之内 tinyint unsigned 1 无符号:0~255
    数百岁 smallint unsigned 2 无符号:0~65535
    恐龙化石 数千万年 int unsigned 4 无符号:0~约42.9亿
    太阳 约50亿年 bigint unsigned 8 无符号:0~10的19次方

(二)索引规约

  1. 【强制】业务上具有唯一特性的字段,及时是多个字段的组合,也必须建成唯一索引

    说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高了查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生

  2. 【强制】超过三个表禁止join。需要join的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。

    说明:即使双标join也要注意表索引和SQL性能

  3. 【强制】varchar字段上建立索引时,必须制定索引长度,没必要对全字段建立索引,根据实际文本区分度决定长度即可。

    说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会打到90%以上,可以使用count(distinct left(列名,索引长度))/count(*)的区分度来确定.

  4. 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决

    说明:索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

  5. 【推荐】如果有order by的场景,请注意利用索引的有序性。order by最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort的情况,影响查询性能。

    正例:where a=? and b=? order by c; 索引:a_b_c

    反例:索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b;索引a_b无法排序

  6. 【推荐】利用覆盖索引来进行查询操作,避免全表。

    说明:如果一本书需要知道第11章是什么标题,会翻开第11章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。

    正例:能够建立索引的中类分别为朱建索引、唯一索引、普通索引三种,而覆盖索引知识一种查询的一种效果,用explain的结果,extra列会出现using index

  7. 【推荐】利用延迟关联或者子查询优化超多分页场景

    说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的地下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。

    正例:先快速定位需要获取的id段,然后再关联

    1
    SELECT a.* FROM1 a, (select id from1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
  8. 【推荐】防止因字段类型不同造成隐式转换,导致索引失效

  9. 【参考】创建索引避免有如下极端误解

    • 宁滥勿缺:任务一个查询就需要建一个索引
    • 宁缺毋滥:认为索引会小号控件、严重拖慢更新和新增速度
    • 地址唯一索引:任务业务的唯一性一律需要在应用层通过“先查后插”方式解决

(三)SQL语句

  1. 【强制】不要使用count(列名)或者count(常量)来代替count(\*),count(*)SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL或非NULL无关。

    说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行。

  2. 【强制】count(distinct col)计算该列除NULL之外的不重复行数,注意count(distinct col1,col2)如果其中一列全为NULL,那么及时另一列有不同的值,也返回0

  3. 【强制】当某一列的值全是NULL时,count(col)的返回结果为0,但是sum(col)的返回结果为NULL,因此使用sum()时需注意NPE问题

    正例:可以使用如下方式来避免sum的NPE问题:

    1
    SELECT IF(ISNULL(SUM(g)),0,SUM(g))FROM table;
  4. 【强制】使用ISNULL()来判断是否为NULL值。

    说明:NULL与任何值的直接比较都为NULL

    • NULL<>NULL的返回结果是NULL,而不是false
    • NULL=NULL的结果返回值是NULL,而不是true
    • NULL<>1的返回结果是NULL,而不是true
  5. 【强制】在代码中写分页查询逻辑时,若count为0应直接返回,避免执行后面的分页语句

  6. 【强制】不得使用外检或级联,一切外键概念必须在应用层解决

    说明:以学生和成绩的关系为例,学生表中的student_id是主键,那么成绩表中的student_id则为外键。如果更新学生表中的student_id,同时触发成绩表中student_id更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

  7. 【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

  8. 【强制】数据订正(特别是删除、修改记录操作)时,要先select,避免出现误删除,确认无误后才能执行更新语句

  9. 【推荐】in操作能避免则避免,是在避免不了,需要仔细评估in后边集合元素数量,控制在1000个内。

  10. 【参考】注意字符串统计函数的区别

    说明:

    • SELECT LENGTH(“轻松工作”);返回12
    • SELECT CHARACTER_LENGTH(“轻松工作”); 返回为 4
    • 如果需要存储表情,那么选择utf8mb4来进行存储,注意它与utf-8编码区别
  11. 【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE
    无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句

    说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同

(四)ORM映射

  1. 【强制】在表查询中,一律不要使用*作为查询的字段列表,需要哪些字段必须明确写明

    说明:

    • 增加查询分析器解析成本
    • 增减字段容易与resultMap配置不一致
    • 无用字段增加网络小号,尤其是text类型字段
  2. 【强制】不要用resultClass当返回参数,及时所有的类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必须有一个POJO类与之对应

    说明:配置映射关系,是字段与DO类解耦,方便维护

  3. 【强制】sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入

  4. 【强制】不允许直接拿HashMapHashtable作为查询结果集的输出。

    说明:resultClass=”Hashtable”,会置入字段名和属性值,但是值的类型不可控

  5. 【强制】不要写一个大而全的数据更新接口。传入为 POJO 类,不管是不是自己的目标更新字段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。执行 SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储

  6. 【参考】@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等

五、Oracle数据库

(一)对象命名规范

  1. 【强制】数据库名称定义为系统名+模块名

    说明:

    • 全局数据库名和SID名要求一致
    • 因SID名只能包含字符和数字,所以全局数据库和SID名中不能含有“_”等字符
  2. 【强制】表空间命名规范

    • 面向用户的专用数据表空间以用户名+_+data命名 ,如Aud 用户专用数据表空间可命名为Aud_data
    • 面向用户的专用索引表空间以用户名+_+idx命名
    • 面向用户的专用临时表空间以用户名+_+tmp命名
    • 面向用户的专用回滚段表空间以用户名+_+rbs命名
    • 面向应用的表空间以应用名+_data/应用名+_idx/应用名+_tmp/应用名+_rbs 命名
    • LOB 段数据专用表空间以其数据表空间+_+lobs 命名,如上例中数据表空间为Aud_data,则LOB 段表空间可命名为Aud_data_lobs
    • 表空间文件命名以表空间名+两位数序号(序号从01开始)组成,如Aud_data01 等
  3. 【强制】表名的命名规范

    • 一般表采用系统名+t_+模块名+_+表义名 格式构成

    • 若数据库中只含有单个模块,命名可采用系统名+t_+表义名格式构成

    • 模块名或表义名均以其汉语拼音的首字符命名,表义名中汉语拼音均采用小写,且字符间不加分割符

    • 表别名命名规则:取表义名的前3 个字符加最后一个字符。如果存在冲突,适当增加字符(如取表义名的前4 个字符加最后一个字符等)

    • 临时表采用系统名+t_tmp_+表义名 格式构成

      正例:

      1
      2
      3
      dft_gy_cbap:系统名(电费 df)+t_+模块名(高压 gy)+_+表义名(抄表安排 cbap)
      dft_cbbj: 系统名(电费 df)+t_+表义名(抄表标记 cbbj)
      dft_tmp_hj: 系统名(电费 df)+tmp+表义名(合计hj)(此处为临时表)
  4. 【强制】属性(列或字段)命名规则

    • 采用有意义的列名,为实际含义的汉语拼音的首字符,且字符间不加任何分割符
    • 属性名前不要加表名等作为前缀
    • 属性后不加任何类型标识作为后缀
    • 不要使用“ID”作为列名
    • 关联字段命名以 cd+_+关联表的表义名(或缩写)+_+字段名进行
  5. 【强制】主键命名规则

    • 任何表都必须定义主键
    • 表主键命名为:pk+_+表名(或缩写)+_+主键标识pk_YHXX_IDKH
  6. 【强制】外键命名规则

    • 表外键命名为: fk+_+表名(或缩写)+_主表名(或缩写)+_+主键标识fk_YHLX_YHXX_SFZH
  7. 【强制】CHECK约束命名规则

    • CHECK 约束命名为: chk+_+CHECK约束的列名(或缩写)
  8. 【强制】UNIQUE约束命名规则

    • UNIQUE 约束命名为: unq+_+UNIQUE约束的列名(或缩写)
  9. 【强制】索引命名规则

    • 索引的命名为:表名(或缩写)+_+列名+_idx,其中多单词组成的属性列列名取前几个单词首字符再加末单词首字符组成如yd_khkhid上的index: yd_kh_khid_idx
  10. 【强制】视图命名规则

    视图命名以系统名v_+模块名作为前缀,其他命名规则和表的命名类似

(二)SQL编写规范

  1. 【强制】表面使用数据库类型的自动转换功能

    反例:

    1
    SELECT * FROM category WHERE id = '123'; -- id如果是数字类型的,在这就需要内置转换,所以尽量保持数据类型一致
  2. 【参考】SELECT语句编写规范

    • 关键字大写,列表名表名小写
    • 由SELECT 开头,后跟一个显示查询结果的列表
    • 由FROM 开头,后跟一个或多个获取数据所涉及的表
    • 由WHERE 开头,后跟一个或多个确定所需值的条件
    • 由GROUP BY开头,后跟一个或多个表列名,通过这些列以对查询结果进行汇总
    • 由ORDER BY开头,后跟一个或多个表列名,通过这些列以对查询结果进行排序
  3. 【参考】尽量避免在循环中使用SQL语句

  4. 【参考】避免在WHERE子句中对列施以函数;

    正例:

    1
    2
    3
    4
    5
    6
    SELECT *
    FROM service_promotion
    WHERE gmt_modified
    >= TO_DATE('2001-9-01','yyyy-mm-dd')
    AND gmt_modified
    < TO_DATE('2001-9-02','yyyy-mm-dd');

    反例:

    1
    2
    3
    4
    SELECT *
    FROM service_promotion
    WHERE TO_CHAR(gmt_modified,’yyyy-mm-dd’)
    = '20001-09-01';

(三)类型选择

  1. 【强制】不同的数据类型选择不同的数据存储类型,不要什么都用varchar2
    • char(1)代表布尔值
    • 应尽量使用varchar2代替char类型
    • varchar2最多4000字符
    • date精确到微秒,而非天
    • 使用clob代替long,blob代替long raw
    • oracle只有number一种数据类型,使用时请给定长度

附件:专有名词解释

  1. POJOPlain Ordinary Java Object):在本手册中,POJO专指只有setter/getter/toString的简单类,包括DO/DTO/BO/VO
  2. OOPObject Oriented Programming):本手册泛指类、对象的变成处理方式
  3. ORMObject Relation Mapping):对象关系映射,对象领域模型与底层数据之间的转换,本文繁殖iBATISmybatis等框架
  4. NPEjava.lang.NullPointterException):空指针异常
  5. IDEIntegrated Development Environment):用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具,本手册泛指IDEAEclipse
  6. OOMOut of Memory):源于java.lang.OutOfMemoryError,当JVM没有足够的内存来为对象分配空间并且垃圾回收无法回收空间时,系统出现的严重状况

背景说明
闲的无聊,F12看了下项目上的登录,一看账号和密码都是明文发送的,因此找了下网络上的一些文章,对项目进行了优化

背景

目前的项目登录的时候都是明文的传递账号和密码,具体如下图所示

解决方案

准备资源

  • jar包(使用Maven引入jar包,如果不是Maven项目可以去Maven仓库将jar下载下来,然后导入项目中)

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk16</artifactId>
    <version>1.46</version>
    </dependency>
  • 加密js文件
    在Github上找到这个项目https://github.com/travist/jsencrypt的bin目录下下载jsencrypt.min.js进行

具体步骤

  1. (后台)创建一个RSA工具类(RSAUtil.java)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    import org.apache.commons.codec.binary.Base64;

    import javax.crypto.Cipher;
    import java.security.*;
    import java.security.interfaces.RSAPublicKey;

    /**
    * @ClassName RASUtil
    * @Description TODO
    * @Date 2018/12/109:52
    * @Version 1.0
    **/
    public class RSAUtil{


    //KeyPair is a simple holder for a key pair.
    private static final KeyPair keyPair = initKey();
    /**
    * 初始化方法,产生key pair,提供provider和random
    * @return KeyPair instance
    */
    private static KeyPair initKey() {

    try {
    //添加provider
    Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
    Security.addProvider(provider);
    //产生用于安全加密的随机数
    SecureRandom random = new SecureRandom();

    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", provider);
    generator.initialize(1024, random);
    return generator.generateKeyPair();
    } catch(Exception e) {
    throw new RuntimeException(e);
    }
    }
    /**
    * 产生public key
    * @return public key字符串
    */
    public static String generateBase64PublicKey() {
    PublicKey publicKey = (RSAPublicKey)keyPair.getPublic();

    //encodeBase64(): Encodes binary data using the base64
    //algorithm but does not chunk the output.
    //getEncoded():返回key的原始编码形式
    return new String(Base64.encodeBase64(publicKey.getEncoded()));
    }
    /**
    * 解密数据
    * @param string 需要解密的字符串
    * @return 破解之后的字符串
    */
    public static String decryptBase64(String string) {
    //decodeBase64():将Base64数据解码为"八位字节”数据
    return new String(decrypt(Base64.decodeBase64(string.getBytes())));
    }

    private static byte[] decrypt(byte[] byteArray) {
    try {
    Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
    Security.addProvider(provider);
    //Cipher: 提供加密和解密功能的实例
    //transformation: "algorithm/mode/padding"
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
    PrivateKey privateKey = keyPair.getPrivate();
    //初始化
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    //doFinal(): 加密或者解密数据
    byte[] plainText = cipher.doFinal(byteArray);
    return plainText;
    } catch(Exception e) {
    throw new RuntimeException(e);
    }
    }

    }
  2. (后台)编写一个生成公钥的接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    *@Description 生成公钥
    *@param
    *@return java.lang.String
    *@date 2018/12/10 9:48
    **/
    @RequestMapping("/genPubKey")
    @ResponseBody
    public String genPubKey(){
    Map<String,Object> map = new HashMap<String,Object>();
    map.put("code",-1);
    String publicKey = "";
    try {
    publicKey = RSAUtil.generateBase64PublicKey();
    map.put("pubkey",publicKey);
    map.put("code",1);
    } catch (Exception e) {
    e.printStackTrace();
    map.put("msg",e.getMessage());
    }
    return publicKey;
    }
  3. (前台)登录界面引入jsencrypt.min.js,初始化登录页的同时初始化公钥

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //页面初始化,调取公钥
    var publicKey = '';
    $(function(){
    getPublicKey();
    })

    //调取后台生成公钥接口
    function getPublicKey(){
    $.ajax({
    url: "user/genPubKey",
    type: "post",
    dataType: "text",
    data:{},
    success: function(data) {
    if(data) publicKey = data;
    if(publicKey==null){
    alert(获取publicKey失败,请联系管理员!);
    }
    }
    });
    }
  4. (前台)点击登录时,使用公钥生成私钥,加密账号和密码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var uname = $.trim($("#userName").val());
    var upwd = $.trim($("#passWord").val());

    var encrypt = new JSEncrypt();
    if(publicKey){
    encrypt.setPublicKey(publicKey);//设置公钥到前端生成私钥
    uname = encrypt.encrypt(uname);//加密用户名
    upwd = encrypt.encrypt(upwd);//加密密码
    //通过ajax发送加密的账号和密码到后台
    .....
    }
  5. (后台)后台账号和密码解密

    1
    2
    String loginID = RSAUtil.decryptBase64(username);
    String pwd = RSAUtil.decryptBase64(passwd);

    总结

    前台页面加载时调用生成公钥方法生成公钥,然后用户点击登录时,获取用户名和密码,根据之前生成的公钥,进行私钥加密,传入后台的数据就是已经加密过的数据,然后我们在后台进行解密,得到用户输入的原始密码和账户

页面打印

​ 打印之前没怎么接触过,最近项目有需求在系统中实现打印合同的功能,系统中的合同是用Bootstrap实现的html页面,因此就查阅资料,发现window.print方法可以在不使用外部打印插件的前提下,可以进行页面的打印操作,这种既简单又好操作,但是存在一个缺点是:页面内容的替换

实现原理

实现原理很简单:是通过页面替换到window.document.body中的内容,然后就可以用window.print就可以打印window.document.body中的内容

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
...
<div id="print-content" class="container" style="clear: both;width: 210mm;height: 297mm">
....
</div>
....

<script>
var bdhtml = window.document.body.innerHTML;//获取当前页的html代码
//获取需要打印的html代码
window.document.body.innerHTML = $("#print-content")[0].innerHTML;
window.print();
window.document.body.innerHTML = bdhtml;//还原页面内容
</script>
</body>

效果

Chrome

Firfox和IE

这两个浏览器是没有打印预览的,是直接弹出打印页面的

扩展

打印的时候出现页眉页脚问题处理

Chrome打印的时候会出现页眉和页脚中有地址的问题,可以直接点击上图中左侧的页眉页脚中取消勾选

打印时出现Bootstrap布局失效的问题

col-xs-4为Bootstrap中的自适应样式,如果不修改的时候,会出现本来在一行中的内容会出现都成一列的问题

1
2
3
col-lg-4
修改为
col-xs-4

效果如下:

背景说明:
常规下一般是用不到Json对象的遍历的,但是如果要做一些公共方法的时候,有可能会用到,比如说后端扔给你一个json串,需要你把数据反显到页面上,这种就需要进行处理了,遍历扔给你json串,然后获取和页面上匹配的内容,进行赋值操作,当然这个过程中还得需要判断控件的类型,毕竟存在一些特殊类型的控件,比如说文件上传和回显、复选框、单选框等等特殊控件

遍历有规律的JSON对象

1
2
3
4
5
6
7
8
9
10
11
12
13
packJson = [

{"name":"nikita", "password":"1111"},

{"name":"tony", "password":"2222"}

];

for(var p in packJson){//遍历json数组时,这么写p为索引,0,1

alert(packJson[p].name + " " + packJson[p].password);

}

遍历无规律的JSON对象

1
2
3
4
5
6
7
8
9
10
11
12
<script>
var json = [
  {dd:'SB',AA:'东东',re1:123},
  {cccc:'dd',lk:'1qw'}
];

for(var i=0,l=json.length;i<l;i++){
  for(var key in json[i]){
    alert(key+':'+json[i][key]);
  }
}
</script>

js字符串转int(parseInt)

1
alert(parseInt(childNode)+1);

js删除Object对象中的属性

1
delete obj.attr

js获取时间的用法

  • 获取时间
    1
    var myDate = new Date();//获取系统的时间
  • 获取特定格式的日期
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    myDate.getYear(); //获取当前年份(2位)
    myDate.getFullYear(); //获取完整的年份(4位,1970-????)
    myDate.getMonth(); //获取当前月份(0-11,0代表1月)
    myDate.getDate(); //获取当前日(1-31)
    myDate.getDay(); //获取当前星期X(0-6,0代表星期天)
    myDate.getTime(); //获取当前时间(从1970.1.1开始的毫秒数)
    myDate.getHours(); //获取当前小时数(0-23)
    myDate.getMinutes(); //获取当前分钟数(0-59)
    myDate.getSeconds(); //获取当前秒数(0-59)
    myDate.getMilliseconds(); //获取当前毫秒数(0-999)
    myDate.toLocaleDateString(); //获取当前日期
    var mytime=myDate.toLocaleTimeString(); //获取当前时间
    myDate.toLocaleString( ); //获取日期与时间