Java开发规范整理
(参考《Java开发手册嵩山版》)
文章目录
一、编程规约
(一)命名
- 宗旨:表达清楚,望文知意,尽量完整单词组合,不要嫌长
- 注意点:
- 命名规则五不能:
- 不能"_“和”$"开始、结束
- 不能拼音英文混合
- 不能中文
- 不能种族歧视词语
- 不能子父类成员变量、不同代码块局部变量完全相同
- 类名、方法名、变量名、常量名、包名命名举例:
- 类名:UpperCamelCase、HtmlTDO、AbstractSquare、int[] arrayDemo
- 方法名、参数名、成员变量名、局部变量名:localValue、POJO类属性boolean deleted对应MySQL字段is_deleted(需要在<resultMap>设置从 is_xxx 到 Xxx 的映射关系)
- 常量名:MAX_STOCK_COUNT
- 包名:单数形式,util
- 后缀提升辨识度,如:
- startTime变量的Time
- CacheServiceImpl接口实现类的Impl,与接口区别
- Translatable接口中的able,形容能力
- ProcessStatusEnum枚举类中的Enum
- OrderFactory类中的Factory,体现工厂设计模式
- UserDO类的DO表示数据对象,HtmlDTO类的DTO表示数据传输对象,WebsiteTitleVO的VO表示展示对象
- 方法前缀(Service/DAO层):
- 获取单个对象get,多个对象list
- 获取统计值count
- 插入save/insert、删除remove/delete、修改update
- 命名规则五不能:
(二)常量定义
- 宗旨:分开管理、分开存储
- 分开管理:不同常量类分开维护不同功能的常量
- 共享常量五层次存储:
- 类内:private static final
- 包内:放在当前包下单独的constant目录下
- 子工程内:放在当前子工程的constant目录下
- 应用内(一方库):放在子模块中的constant目录下
- 跨应用(二方库):client.jar中的constant目录下
- 其他注意点:
- 常量必须预先定义
- long或Long型,数值后用大写字母L:如Long a=2L
- enum类型定义在固定范围内变化的变量(春夏秋冬)、有名称之外延伸属性的变量(表示一年中的第几个季节的属性)
(三)代码格式
-
模板(不加解释和注释):
public void method(int a, StringBuilder sb) { double d = 1.0; int b = (int)d; if (a == b) { System.out.println("a等于1"); }else { System.out.println("a不等于1"); } if (1 == 1) {} sb.append("yang").append("hao")... .append("chen")... .append("chen")... .append("chen"); submethod(args1, args2, args3, ..., argsX); }
-
模板(加解释和注释):
public void method(int a, 逗号后面加空格StringBuilder sb)左大括号前空格 {左大括号后换行 /* * 单个方法总行数不要超过80行(除注释),善于抽出共性方法,使主干代码更清晰 * 注释双斜线与注释内容之间有且仅有一个空格 * 设置一个Tab缩进4个空格,否则禁用 */ double d运算符左边空格 = 运算符右边空格1.0; int b = (int)右括号与强制转换值之间不空格d; if 保留字与括号之间加空格(a运算符左边空格 == 运算符右边空格b) { System.out.println("a等于1"); }不换行else { System.out.println("a不等于1"); }表示终止的右大括号换行 //插入一个空行分隔不同逻辑、不同语义、不同业务的代码,提高可读性 if (1 == 1) {大括号内为空则不需空格和换行} // 超过 120 个字符的情况下,换行缩进 4 个空格,并且方法前的点号一起换行 sb.append("yang").append("hao")... .append("chen")... .append("chen")... .append("chen"); // 参数很多的方法调用可能超过 120 个字符,逗号后才是换行 submethod(args1, args2, args3, ..., argsX); }
(四)OOP面向对象程序设计
- 宗旨:降低耦合性、类的定义规范性、方法使用严谨性、提高速度、增强可读性
- 降低耦合性:
- 外部正在调用或者二方库依赖的接口,不允许修改方法签名
- 任何类、方法、参数、变量,严控访问范围(详见本节26)
- 类的定义规范性:(顺序:属性 > 构造方法 > 公有方法或保护方法 > 私有方法 > getter / setter 方法 > toString方法)(类中删除未使用的任何字段、方法、内部类) (避免存在过多的全局变量和静态方法和过多的外部依赖,不利于测试)
- 属性
- 货币金额以最小货币单位且整型类型存储
- 善于使用final(详见本节24),若是 static 成员变量,考虑是否为 final
- 序列化类新增属性时,不要修改 serialVersionUID 字段
- 定义 DO/DTO/VO 等 POJO 类时不设属性默认值,POJO 类属性必须使用包装数据类型,DO 类属性类型与数据库字段类型相匹配
- 构造方法
- 一个类多个构造方法,多个同名方法应该按顺序放在一起
- 构造方法里面禁止加入任何业务逻辑,初始化逻辑放在 init 方法中
- 自定义方法
- 可变参数:需相同参数类型,相同业务含义,且放在参数列表最后
- 所有的局部变量使用基本数据类型
- 在方法中删除未使用的任何参数声明与内部变量
- get、set方法
- setter 方法中,参数名称与类成员变量名称一致,getter/setter 方法中,不要增加业务逻辑
- POJO 类中不要同时存在对应属性 xxx 的 isXxx()和 getXxx()方法
- toString方法
- POJO 类必须写 toString 方法,如果继承了另一个 POJO 类,注意在前面加一下 super.toString
- 属性
- 方法使用严谨性
- 不能使用过时的类或方法
- 静态变量、静态方法类访问
- velocity 调用 POJO 类的属性时,直接使用属性名取值即可
- 使用常量或确定有值的对象来调用 equals
- 整型包装类对象之间值的比较用 equals
- 浮点数存储有精度损失,在比较、类型转换时要用特殊方式(详见手册本节9、12)
- RPC 方法的返回值和参数必须使用包装数据类型
- String 的 split 方法需做最后一个分隔符后有无内容的检查
- 对象 clone 方法默认是浅拷贝,慎用,深拷贝需覆写
- Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够 取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后 取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法
- 提高速度:
- 循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展
- 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
- 避免用 Apache Beanutils 进行属性的 copy, Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier,注意 均是浅拷贝
- 增强可读性:
- 覆写方法加@Override 注解
- 接口过时必须加@Deprecated 注解
- 降低耦合性:
(五)时间日期
- 宗旨:规范、提高速度、考虑特殊情况
- 规范(yyyy-MM-dd HH:mm:ss)
- 提高速度(获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime())
- 考虑特殊情况(闰年问题、2月问题)
- 其他注意点:
- YYYY 代表是 week in which year(JDK7 之后 引入的概念),一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年
- 不能用java.sql.Date等,用java.util.Date等
- Date,Calendar 等日期相关类的月份 month 取值在 0-11 之间
- 使用枚举值来指代月份: Calendar.JANUARY,Calendar.FEBRUARY,Calendar.MARCH 等
(六)集合处理
- 宗旨:严谨使用避免错误、合理使用提高性能
- 严谨使用避免错误
- Set:
- Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,只要覆写 equals,就必须覆写 hashCode,才能作为key,如String类
- Collections 、Collection
- Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list, 不可对其进行添加或者删除元素的操作。
- 使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行 NPE 判断
- 使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要使 用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,当出现 key 重复时,自定义对 value 的处理策略,但当value为null时抛NPE异常(详见本节3、4)
- ArrayList
- ArrayList 的 subList 结果不可强转成 ArrayList,subList()返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身
- 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一 致、长度为 0 的空数组,如String[] array = list.toArray(new String[0]);
- 使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配 器模式,只是转换接口,后台的数据仍是数组。
- Map
- 注意Map类集合K/V不能存储null值的情况(详见本节19)
- 使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添 加元素操作
- 使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历
- 泛型
- 泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法, 而不能使用 get 方法
- 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行 instanceof 判断
- 在 JDK7 及以上,直接使用<>来指代前边已经指定的类型
- foreach
- 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
- Set:
- 合理使用提高性能
- 判断所有集合内部的元素是否为空,使用 isEmpty()方法,而不是 size()==0 的方式
- 集合初始化时,指定集合初始值大小。
- 合理利用好集合的有序性(sort)和稳定性(order),如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort
- 利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains()进行遍历去重或者判断包含操作
- 严谨使用避免错误
(七)并发处理
- 概述:保证线程安全、恰当使用锁、善用线程池、注意高并发内存泄漏
- 保证线程安全
- 资源驱动类、工具类、单例工厂类都需要注意
- SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static, 必须加锁,或者使用 DateUtils 工具类,JDK8 可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat
- 多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题
- 使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方 法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。子线程抛出异常堆栈,不能在主线程 try-catch 到
- 避免 Random 实例被多线程使用,虽然线程安全,但会因竞争同一 seed 导致的性能下降。 JDK7 之后,可以直接使用 API ThreadLocalRandom, JDK7 之前,需要编码保证每个线程持有一个单独的 Random 实例
- 多写问题:如果是 count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); JDK8推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)
- ThreadLocal 对象使用 static 修饰,这个变量是针对一个线程内所有操作共享的
- 恰当使用锁
- 考量锁的性能损耗
- 对多个资源、数据库表、对象同时加锁时,保持一致的加锁顺序
- 使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代 码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。
- 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁
- 要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
- 资金相关的金融敏感信息,使用悲观锁策略。
- 多读问题:双重检查锁场景推荐将目标属性声明为 volatile 型,volatile是一个特征修饰符,作为指令关键字,确保本条指令不会因编译器的优化而省略
- 善用线程池
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,可以更加明确线程池的运行规则,规避资源耗尽的风险
- 注意高并发内存泄漏
- 必须回收自定义的 ThreadLocal 变量,在代理中使用 try-finally 块进行回收
- HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中注意规避此风险。
- 保证线程安全
(八)控制语句
- 宗旨:逻辑可读性强、使用严谨、性能考量、代码规范
- 逻辑可读性强
- 一个 switch 块内,每个 case 要么通过 continue/break/return 等来终止,要么 注释说明程序将继续执行到哪一个 case 为止
- 除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,赋值一个非常好理解的布尔变量名字
- 表达异常的分支时,少用 if-else 方式,超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式(见手册本节7)
- 不要在其它表达式(尤其是条件表达式)中,插入赋值语句。
- 避免采用取反逻辑运算符。
- 使用严谨
- 当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断。
- 三目运算符 condition? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐 时,可能抛出因自动拆箱导致的 NPE 异常。
- 在高并发场景中,避免使用”等于”判断作为中断或退出的条件。
- 性能考量
- 定义对象、变量、 获取数据库连接,不必要的 try-catch 操作尽量移至循环体外处理
- 公开接口需要进行入参保护,尤其是批量操作的接口。
- 调用频次低的方法、执行时间开销很大的方法、需要极高稳定性和可用性的方法、对外提供的开放接口、敏感权限入口需要进行参数校验
- 循环调用的方法、底层调用频度比较高的方法、被声明成 private 只会被自己代码所调用的方法不需要参数校检
- 代码规范
- 当某个方法的代码总行数超过 10 行时,return / throw 等中断逻辑的右大括号后均 需要加一个空行。
- if/else/for/while/do 语句中必须使用大括号。
- 逻辑可读性强
(九)注释规约
-
宗旨:格式规范、注释全面不冗杂、及时修改
-
注意点:
-
格式规范
- 类、类属性、类方法:使用Javadoc 规范,所有的类都必须添加创建者和创建日期。
- 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使 用/* */注释,注意与代码对齐。
-
注释全面不冗杂
- 注释要准确反映设计思想和代码逻辑,能够描述业务含义
- 过多过滥的注释
- 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。
- 对子类的实现要求,或者调用注意事项,请一并说明。
- 所有的枚举类型字段必须要有注释,说明每个数据项的用途。
-
及时修改
- 代码修改的同时,注释也要进行相应的修改
- 谨慎注释掉代码,如果无用,则删除
- 待办事宜等类型的注释标记,要通过标记扫描, 经常清理此类标记
-
(十)前后端规约
-
概述:API与协议规定、数据格式与类型规定
-
API与协议规定
- 前后端交互的 API,需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体。(详见本节1)
- 服务端发生错误时,返回给前端的响应信息必须被标记是否可以缓存,必须包含 HTTP 状态码:errorCode、 errorMessage(前后端错误追踪机制的体现,可以在前端输出到 type=“hidden” 文字类控件中,或者用户端的日志中,帮助我们快速地定位出问题)、用户提示信息四个部分。(详见本节3)
- HTTP 请求通过 URL 传递参数时,不能超过 2048 字节,HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错。
- 版本控制在 HTTP 头信息中体现
-
数据格式与类型规定
- 服务端返回的数据,使用 JSON 格式而非 XML。在前后端交互的 JSON 格式数据中,所有的 key 必须为小写字母开始的,如:errorCode
- 前后端的时间格式统一为"yyyy-MM-dd HH:mm:ss",统一为 GMT。
- 对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用 Long 类型。
-
-
其他注意点
- 服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成
- 减少null值判断:前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}
- 后台输送给页面的变量必须加$!{var}——中间的感叹号,避免因var为null值而直接显示在页面上
二、异常日志
(一)错误码
-
宗旨:快速溯源、沟通标准化、先到先得同一平台审批发布、发布后永久固定
-
错误码构成: 错误产生来源+四位数字编号(字符串类型),没有错误但不得不填充时:00000
- 错误产生来源三类:A、B、C
- A 表示错误来源于用户,宏观错误码:A0001(用户端错误)
- B 表示错误来源于当前系统,宏观错误码:B0001(系统执行出错)
- C 表示错误来源于第三方服务,宏观错误码:C0001(调用第三方服务出错)
- 四位数字编号
- 从 0001 到 9999,大类之间的步长间距预留 100
- 数字是一个整体,每位数字的地位和含义都是相同的
- 后三位与HTTP状态码无关
- 错误产生来源三类:A、B、C
-
其他注意点:
-
错误码不体现版本号和错误等级,以追加的方式兼容,错误等级由日志和错误码本身的释义决定。
-
不能越俎代庖,错误码之外的业务独特信息由 error_message 来承载,错误码不能直接输出给用户
-
获取第三方服务错误码时,向上抛出允许本系统转义(C 转B),并在错误信息上带上原第三方错误码
-
(二)异常处理
- 宗旨:能规避则规避、能处理则处理、异常精准定位、异常类型具体、耦合交互场景异常说明到位
- 能规避则规避、能处理则处理
- 可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,如:NullPointerException,IndexOutOfBoundsException 等
- 异常的处理效率比条件判断方式要低很多,异常捕获后不要用来做流程控制,条件控制。
- 尽可能处理异常,如果抛给调用者,则最外层的业务使用者,必须处理异常
- 异常精准定位、异常类型具体
- catch 时请分清稳定代码和非稳定代码,对于非稳定代码的 catch 尽可能进行区分异常类型
- 捕获异常是抛异常的父类
- 耦合交互场景异常说明到位
- 内部应用直接抛出异常,公司外的 http/api 开放接口必须使用 errorCode,跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、errorCode、 errorMessage
- 方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
- 能规避则规避、能处理则处理
- 其他注意点:
- 事务:
- 抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务
- 调用 RPC、二方包、或动态生成类的相关方法时:
- 捕捉异常必须使用 Throwable 类来进行拦截,以便捕获到各种异常
- try-catch-finally
- finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。如果 JDK7 及以上,可以使用 try-with-resources 方式,自动进行流关闭
- 不要在 finally 块中使用 return,否则将无情丢掉try中的返回点
- NPE(空指针异常)
- 可能发生NPE的场景:
- 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE
- 数据库的查询结果可能为 null。
- 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
- 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
- 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
- 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
- 防止 NPE:
- 使用 JDK8 的 Optional 类,Optional类是一个可以为null的容器对象,提供很多方法,我们不用显式地进行空值检测,如调用get()方法前,会调用isPresent()方法,若返回值为true,则调用get()
- 可能发生NPE的场景:
- 事务:
(三)日志规约
- 概述:使用日志框架、规范存储和命名、日志打印输出规范和开销
- 使用日志框架
- 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 (SLF4J、JCL–Jakarta Commons Logging)中的 API(推荐使用 SLF4J),使用门面模式的日志框架,有利于维护和 各个类的日志处理方式统一。
- 规范存储和命名
- 所有日志文件至少保存 15 天,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存 的日志不少于六个月,并且进行网络多机备份
- 推荐对日志进行分类,如将错误日志和业务日志分开存放
- 对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,过往日志格式为: 应用名.log.保存日期,日期格式:yyyy-MM-dd
- 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式: 应用名 ‾ \underline{} 日志类型 ‾ \underline{} 日志描述.log。logType:日志类型,如 stats/monitor/access 等;logName:日志描述。
- 日志打印输出规范和开销
- 规范
- 日志打印时禁止直接用 JSON 工具将对象转换成 String。打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
- 异常信息应该包括两类信息:案发现场信息和异常堆栈信息,如果不处理,那么通过 关键字 throws 往上抛出。
- 使用 warn 日志级别来记录用户输入参数错误的情况,error 级别只记录系统逻辑出错、异常或者重要的错误信息
- 开销
- 在日志输出时,字符串变量之间的拼接使用占位符的方式,如logger.debug(“Processing trade with id: {} and symbol: {}”, id, symbol); String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅 是替换动作,可以有效提升性能
- 对于 trace/debug/info 级别的日志输出,判断日志级别开关debug是否打开,避免无谓方法调用的开销
- 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false。
- 生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace()打印异常堆栈。标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易 造成文件大小超过操作系统大小限制。
- 生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑 爆,并记得及时删除这些观察日志。
- 规范
- 使用日志框架
三、单元测试
- 宗旨:AIR、BCDE、及时补充、规范存储
- 自动化、独立性、可重复(AIR)
- Automatic:单元测试中不准使用 System.out 人肉验证,必须使用 assert 来验证
- Independent:单元测试用例之间 决不能互相调用,也不能依赖执行的先后次序
- Repeatable:单元测试通常会被放到持续集成中,每次有代码check in时都会执行
- 粒度尽量小:有助于精确定位问题,单测粒度至多是类级别,一般是方法级别。
- 保证工程交付质量(BCDE)
- B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等
- C:Correct,正确的输入,并得到预期的结果。
- D:Design,与设计文档相结合,来编写单元测试。
- E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。
- 单元测试最好覆 盖所有测试用例
- 不建议项目发布后补 充单元测试用例。
- DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。
- 及时补充、规范存储
- 新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正
- 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。
- 自动化、独立性、可重复(AIR)
- 其他注意点:
- 数据库相关测试
- 使用程序插入或者导入数据的方式来准备数据,以免新增数据不符合插入规则导致测试失败。
- 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识。
- 数据库相关测试
四、安全规约
- 概述:校检验证、脱敏、过滤、避免恶意攻击
- 校检验证
- 隶属于用户个人的页面或者功能必须进行权限控制校验。
- 用户请求传入的任何参数必须做有效性验证,否则造成如SSRF(Server-Side Request Forgery)服务端请求伪造的危险,攻击者借由服务端为跳板来攻击目标系统
- 表单、AJAX 提交必须执行 CSRF (Cross-site request forgery跨站请求伪造)(攻击者借用你的名义发起请求)安全验证。
- 脱敏
- 用户敏感数据禁止直接展示,必须对展示数据进行脱敏,如139****1219
- 过滤
- 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。
- URL 外部重定向传入的目标地址必须执行白名单过滤。
- 避免恶意攻击
- 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入, 禁止字符串拼接 SQL 访问数据库。
- 使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机 制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。
- 校检验证
五、MySQL数据库
(一)建表规约
- 概述:表的创建标准、规范命名、合理使用字段类型
- 表的创建标准
- 表的必备三字段
- id:主键,bigint unsign类型,单表时自增,步长为1
- create_time:主动式创建,datetime类型
- updated_time:被动式更新,datetime类型
- 分库分表
- 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
- 表的必备三字段
- 规范命名
- 禁用保留字,如 desc、range、match、delayed 等
- 库名与应用名称尽量一致。
- 表名不使用复数名词,最好是遵循“业务名称_表的作用”。
- 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字
- 字段允许适当冗余
- 主键索引名为 pk ‾ \underline{} 字段名;唯一索引名为 uk ‾ \underline{} 字段名;普通索引名则为 idx ‾ \underline{} 字段名。
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint (1 表示是,0 表示否)。
- 合理使用字段类型
- 小数类型为 decimal,禁止使用 float 和 double,避免精度损失,如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。
- 如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
- varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度 大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效 率。
- 合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度,无符号值可以避免误存负数,且扩大了表示范围。(详见本节15)
- 表的创建标准
(二)索引规约
- 宗旨:考虑数据库性能
- 能够建立索引的种类分为主键索引、唯一索引(数据库性能优化目标consts级)、普通索引(数据库性能优目标ref级)三种,另外还有范围检索(数据库性能优化目标range级),而覆盖索引只是一种查询的一种效 果,用 explain 的结果(比range级还低),extra 列会出现:using index。
- 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引,但索引不要太多,没必要不新建。
- 超过三个表禁止 join,需要 join 的字段,数据类型保持绝对一致;多表关联查询时, 保证被关联的字段需要有索引。即使双表 join 也要注意表索引、SQL 性能。
- 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据 实际文本区分度决定索引长度。一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上
- 先快速定位需要获取的 id 段,然后再关联(延迟关联)
- 建组合索引的时候,区分度最高的在最左边,把等号条件的列前置。
- 其他注意点:
- 索引如果存在范围查询,那么索引有序性无法利用
- 防止因字段类型不同造成的隐式转换,导致索引失效。
- 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决
(三)SQL语句
-
宗旨:严谨使用、高效查询
- 严谨使用
- 数据订正(特别是删除或修改记录操作)时,要先 select,避免出现误删除
- 对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或 表名)进行限定。
- SQL 语句中表的别名前加 as,并且以 t1、t2、t3、…的顺序依次命名。
- 高效查询
- in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控 制在 1000 个之内。
- 代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
- 不得使用外键与级联,一切外键概念必须在应用层解决。外键影响数据库 的插入速度。
- 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
- 严谨使用
-
其他注意点:
-
NULL
- count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
- count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
- 当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题
- 使用 ISNULL()来判断是否为 NULL 值。 说明:NULL 与任何值的直接比较都为 NULL。
-
TRUNCATE 与 DELETE
- TRUNCATE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE 无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。
-
LENGTH 与 CHARACTER_LENGTH
- 所有的字符存储与表示均采用 utf8 字符集,字符计数方法需要注意。SELECT LENGTH(“轻松工作”); 返回为 12 ,SELECT CHARACTER_LENGTH(“轻松工作”); 返回为 4 如果需要存储表情,那么选择 utf8mb4 来进行存储,注意它与 utf8 编码的区别。
-
(四)ORM对象关系映射
- 概述:映射关系规范配置、操作严谨、注意性能
- 映射关系规范配置
- POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行 字段与属性之间的映射。
- 不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要 定义;反过来,每一个表也必然有一个与之对应。配置映射关系,使字段与 DO 类解耦,方便维护。
- sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。
- 操作严谨
- 不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出
- 更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。
- 注意性能
- 不要更新无改动的字段
- 在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明,减少成本与消耗
- iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使用,性能很慢
- @Transactional 事务不要滥用,会影响数据库QPS
- 映射关系规范配置
- 其他注意点:
- <isEqual>中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时带上此条件;<isNotEmpty>表示不为空且不为 null 时执行;<isNotNull>表示不为 null 值 时执行
六、工程结构
(一)应用分层
- 概述:使用推荐的分层结构、分层异常处理规范
- 推荐的分层结构
- 最顶层:
- 终端显示层(velocity 渲染,JS 渲染,JSP 渲染,移动端展示)
- 显示层对象VO(View Object):通常是 Web 向模板渲染引擎层传输的对象。
- 开放API层(可直接封装 Service 接口暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等)
- 终端显示层(velocity 渲染,JS 渲染,JSP 渲染,移动端展示)
- 第二层:
- Web层(对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理)
- 第三层:
- Service层(相对具体的业务逻辑服务层)
- 业务对象BO(Business Object):可以由 Service 层输出的封装业务逻辑的对象。
- 数据传输对象DTO(Data Transfer Object):Service 或 Manager 向外传输的对象。
- 数据查询对象Query:各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类 来传输。
- Service层(相对具体的业务逻辑服务层)
- 第四层:
- Manager层(通用业务处理层)
- 数据传输对象DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
- 数据查询对象Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类 来传输。
- Manager层(通用业务处理层)
- 第五层:
- DAO层(与底层 MySQL、Oracle、Hbase、OB 等进行数据交互)
- 数据对象DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
- 第三方服务层(其它部门 RPC 服务接口,基础平台,其它公司的 HTTP 接口,如淘宝开放平台、支 付宝付款服务、高德地图服务等)
- DAO层(与底层 MySQL、Oracle、Hbase、OB 等进行数据交互)
- 最底层:
- 数据存储系统
- 外部数据接口(外部(应用)数据存储服务提供的接口,多见于数据迁移场景中)
- 最顶层:
- 分层异常处理规范
- 开放接口层要将异常处理成错误码和错误信息方式返回。
- Web 层绝不应该继续往上抛异常,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面, 尽量加上友好的错误提示信息
- Service 层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息, 相当于保护案发现场
- Manager 层:
- 与 Service 同机部署,日志方式与 DAO 层处理一致
- 单独部署,则采用与 Service 一致的处理方式
- DAO 层使用 catch(Exception e)方式,并 throw new DAOException(e),不需要打印日志
- 推荐的分层结构
(二)二方库依赖
- 概述:二方库依赖规范、二方库命名格式规范、二方库定义规范、二方库发布和升级规范
- 二方库依赖规范
- 线上应用不要依赖 SNAPSHOT 版本, 用RELEASE 版本,尽量使用业界稳定的二方工具包
- 依赖于一个二方库群时,必须定义一个统一的版本变量,避免版本号不一致。
- 禁止在子项目的 pom 依赖中出现相同的 GroupId,相同的 ArtifactId,但是不同的 Version。
- 如果依赖其它二方库,尽量是 provided 引入,让二方库使用者去依赖具体版本号;无 log 具体实现,只依赖日志框架。
- 所有 pom 文件中的依赖声明放在<dependencies>语句块中,<dependencies>所有声明在主 pom 的里的依赖都会自动引入,并默认被所有的子项目继承。
- 所有版本仲裁放在<dependencyManagement>语句块中,只是声明版本,并不实现引入,因此子项目需要显式的声明依赖, version 和 scope 都读取自父 pom。
- 二方库命名格式规范
- GroupID 格式:com.{公司/BU }.业务线 [.子业务线],最多 4 级。如com.alibaba.dubbo.register
- ArtifactID 格式:产品线名-模块名。如jstorm-tool
- Version:主版本号.次版本号.修订号。起始版本号必须为:1.0.0
- 二方库定义规范
- 二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚 举类型或者包含枚举类型的 POJO 对象。
- 移除一切不必要的 API 和依赖,只包含 Service API、必要的领域模型对象、Utils 类、 常量、枚举等。
- 二方库不要有配置项
- 二方库发布和升级规范
- 正式发布的类库必须先去中央仓 库进行查证,使 RELEASE 版本号有延续性,且版本号不允许覆盖升级。
- 二方库应保持稳定,且由谁维护,源码在哪里,都需要能方便查到
- 二方库的新增或升级,保持除功能点之外的其它 jar 包仲裁结果不变
- 二方库依赖规范
(三)服务器
- 宗旨:提高高并发性能
- 高并发服务器建议调小 TCP 协议的 time_wait 超时时间,linux 服务器上请通过变更/etc/sysctl.conf 文件去修改该缺省值(秒):net.ipv4.tcp_fin_timeout = 30
- 建议将 linux 服务器所支持的最大句柄数调高数倍
- 线上生产环境,JVM 的 Xms 和 Xmx 设置一样大小的内存容量,避免在 GC 后调整 堆大小带来的压力。
- 其他注意点:
- 给 JVM 环境参数设置-XX:+HeapDumpOnOutOfMemoryError 参数,让 JVM 碰到 OOM 场景时输出 dump 信息,出错时的堆内信息对解决问题非常有帮助
- 服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL Broker 生成,否 则因线上采用 HTTPS 协议而导致浏览器提示“不安全“。此外,还会带来 URL 维护不一致的 问题。
七、设计规约
- 概述:需求分析 -> 系统设计 -> 评审并形成设计文档
- 需求分析:
- **例图:**与系统交互的 User 超过一类并且相关的 User Case 超过 5 个时,用例图表达更加清晰的结构化需求
- **状态图:**某个业务对象的状态超过 3 个时,用状态图来表达并且明确状态变化的各个触发条件。
- **时序图:**系统中某个功能的调用链路上的涉及对象超过 3 个时,用时序图来表达并且明确 各调用环节的输入与输出。
- **类图:**系统中模型类超过 5 个,并且存在复杂的依赖关系时,用类图来表达并且明确类 之间的关系。
- **活动图:**系统中超过 2 个对象之间存在协作关系,并且需要表示复杂的处理流程时,用活 动图来表示。
- 系统设计:
- 系统架构设计:
- 确定系统在技术层面上的做与不做
- 确定模块之间的依赖关系及模块的宏观输入与输出
- 确定指导后续设计与演化的原则,使后续的子系统或模块设计在一个既定的框架内和技术方向上继续演化
- 系统内部设计原则:
- Don’t Repeat Yourself:共性业务或公共行为抽取出来公共模块、公共配置、公共类、公共方 法等,在系统中不出现重复代码的情况
- 尽量依赖抽象类与接口,有利于扩展与维护
- 对扩展开放,对修改闭合,找到系统的变化点,并隔离变化点
- 考虑主干功能的同时,需要充分评估异常流程与业务边界
- 设计文档:核心关键点上的必要设计和文档有助于后期的系统维护和重构,设计结果需要进行分类归 档保存
- 类、接口设计原则:
- 类在设计与实现时要符合单一原则
- 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现,不得已使用继承的话,保证父类能够出现的地方子类一定能够出现
- 无障碍产品设计注意点:
- 所有可交互的控件元素必须能被 tab 键聚焦
- 用于登录校验和请求拦截的验证码均需提供图形验证以外的其它方式。
- 自定义的控件类型需明确交互方式。
- 系统架构设计:
- 评审内容:
- 存储方案:包括存储介质选型、表结构设计能否满足技术方案、存取性能和存储空间能否满足业务发展、表或字段之间的辩证关系、字段名称、字段类型、索引等;
- 底层数据结构的设计:数据结构变更(如在原有表中新增字段) 也需要进行评审通过后上线。
- 需求分析: