程序员社区

p3c规范学习


title: p3c规范学习
date: 2019/04/10 17:38


引言

离娄之明,公输子之巧,不以规矩,不能成方圆。 —— 战国·邹·孟轲《孟子·离娄上》

一、编程规约

1、命名风格

1.1 如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式。

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

1.3 实体与方法命名规范:

A) Service/DAO层方法命名规约

  • 1、获取单个对象的方法用get作前缀。
  • 2、获取多个对象的方法用list作前缀。
  • 3、获取统计值的方法用count作前缀。
  • 4、插入的方法用save/insert作前缀。
  • 5、删除的方法用remove/delete作前缀。
  • 6、修改的方法用update作前缀。

B) 领域模型命名规约

  • 1、数据对象:xxxDO,xxx即为数据表名。
  • 2、数据传输对象:xxxDTO,xxx为业务领域相关的名称。
  • 3、展示对象:xxxVO,xxx一般为网页名称。
  • 4、POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。

1.4 POJO类中的任何布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列 化错误。

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

1.5 避免在父子类中定义相同命名的变量,会降低可读性

public class ConfusingName { 
    public int stock;
}

class Son extends ConfusingName {
    // 不允许与父类的成员变量名称相同 
    public int stock;
}

1.6 避免在一个方法中的不同代码块中采用相同命名的变量

public void get(String alibaba) {
    if (condition) {
        final int money = 666;
        // ...
    }
    for (int i = 0; i < 10; i++) {
        // 在同一方法体中,不允许与其它代码块中的 money 命名相同 
            final int money = 15978;
        // ...
    }
}

1.7 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。

正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT

反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD

2、常量定义

2.1 不要使用一个常量类维护所有常量,按常量功能进行归类,分开维护。

大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护

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

2.2 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包 内共享常量、类内共享常量。

  • 1、跨应用共享常量:放置在二方库中(通用模块),通常是client.jar中的constant目录下。
  • 2、应用内共享常量(应用间通用):放置在一方库(base包)中,通常是子模块中的constant目录下。
  • 3、子工程内部共享常量:即在当前子工程的constant目录下。
  • 4、包内共享常量:即在当前包下单独的constant目录下。
  • 5、类内共享常量:直接在类内部private static final定义。

2.3 如果变量值仅在一个固定范围内变化用 enum 类型来定义。

春夏秋冬

public enum SeasonEnum {
   SPRING(1), 
   SUMMER(2), 
   AUTUMN(3), 
   WINTER(4);

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

2.4 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。

3、代码格式

3.1 单行字符数限制不超过 120 个,超出需要换行

  • 1、方法调用的点符号与下文一起换行。
  • 2、方法调用时,多个参数,需要换行时,在逗号后进行。

3.2 不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。

4、OOP 规约

4.1 接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。

4.2 Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。

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

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

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

4.3 包装类对象之间值的比较,全部使用 equals 方法比较。

注:浮点类型除外

float a = 1.0f - 0.9f; 
float b = 0.9f - 0.8f;
if (a == b) {
    // 预期进入此代码快,执行其它业务逻辑 
    // 但事实上 a==b 的结果为 false
}

Float x = Float.valueOf(a); 
Float y = Float.valueOf(b); 
if (x.equals(y)) {
    // 预期进入此代码快,执行其它业务逻辑
    // 但事实上 equals 的结果为 false
}

4.4 定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。

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

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

4.6 POJO 类必须写 toString 方法。如果继承了另一个 POJO 类,注意在前面加一下 super.toString。

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

4.7 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法。

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

不过也可以学习 jdk 的源码,将私有/受保护方法放到第一个使用到这个方法的方法下面。

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

get/set的目的是将变量封装起来,按照上面说的,你可能会觉得直接obj.变量名获取或设置值不就好了吗,但是有一种情况只允许get不许set,比如:单例模式,构造的时候赋默认值

4.9 循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。

说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。

String a = "abc" + "bcd" + "efg" 会创建出5个对象

4.10 final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字:

  • 1、不允许被继承的类,如:String 类。
  • 2、不允许修改引用的域对象,如:POJO 类的域变量。
  • 3、不允许被重写的方法,如:POJO 类的 setter 方法。
  • 4、不允许运行过程中重新赋值的局部变量。
  • 5、避免上下文重复使用一个变量,使用 final 描述可以强制重新定义一个变量,方便更好 地进行重构。

eg1. 由于String中的char[]是final的,所以b创建好了之后,a再修改不会导致b修改

String a = "a"
String b = "123" + a

a = "abc"
sout(b) 

>> 123a

eg2. 被final修饰的不能修改引用的指针,但是可以修改指针指向的对象的数据

p3c规范学习插图

4.11 什么时候使用静态变量和方法

静态成员变量:当所有对象中的成员变量的数值相同时,此成员变量可以用static修饰

静态方法:当一个函数中没有调用静态变量时,此方法可以用static修饰;否则,此方法不能用static修饰

4.12 什么时候使用内部类

  • 1、内部类方法可以访问该类定义所在作用域中的数据,包括被 private 修饰的私有数据
  • 2、内部类可以对同一包中的其他类隐藏起来
  • 3、内部类可以实现 java 单继承的缺陷

使用:

  • 1、一个类中的一些代码在这个类中和外部类都可以使用,可以使用内部类规范在一起
  • 2、新建线程的时候

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

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

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

4.15 定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。

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

4.16 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。

Spring 0.9 版本中 BeanFactory 等代码的初始化逻辑全部写在构造方法中,看的时候心里全是 mmp,所以不要这样写了。

4.17 getter/setter 方法中,不要增加业务逻辑,增加排查问题的难度。

5、集合处理

5.1 如果对象要放入Set中或作为Map的key,那么要重写hashCode和equals方法

只要重写equals,就必须重写hashCode

== 是比较内存地址

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

说明:asList 的返回对象是一个 Arrays内部类,并没有实现集合的修改方法。

Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组

p3c规范学习插图1
p3c规范学习插图2

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

扩展说一下PECS(Producer Extends Consumer Super)原则:

  • 第一、频繁往外读取内 容的,适合用<? extends T>。
  • 第二、经常往里插入的,适合用<? super T>。

5.4 注意 Map 类集合 K/V 能不能存储 null 值的情况

p3c规范学习插图3

jdk1.8中聚合成map的方法

Map<String, String> attribute = modelCalculationResults.stream().collect(Collectors.toMap(ModelCalculationResult::getKey, ModelCalculationResult::getValue));

问题:由于调用的是merge方法,对key、value判空,所以 value为空的时候会报错;若想装逼,可以使用下面的这种:
Map<String, String> attribute = modelCalculationResults.stream().collect(HashMap::new, (map, modelCalculationResult) -> map.put(modelCalculationResult.getKey(), modelCalculationResult.getValue()), HashMap::putAll);

5.5 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式。

Iterator 是工作在一个独立的线程中,并且拥有一个 mutex(互斥) 锁

p3c规范学习插图4
p3c规范学习插图5

问题:为什么下面这种方式不会抛出异常?

答:因为移除的整好是倒数第二个元素;抛出异常的操作是在继续向下循环时调用next方法时抛出,而移除了倒数第二个元素,hasNext方法返回的false,就不在向下循环了,从而不会抛出异常。

ArrayList<Object> objects = new ArrayList<>();

objects.add("x");
objects.add("x1");
objects.add("x2");

for (Object object : objects) {
    System.out.println("object = " + object);
    if (object.equals("x1")) {
        objects.remove("x1");
    }
}

5.6 使用 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 值组合集合。

5.7 集合的有序性(sort)和稳定性(order)

集合的有序、无序是指插入元素时,保持插入的顺序性,也就是先插入的元素优先放入集合的前面部分。

而排序是指插入元素后,集合中的元素是否自动排序。(例如升序排序)

HashMap、 HashSet、 HashTable 等 基于哈希存储方式的集合是无序的。其它的集合都是有序的。

而TreeMap TreeSet 等集合是排序的。

TreeSet<Integer> integers = new TreeSet<>();

integers.add(5);
integers.add(2);
integers.add(3);
integers.add(4);

integers.forEach(x -> System.out.println("x = " + x));

>> 2 3 4 5

5.8 在使用 Collectors#toMap() 方法时,要使用 3 个参数的那个方法。

// 反例:

String[] departments = new String[] {"iERP", "iERP", "EIBU"}; // 抛出 IllegalStateException 异常
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str));


// 正例:
String[] departments = new String[] {"iERP", "iERP", "EIBU"};
Map<Integer, String> map = Arrays.stream(departments)
.collect(Collectors.toMap(String::hashCode, str -> str, (v1, v2) -> v2));

5.9 在使用 Collectors#toMap() 方法时,记得对 value 判空,否则会报空指针

Map<String, String> attribute = modelCalculationResults.stream().collect(Collectors.toMap(ModelCalculationResult::getKey, ModelCalculationResult::getValue));

问题:由于调用的是merge方法,对key、value判空,所以 value为空的时候会报错;若想装逼,可以使用下面的这种:

Map<String, String> attribute = modelCalculationResults.stream().collect(HashMap::new, (map, modelCalculationResult) -> map.put(modelCalculationResult.getKey(), modelCalculationResult.getValue()), HashMap::putAll);

注:null可以做key

5.10 Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list,不可对其进行添加或者删除元素的操作。

反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。

所以为空的话如果调用方可以对结果进行操作,那么还是返回 new ArrayList<>(0) 吧。

5.11 对 List#subList() 方法返回的 SubList 对象进行操作,会对源 List 产生影响,反之一样。

5.12 如果要将集合转为数组,必须使用 Collection#toArray(T[] array) 方法

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

6、并发处理

6.1 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

public class TimerTaskThread extends Thread { 
    public TimerTaskThread() {
        super.setName("TimerTaskThread");
        ... 
}

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

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

1)FixedThreadPool 和 SingleThreadPool:

  • 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM(Out Of Memory 内存溢出)。

2)CachedThreadPool 和 ScheduledThreadPool:

  • 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

6.3 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,官方给出的解释:线程安全且不可变(使用final修饰的)

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

6.5 双重检查锁实现单例

双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。 —— Wiki

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }

    // other functions and members...
}

直觉上,这个算法看起来像是该问题的有效解决方案。然而,这一技术还有许多需要避免的细微问题。例如,考虑下面的事件序列:

    1. 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
    1. 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,将变量指向部分初始化的对象
    1. 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有覆盖B使用的内存(缓存一致性)),程序很可能会崩溃。
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}

7、控制语句

7.1 在高并发场景中,避免使用”等于”判断作为中断或退出的条件。 说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间 判断条件来代替。

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

7.2 循环体中的语句要考量性能,以下操作尽量移至循环体外处理:

如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。

7.3 下列情形,需要进行参数校验:

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

7.4 下列情形,不需要进行参数校验:

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

7.5 三目运算符 condition? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。

以下两种场景会触发类型对齐的拆箱操作:

  1. 表达式 1 或表达式 2 的值只要有一个是原始类型
  2. 表达式 1 或表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。

反例:

Integer c = null;
Integer d = false ? 1 : c;
System.out.println(d);  // NPE

因为表达式1是基本类型,会导致 c 进行拆箱,即调用 c.intValue();

详细解释

7.6 条件判断语句中不要写特别复杂的逻辑,且最好不用使用 ! 运算符

二、异常日志

0、错误码

0.1 错误码为字符串类型,共5位,分成两个部分:错误产生来源+四位数字编号。

说明:错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100。

1、异常处理

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

1.2 finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。

说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。

try (InputStream inputStream = result.get(0).getInputStream()) {
    log.info("{}的文件流获取成功!", path);
    return inputStream;
} catch (IOException e) {
    log.error("获取流失败,{}", e);
}

1.3 不要在 finally 块中使用 return。

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

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

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

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

  • 1、使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
  • 2、如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用 端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输 的性能损耗也是问题。

2、日志规约

2.1 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:

appName_logType_logName.log。

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

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

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

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

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

2.3 谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。

说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请

思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

2.4 可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。

说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。

2.5 日志打印时禁止直接用JSON工具将对象转换成String

打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。

三、单元测试

优秀的单元测试

(1)单元测试是“白盒测试”,应该覆盖各个分支流程、异常条件。(所以他会促进我们使用设计模式,而不是一堆 if/else)

(2)单元测试面向的是一个单元(Unit),是由Java中的一个类或者几个类组成的单元。

(3)单元测试的运行速度一定要快!

(4)单元测试一定是可重复执行的!

(5)单元测试之间不能有相互依赖,应该是独立的!

(6)单元测试代码和业务代码同等重要,要一并维护!

四、安全规约

五、MySQL 数据库

1、建表规约

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

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

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

1.2 表名不使用复数名词。

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

1.3 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。

说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。

1.4 小数类型为 decimal,禁止使用 float 和 double。

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

也可以将单位设置的小一点(分)

1.5 表必备三字段:id, gmt_create, gmt_modified。

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

1.6 表的命名最好是加上“业务名称_表的作用”。

正例:alipay_task / force_project / trade_config

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

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

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

2、索引规约

2.1 业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。

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

2.2 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度即可。

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

索引就像字典一样;A下面对应的一些单词,B下面对应的一些单词...,而这个ABC再索引中就是它的长度,比如

于 于佳 于佳鑫

如果索引长度是1 => 于:[于,于佳,于佳鑫]

如果索引长度是2 => 于:[于] 于佳: [于佳,于佳鑫]

如果索引长度是3 => 于:[于] 于佳: [于佳] 于佳鑫: [于佳鑫]

以此类推,长度越长,查询速度越快(区分度越高),但是内存消耗也越高,所以要找一个平衡点。

2.3 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。

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

2.4 如果有 order by 的场景,请注意利用索引的有序性

order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。

正例:

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

where a=? order by c

反例:

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

where b=? order by c 由于最左原则不会使用索引

2.5 利用延迟关联或者子查询优化超多分页场景。

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

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

SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id

2.6 建组合索引的时候,区分度最高的在最左边。

正例:如果 where a=? and b=? ,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即 可。 说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where a>? and b=? 那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。

2.7 创建索引时避免有如下极端误解:

    1. 宁滥勿缺。认为一个查询就需要建一个索引。
    1. 宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。
    1. 抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决。

3、SQL语句

3.1 不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的 标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。

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

3.2 in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控 制在1000 个之内。

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

4、ORM 映射

由于讲的是mybatis,日后再补

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

六、工程结构

1、应用分层

1.1 图中默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于Web 层,也可以直接依赖于 Service 层,依此类推:

p3c规范学习插图6
  • 开放接口层:可直接封装Service层方法暴露的RPC接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。
  • 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。
  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service 层:相对具体的业务逻辑服务层。
  • Manager 层:通用业务处理层,它有如下特征:(我们的remote包+ 我们的service层部分功能下降)
      1. 第三方平台封装的层,预处理返回结果及转化异常信息;
      1. 对Service层通用能力的下沉,如缓存方案、中间件通用处理;
      1. 与DAO层交互,对多个DAO的组合复用。
  • DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。(我们的dmn层)
  • 外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。(模型、指标、档案系统

1.2 分层领域模型规约:

  • DO(Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象
  • BO(Business Object):业务对象。由 Service 层输出的封装业务逻辑的对象。
  • AO(ApplicationObject):应用对象。在Web层与Service层之间抽象的复用对象模型, 极为贴近展示层,复用度不高。
  • VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
  • Query:数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止
    使用 Map 类来传输。

。。。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » p3c规范学习

一个分享Java & Python知识的社区