1.编程规约
1.1 命名风格
-
杜绝完全不规范的缩写,避免望文不知义
-
代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束
-
代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式
-
为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意
-
包命名
- 使用小写字母命名
- 域名倒写使用 . 符号分割,每个 . 之间有且只有一个自然单词
- 包名不能为复数形式,统一使用单数
- 包中类可以使用复数形式,如 com.alibaba.ai.util,MessageUtils
-
类命名
- 类命名一般使用 UpperCamelCase 风格
- 特殊情况,如 DO/BO/DTO/VO/AO/PO/UID 等可以不为上面的通用格式
- 抽象类命名使用 Abstract 或者 Base 开头
- 接口命名无特殊规则
- 但内部的接口与接口的实现类要使用 Impl 后缀进行区分 如:CacheServiceImpl 实现 CacheService 接口
- 如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词) 如:AbstractTranslator 实现 Translatable 接口
- 异常类使用 Exception 结尾
- 测试类使用被测试类名称 + Test 命名
- 枚举类使用类名 + Enum 命名
- 如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式
-
类内部成员命名
- 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式
- 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度
- 常量
- 命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长
- 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中
- 不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护
- 常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量
- 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下
- 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下
- 子工程内部共享常量:即在当前子工程的 constant 目录下
- 包内共享常量:即在当前包下单独的 constant 目录下
- 类内共享常量:直接在类内部 private static final 定义
// 反例 类 A 中:public static final String YES = "yes"; 类 B 中:public static final String YES = "y"; A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致线上问题。
- 数组:类型与中括号紧挨相连来表示数组
- POJO 类中布尔类型变量都不要加 is 前缀,否则部分框架解析会引起序列化错误
- 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可读性降低
// 反例 public class ConfusingName { public int age; // 非 setter/getter 的参数名称,不允许与本类成员变量同名 public void getData(String alibaba) { if (condition) { final int money = 531; // ... } for (int i = 0; i < 10; i++) { // 在同一方法体中,不允许与其它代码块中的 money 命名相同 final int money = 615; // ... } } } class Son extends ConfusingName { // 不允许与父类的成员变量名称相同 public int age; } -
如果变量值仅在一个固定范围内变化用 enum 类型来定义
-
接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量
-
各层命名规约
- Service/DAO 层方法命名规约
- 获取单个对象的方法用 get 做前缀
- 获取多个对象的方法用 list 做前缀,复数形式结尾如:listObjects
- 获取统计值的方法用 count 做前缀
- 插入的方法用 save/insert 做前缀
- 删除的方法用 remove/delete 做前缀
- 修改的方法用 update 做前缀
- 领域模型命名规约
- 数据对象:xxxDO,xxx 即为数据表名
- 数据传输对象:xxxDTO,xxx 为业务领域相关的名称
- 展示对象:xxxVO,xxx 一般为网页名称
- POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO
- Service/DAO 层方法命名规约
1.2 代码格式
- 单个方法的总行数不超过 80 行
- 注释的双斜线与注释内容之间有且仅有一个空格
// 这是示例注释,请注意在双斜线之后有一个空格 String param = new String(); - 任何二目、三目运算符的左右两边都需要加一个空格
- 采用 4 个空格缩进,禁止使用 tab 字符
- 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格;而左大括号前需要空格
- if/for/while/switch/do 等保留字与括号之间都必须加空格
- 如果是大括号内为空,则简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块则:
- 左大括号前不换行
- 左大括号前不换行
- 左大括号前不换行
- 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行
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"); // 在右大括号后直接结束,则必须换行 } } - 单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则
- 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例
- 运算符与下文一起换行
- 方法调用的点符号与下文一起换行
- 方法调用中的多个参数需要换行时,在逗号后进行
- 在括号前不要换行
public static void main(String[] args) { StringBuilder sb = new StringBuilder(); // 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行 sb.append("Jack").append("Ma") .append("alibaba") .append("alibaba") .append("alibaba"); } - IDE 的 text file encoding 设置为 UTF-8,IDE 中文件的换行符使用 Unix 格式
- 方法参数在定义和传入时,多个参数逗号后边必须加空格
1.3 OOP规约
- 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可
- 所有的覆写方法,必须加 @Override 注解
- 相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object
- 外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响
- 接口过时必须加 @Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么
- 不能使用过时的类或方法
- Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals
- 所有整型包装类对象之间值的比较,全部使用 equals 方法比较(如避免 Integer -128 ~ 127 的缓存)
- 浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断
public static void main(String[] args) { // 浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。 // 二进制无法精确表示大部分的十进制小数,具体原理参考《码出高效》 // 反例 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 } // 正例 1 指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的 float i = 1.0f - 0.9f; float j = 0.9f - 0.8f; float diff = 1e-6f; if (Math.abs(i - j) < diff) { System.out.println("true"); } // 正例 2 使用 BigDecimal 来定义值,再进行浮点数的运算操作 BigDecimal e = new BigDecimal("1.0"); BigDecimal f = new BigDecimal("0.9"); BigDecimal c = new BigDecimal("0.8"); BigDecimal m = e.subtract(f); BigDecimal n = f.subtract(c); if (m.equals(n)) { System.out.println("true"); } } - 定义数据对象 DO 类时,属性类型要与数据库字段类型相匹配
- 为了防止精度损失,禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象
// 实际的存储值为:0.10000000149,不能使用 BigDecimal g = new BigDecimal(0.1f); // 应该使用 优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法 // 此方法内部其实执行了 Double 的 toString // Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断 BigDecimal recommend1 = new BigDecimal("0.1"); BigDecimal recommend2 = BigDecimal.valueOf(0.1); - POJO的相关规范
- 所有的 POJO 类属性必须使用包装数据类型
- 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
- 序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值
- POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString
- 禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx()和 getXxx()方法
- 框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到
- 只要覆写 equals,就必须覆写 hashCode
- RPC 方法的返回值和参数必须使用包装数据类型
1.4 集合处理
- 对于集合存储的对象,如果该对象 复写了 equals 方法一定要复写 hashCode 方法
- ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
- 使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常
- Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list,不可对其进行添加或者删除元素的操作
- 在 subList 场景中,高度注意对原集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常
- 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组
- 直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误
- 在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行 NPE 判断
- 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行 instanceof 判断,避免抛出 ClassCastException 异常
- 毕竟泛型是在 JDK5 后才出现,考虑到向前兼容,编译器是允许非泛型集合与泛型集合互相赋值
- 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁
- 在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sort,Collections.sort 会抛 IllegalArgumentException 异常
- x,y 的比较结果和 y,x 的比较结果相反
- x > y,y > z,则 x > z
- x = y,则 x,z 比较结果和 y,z 比较结果相同
1.5 并发处理
- 获取单例对象需要保证线程安全,其中的方法也要保证线程安全
- 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
- FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
- CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
- SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类
- 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题,尽量在代理中使用 try-finally 块进行回收
- 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁
- 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法,RPC 调用的阻塞时间比较长
- 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
- 线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁
- 在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无解锁
- 如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁
- 如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛 IllegalMonitorStateException 异常
- 在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与二相同
- 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同
- 没有获取锁就不能释放锁
- 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据
- 多线程并行处理定时任务时强制使用 ScheduledExecutorService 不能使用 Timer
- Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行
- 在高并发场景中,避免使用”等于”判断作为中断或退出的条件,建议使用区间值去作为终止推出条件
1.6 注释规约
- 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用// xxx 方式
- 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能
- 所有的类都必须添加创建者和创建日期
- 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐
- 所有的枚举类型字段必须要有注释,说明每个数据项的用途
1.7 其他
- 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度
- 不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
- 注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法
- 获取当前毫秒数 System.currentTimeMillis(); 而不是 new Date().getTime();
- 如果想获取更加精确的纳秒级时间值,使用 System.nanoTime()的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类
- 日期格式化时,传入 pattern 中表示年份统一使用小写的 y
- 日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),意思是当天
- 表示月份是大写的 M
- 表示分钟则是小写的 m
- 24 小时制的是大写的 H
- 12 小时制的则是小写的 h
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
2 异常日志
2.1 异常处理
- Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等
- 能够预先判断的的异常不是异常,只有不能确定的异常才应该使用 try - catch - finally 处理
- 异常不要用来做流程控制,条件控制
- 控制 try - catch 中的代码范围,不应该将不会出现异常的代码放入 try - catch 中处理
- 对于需要进行异常处理的代码应该进行异常的分类,对于不同的异常进行不同的处理
- 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容
- 有 try 块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回滚事务
- finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch
- 不要在 finally 块中使用 return,finally 中的 return 会覆盖 try 中的 return 点
- 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类
- 在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable 类来进行拦截
2.2 日志规约
- 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一
- 所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。网络运行状态、安全相关信息、系统监测、管理后台操作、用户敏感操作需要留存相关的网络日志不少于 6 个月
- 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式appName_logType_logName.log,这种命名的好处是通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找
- logType:日志类型,如 stats/monitor/access 等
- logName:日志描述
- 在日志输出时,字符串变量之间的拼接使用占位符的方式
- 因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能
- 对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断
- 避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false
- 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通 过关键字 throws 往上抛出
3 单元测试
- 好的单元测试必须遵守 AIR 原则
- A:Automatic(自动化)
- I:Independent(独立性)
- R:Repeatable(可重复)
- 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证
- 保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序
- 单元测试是可以重复执行的,不能受到外界环境的影响
- 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别
- 核心业务、核心应用、核心模块的增量代码确保单元测试通过
- 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下
- 单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%
- 编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量
- B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等
- C:Correct,正确的输入,并得到预期的结果
- D:Design,与设计文档相结合,来编写单元测试
- E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果
4 安全规约
- 隶属于用户个人的页面或者功能必须进行权限控制校验
- 用户敏感数据禁止直接展示,必须对展示数据进行脱敏
- 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库
- 用户请求传入的任何参数必须做有效性验证
- 忽略参数校验可能导致
- page size 过大导致内存溢出
- 恶意 order by 导致数据库慢查询
- 任意重定向
- SQL 注入
- 反序列化注入
- 正则输入源串拒绝服务 ReDoS (Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果)
- 忽略参数校验可能导致
- 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据
- 表单、AJAX 提交必须执行 CSRF 安全验证
- 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的 机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损
5 数据库
5.1 建表规约
- 统一建表字符集
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)
- 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑
- 表名不使用复数名词
- 禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字
- 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名
- 小数类型为 decimal,禁止使用 float 和 double
- 如果存储的字符串长度几乎相等,使用 char 定长字符串类型
- varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率
- 表必备三字段:id, create_time, update_time
5.2 索引规约
- 业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引
- 超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引
- 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度即可
- 页面搜索严禁左模糊或者全模糊(最左匹配原则),如果需要请走搜索引擎来解决
- 如果有 order by 的场景,请注意利用索引的有序性,联合索引的排序顺序,从左至右
- 利用覆盖索引来进行查询操作,避免回表
- 利用延迟关联或者子查询优化超多分页场景
SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id - SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好
- consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据
- ref 指的是使用普通的索引(normal index)
- range 对索引进行范围检索
- 建组合索引的时候,区分度最高的在最左边
- 防止因字段类型不同造成的隐式转换,导致索引失效
- 防止字符集不同进行隐式转换
5.3 SQL 语句
- 不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关
- count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0
- 当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题
- 使用 ISNULL()来判断是否为 NULL 值
- 代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句
- 不得使用外键与级联,一切外键概念必须在应用层解决
- 数据订正(特别是删除、修改记录操作)时,要先 select,避免出现误删除,确认无误才能执行更新语句
5.4 ORM 映射
- 在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明
- POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射
- 不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个 POJO 类与之对应
- sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入
- iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使用
- 不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出
- 更新数据表记录时,必须同时更新记录对应的 gmt_modified 字段值为当前时间
6 工程结构
6.1 应用分层
- 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行网关安 全控制、流量控制等
- 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等
- Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等
- Service 层:相对具体的业务逻辑服务层
- Manager 层:通用业务处理层,它有如下特征
- 对第三方平台封装的层,预处理返回结果及转化异常信息
- 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理
- 与 DAO 层交互,对多个 DAO 的组合复用
- DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互
- 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口
- 分层领域模型规约
- DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象
- DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象
- BO(Business Object):业务对象,由 Service 层输出的封装业务逻辑的对象
- AO(Application Object):应用对象,在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高
- VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象
- Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输