《Java开发手册(嵩山版)》1.7.0版(2020.08.03)
制定团队:阿里巴巴与Java社区开发者

Java开发手册(嵩山版)

后面陆续看到的一些开发规范也补充在这里面

编程规约

阿里规约的话可以直接在IDEA中下载插件Alibaba Java Coding Guidelines
以下是过一遍手册后觉得需要重视以及自己还没有掌握的点:

命名风格

  • 类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
    要说明的一点是XMLService、TCPDeal这种是错误的,要注意开有没有黄色波浪线警告。
  • 抽象类命名使用 Abstract 或 Base 开头
  • POJO 类中的任何布尔类型的变量,都不要加 is 前缀(但是isDelete扫描器没有警告),否则部分框架解析会引起序列化错误。而阿里规约中是用is_xxx这种命名方式,需要在<resultMap>设置is_xxx到xxx的映射关系。
    反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
  • 这点感觉跟上一点有冲突:
    velocity 调用 POJO 类的属性时,直接使用属性名取值即可,模板引擎会自动按规范调用 POJO 的 getXxx() ,如果是 boolean 基本数据类型变量 (boolean 命名不需要加 is 前缀 ) ,会自动调用 isXxx() 方法(如果是 Boolean 包装类对象,优先调用 getXxx()的方法)。
  • POJO类中不要设任何属性默认值
  • 包名统一小写,且只有.不要有_,包名统一使用单数形式,类名用复数形式。
    另外,如果类中用到设计模式最好用OrderFactoryLoginProxyXxxObserver这样表现出来。
  • 如果接口不是xxxService—xxxServiceImpl,而是形容能力的接口名称,则要取对应的形容词为接口名(AbstractTranslator 实现 Translatable 接口)
  • 变量是nameList;方法是listName
    增insert/save(没有add)、删delete/remove、改update、查getXxx/listXxx、统计count

常量定义

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

    • 1) 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。
    • 2) 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。(如A中的String YES = "yes",B中的为String YES ="y" 。A.YES.equals(B.YES),预期是 true,但实际返回为 false,导致线上问题)
    • 3) 子工程内部共享常量:即在当前子工程的 constant 目录下。
    • 4) 包内共享常量:即在当前包下单独的 constant 目录下。
    • 5) 类内共享常量:直接在类内部 private static final 定义。
  • 要活用菜单(枚举)enum,如果变量值仅在一个固定范围内变化用 enum 类型来定义。(星期季节月份之类的,如0-11指代月份)
    菜单(枚举)enum的成员需要全大写,单词用下划线隔开。
    枚举的属性字段必须是私有且不可变,即private final
  • 对于集合类型的静态成员变量,应该使用静态代码块赋值,而不是使用集合实现来赋值。
//赋值静态成员变量反例
private static Map<String, Integer> map = new HashMap<String, Integer>(){{
        map.put("Leo",1);
        map.put("Family-loving",2);
        map.put("Cold on the out side passionate on the inside",3);
    }};
private static List<String> list = new ArrayList<>(){    {
        list.add("Sagittarius");
        list.add("Charming");
        list.add("Perfectionist");
    }};



//赋值静态成员变量正例
private static Map<String, Integer> map = new HashMap<String, Integer>();
static {
    map.put("Leo",1);
    map.put("Family-loving",2); 
    map.put("Cold on the out side passionate on the inside",3);
}
private static List<String> list = new ArrayList<>();
static {
    list.add("Sagittarius");
    list.add("Charming");
    list.add("Perfectionist");
}

代码格式

  • 空方法直接写成{},里面不需要空格!
    小括号相邻字符不需要空格:if/for/while/switch/do (a == 0)
  • 双斜杠//注释后面只有一个空格(VUE中注释后面要有两个空格)
  • 规范中定义强转中不要有空格(int)a而IDEA的代码自动对齐有空格(int) a
  • 代码超过120字要换行,.append()另取一行,,不跟随换行
  • 单个方法不超过80行

OPP规约

  • 尽量不要用可变参数变成(Object),如果有,要放参数列表最后面。
    (避免无法检测方法传参顺序错误)
  • 方法顺序:公有、保护、私有、getter/Setter
    方法在使用中时不能修改方法签名。接口过时要用@Deprecated注解,并在此说明新街口。
  • 构造方法禁止加入任何初始化逻辑,如有请放init()中。(现在都用注解直接生成构造)
  • 关于equal:推荐使用 JDK7 引入的工具类 java.util.Objects#equals(Object a, Object b)
  • 所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
    因为对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
  • 任何货币金额,均以最小货币单位且整型类型来进行存储。
    有点像开发过程中充值6元的amount=600。因为以最小的趣豆作为单位。
  • 规约中指出:浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals来判断。(补充:敢在处理诸如 商品金额、订单交易、以及货币计算时用浮点型数据(double/float)是会被开除的,危!)
    推荐使用BigDecimal来处理浮点数, BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法,因为equals()方法会比较值和精度 (1.0 与 1.00 返回结果为 false) ,而 compareTo()则会忽略精度。

IEEE754标准

日期和时间

  • new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")注意y是小写
    y当天所在年份、Y当周所在年份、m分钟、M月份、h12小时制的小时、H24小时制的小时、s
  • 不要把天数写死365。注意闰年2月份29天。
    获取今年int daysOfThisYear = LocalDate.now().lengthOfYear()
    获取指定某年LocalData.of(2011.11.11).lengthOfYear
  • Date老了,请灵活使用LocalDateTimeDateTimeFormatter!,这条我加的。
    食用方法在4.3

集合处理

  • 集合初始化时请指定大小,即使默认是16也要写上去。
  • 集合判空用isEmpty()而不要用size()==0方式。因为前者时间复杂度为O(1),且可读性更好。
  • 集合中任何addAll()方法(如:toArray()),都要就行NPE判断。
List<Integer> c = null;//空指针
List<Integer> c = new ArrayList<>(2);//不报错
Object[] a = c.toArray();

List<String> list = new ArrayList<>(2);
list.add("a");
list.add("b");
String[] array = (String[]) list.toArray();
String[] array = list.toArray(new String[0]);//推荐
  • 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
  • 实现Comparator的时候注意=问题,如果用三目或中≥这种会出现交换对象但是结果不互反的问题。
  • 利用set机制去重,而避免用List.contains()遍历去重
  • 泛型中频繁往外读取数据用<? extends T>,经常往里插入用<? super T>
  • 在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要使用含有参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key值时会抛出 IllegalStateException 异常。
    参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。
正例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(3);
pairArrayList.add(new Pair<>("version", 12.10));
pairArrayList.add(new Pair<>("version", 12.19));
pairArrayList.add(new Pair<>("version", 6.28));
Map<String, Double> map = pairArrayList.stream().collect(
// 生成的 map 集合中只有一个键值对:{version=6.28}
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
反例:
String[] departments = new String[] {"iERP", "iERP", "EIBU"};
// 抛出 IllegalStateException 异常
Map<Integer, String> map = Arrays.stream(departments).collect(Collectors.toMap(String::hashCode, str -> str));
  • 在使用 java.util.stream.Collectors 类的 toMap()方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。
    在 java.util.HashMap 的 merge 方法里会进行如下的判断:
if (value == null || remappingFunction == null)
throw new NullPointerException();
反例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 8.3));
pairArrayList.add(new Pair<>("version2", null));
Map<String, Double> map = pairArrayList.stream().collect(
// 抛出 NullPointerException 异常
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
  • ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常: java.util.RandomAccessSubList cannot be cast to java.util.ArrayList 。
    subList()返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视图,对于 SubList 的所有操作最终会反映到原列表上。
  • 使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。
    使用 entrySet 遍历 Map 类集合 KV(遍历一遍) ,而不是 keySet 方式进行遍历(遍历两倍)。如果是 JDK8,使用Map.forEach 方法。
  • Collections 类返回的对象,如: emptyList() / singletonList() 等都是 immutable list,不可对其进行添加或者删除元素的操作。
    如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
    另外,推荐使用Collection.isEmpty()代替Collection.size()检测空,因为它的性能更好并且实现的时间复杂度都是O(1) 。(检测是否为null,可以使用CollectionUtils.isEmpty()
  • 在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
  • 有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。
    如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。

并发处理

  • 获取单例对象(资源驱动类、工具类、单例工厂类)需要保证线程安全,其中的方法也要保证线程安全。
    并且要定义有意义的线程名,方便出错的时候回溯。
  • 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明:Executors 返回的线程池对象的弊端如下:
    1) FixedThreadPool 和SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
    2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
  • try-finally块回收自定义ThreadLocal变量。finally { objectThreadLocal.remove(); }
  • ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。
    这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
  • 并发修改中,如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3 次。而资金相关的金融敏感信息,使用悲观锁策略(一锁、二判、三更新、四释放)。
    总之,尽可能使加锁的代码块工作量尽可能的小,并且避免在锁代码块中调用 RPC 方法。
  • 多线程并行处理定时任务时, Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。
  • 在高并发场景中,避免使用”等于”判断作为中断或退出的条件。(说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。)
    反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。
  • 在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。
    说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
    说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出IllegalMonitorStateException 异常。
    说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。
正例:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
    doSomething();
    doOthers();
} finally {
    lock.unlock();
}

反例:
Lock lock = new XxxLock();
// ...
try {
    // 如果此处抛出异常,则直接执行 finally 代码块
    doSomething();
    // 无论加锁是否成功,finally 代码块都会执行
    lock.lock();
    doOthers();
} finally {
    lock.unlock();
}
  • 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
    Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。
正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
    try {
        doSomething();
        doOthers();
    } finally {
        lock.unlock();
    }
}
  • 使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至await 方法,直到超时才返回结果。
    其中子线程抛出异常堆栈,不能在主线程 try-catch 到。
  • 避免 Random 实例(java.util.Random 的实例或者 Math.random()的方式)被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed导致的性能下降。
    在 JDK7 之后,可以直接使用 API ThreadLocalRandom。
  • 通过双重检查锁 (double - checked locking)( 在并发场景下 ) 存在延迟初始化的优化问题隐患 ( 可参考 The " Double - Checked Locking is Broken " Declaration) ,推荐解决方案中较为简单一种 ( 适用于 JDK 5 及以上版本 ) ,将目标属性声明为 volatile 型,比如将 helper 的属性声明修改为private volatile Helper helper = null;
正例:
public class LazyInitDemo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) { helper = new Helper(); }
            }
        }
        return helper;
    }
    // other methods and fields...
}
  • volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
    如果是 count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger();count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比AtomicLong 性能更好(减少乐观锁的重试次数)。

控制语句

  • switch中变量是string的话记得先判空
  • 除常用方法(如 getXxx/isXxx )等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
  • 不要在其它表达式(尤其是条件表达式)中,插入赋值语句。
  • 方法的总行数超过10,return/throw结尾的}需要加一个空行
  • 尽量把if-else拆分成多个if-return

注释规约

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

前后端规约

  • 生产环境必须使用HTTPS,API的URL不能大写,分隔用_,最好用名词复数。body 里带参数时必须设置 Content-Type。
  • 后端返回给前端的数据如果为空,则返回空数组[]或空集合{}
    这样减少了前端很多判null/undefined的麻烦
  • errorMessage 是前后端错误追踪机制的体现,可以在前端输出到 type="hidden"文字类控件中,或者用户端的日志中,帮助我们快速地定位出问题。
    而我们一般做法都是console.log
  • 后端Long 16位到前端,即:超过 2 的 53 次方 (9007199254740992)的数值。JS会自动转换为Number类型并有丢失精度问题,如果遇到像这么大的(如,订单号、交易号)要用String
    扩展:在 Long 取值范围内,任何 2 的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有 52 位。
  • HTTP 请求通过 URL 传递参数时,不能超过 2048 字节(所有浏览器的最小值)。
  • POST其实也有长度限制,HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错。(nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以通过调大服务器端的限制。)
  • 在翻页场景中,用户输入参数的小于 1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页。
    业务中是直接隐藏了跳转框,避免让用户输入,每页固定12/15条数据。
  • 服务器返回信息必须被标记是否可以缓存,如果缓存,客户端可能会重用之前的请求结果。这样缓存有利于减少交互次数,减少交互的平均延迟。
    如http 1.1 中,s-maxage 告诉服务器进行缓存,时间单位为秒:response.setHeader("Cache-Control", "s-maxage=" + cacheSeconds);
  • 后台输送给页面的变量必须加 $!{var}
    如果 var 等于 null 或者不存在,那么${var}会直接显示在页面上。

其他

  • 避免用 Apache Beanutils 进行属性的 copy。
    Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝。
  • 注意 Math . random() 这个方法返回是 double 类型,注意取值的范围 0≤ x <1 ( 能够取到零值,注意除零异常 ) ,如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者nextLong 方法。
  • 使用String.valueOf(value)""+value 的效率更高。
  • 使用字符串String 的plit 方法时,传入的分隔字符串是正则表达式,则部分关键字(比如 .| 等)需要转义。
// String.split(String regex) 反例
String[] split = "a.ab.abc".split(".");
System.out.println(Arrays.toString(split));// 结果为[]
String[] split1 = "a|ab|abc".split("|");
System.out.println(Arrays.toString(split1));  // 结果为["a", "|", "a", "b", "|", "a", "b", "c"]




// String.split(String regex) 正例
// . 需要转译
String[] split2 = "a.ab.abc".split("\\.");
System.out.println(Arrays.toString(split2));  // 结果为["a", "ab", "abc"]
// | 需要转译
String[] split3 = "a|ab|abc".split("\\|");
System.out.println(Arrays.toString(split3));  // 结果为["a", "ab", "abc"]
  • 在工具类中屏蔽构造函数。工具类是一堆静态字段和函数的集合,其不应该被实例化;但是,Java 为每个没有明确定义构造函数的类添加了一个隐式公有构造函数,为了避免不必要的实例化,应该显式定义私有构造函数来屏蔽这个隐式公有构造函数。
public class PasswordUtils {
//工具类构造函数反例
private static final Logger LOG = LoggerFactory.getLogger(PasswordUtils.class);
public static final String DEFAULT_CRYPT_ALGO = "PBEWithMD5AndDES";
public static String encryptPassword(String aPassword) throws IOException {
    return new PasswordUtils(aPassword).encrypt();



//工具类构造函数正例
private static final Logger LOG = LoggerFactory.getLogger(PasswordUtils.class);
//定义私有构造函数来屏蔽这个隐式公有构造函数
private PasswordUtils(){}
public static final String DEFAULT_CRYPT_ALGO = "PBEWithMD5AndDES";
public static String encryptPassword(String aPassword) throws IOException {
    return new PasswordUtils(aPassword).encrypt();
}

异常日志

错误码

  • 错误码是字符串的5位(错误来源+四位数字编号),如果全部正常返回00000,错误码不体现版本号和错误等级信息并且要避免随意定义新的错误码,且错误码不能直接输出给用户看(可以发生错误信息err_msg再接一个错误码[A0001])。

异常处理

  • Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
    说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
  • 最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
    (这点在开发过程中经常遇到,比如用引入的工具类或者其他可能出现异常的方法,只要可能出现异常就必须要在业务中捕获它,并根据上下文做一些补偿策略、获取上下文的参数打出日志并抛出异常等操作。打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法)
    这样确实加了很多代码量,因为工具类、SOA、RPC调用等都是没有办法根据上下文来抛出异常,并且测试代码覆盖率时很难进入到这些异常中,但是却是对项目稳定性的最大保障。
    补充说明一点,是否抛出异常还是做其它处理需要根据业务来决定,比如业务中掉SOA接口判断是否是白名单用户,但是调用失败默认当成是返回true,而不抛出异常来确保业务剩下的内容正常执行。
  • 定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
    (这点很好的补充了上点的说明,公司中用的也是自定义的ServiceException)
  • 事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
  • finally中只做对资源对象/流对象的关闭,不要在里面return(因为它会吧try中的return吞了)。
  • 对于公司外的 http/api 开放接口必须使用 errorCode;而应用内部推荐异常抛出;跨应用间 RPC调用优先考虑使用 Result 方式,封装 isSuccess()方法、errorCode、errorMessage;而应用内部直接抛出异常即可。
    (公司的SOA服务化接口就有根据这点做出统一返回处理,不足是errorCode没有明确的规范,无法根据它来判定到底是什么错误)
  • 分层异常处理规约: 在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行 catch ,使用 catch(Exception e) 方式,并 throw new DAOException(e) ,不需要打印日志,因为日志在 Manager / Service 层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在 Service 层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。 Manager 层与 Service 同机部署,日志方式与 DAO 层处理一致,如果是单独部署,则采用与 Service 一致的处理方式。 Web 层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,尽量加上友好的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。

日志规约

  • 生产环境禁止直接使用 System.out 或 System.err 输出日志或使用e.printStackTrace()打印异常堆栈。
    因为标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
  • 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架(SLF4J、JCL--Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
    说明:日志框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推荐使用 SLF4J),所有日志至少保存15天,且当前日志应该为:/home/admin/aapserver/logs/aap.log,历史日志为:.../app.log.2020-11-16
使用 SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);

使用 JCL:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);
  • 在日志输出时,字符串变量之间的拼接使用占位符的方式。
    说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。(这点在开发过程中确实容易忽略掉)
    正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
  • 对于 trace / debug / info 级别的日志输出,必须进行日志级别的开关判断。
  • 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity = false 。
    <logger name="com.taobao.dubbo.config" additivity="false">
  • 谨慎地记录日志。生产环境中,禁止输出 debug 日志 ; 有选择地输出 info 日志 ; 如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
    说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
  • 可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
    说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。

补充说明:

  • 所有的异常都要给出对应级别的日志:
1. info:轮询开始前后,方便出错时快速定位
2. warn:补偿策略
3. error:影响到业务运行流程(业务中止)
  • 工具类中可能出现的异常,并在业务中捕获,做出一些处理。

单元测试

  • 单元测试AIR原则(Automatic自动化、Independent独立性、Repeatable可重复)。

    • 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
    • 保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
    • 单元测试是可以重复执行的,不能受到外界环境的影响。单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
      为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring 这样的 DI

框架注入一个本地(内存)实现或者 Mock 实现。

  • 单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%。
    在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。
  • 单元测试也是需要维护的。新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。

安全规约

  • 隶属于用户个人的页面或功能都需要进行权限校验,并且对手机号等敏感数据进行脱敏。
    同时要预防能被SQL注入的符号输入(如只能用_、英文、数字之类的)
  • AJAX提交必须执行CSRF安全验证。(CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。)
  • URL 外部重定向传入的目标地址必须执行白名单过滤。
  • 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。
    如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。
  • 发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。

MySQL数据库

建表规约

  • 小数类型为 decimal,禁止使用 float 和 double。
    在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。
  • varchar最大长度不能大于5000,否则需要定义成text,并独立出来一张表,用主键来对应,避免影响其它字段索引效率。
  • 必备idcreate_timeupdate_time
  • 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。(如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。)
  • 活用tinyint/smallint/int/bigint以及unsigned
    无符号值可以避免误存负数,且扩大了表示范围。

索引规约

  • 业务上具有唯一特性的字段,即使是组合字段。也必须建立唯一索引。
    (不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。)
  • 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。(索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。)
  • 索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无法排序。
  • 建组合索引的时候,区分度最高的在最左边。

SQL语句

  • 查询总条数用count(*)。count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
    count(distinct col)计算该列除 NULL 之外的不重复行数
  • 注意sum()的NPE问题:SELECT IFNULL(SUM(column), 0) FROM table;
  • isnull()来判断null,因为null与任何值的直接比较结果都为null
  • 分页时,若count=0直接返回,避免执行后面的sql
  • 不要使用物理外键,用逻辑外键。
    因为外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
  • 禁用存储过程,存储过程难以调试和扩展,更没有移植性。(这里主要针对的是MySQL,因为它对存储过程、存储函数的支持比较少)
    但是这个还是比较有争议的,可以看看这里的Mybatis批量操作
  • 尽量避免in,如果避免不了要确定子集在1000以内。

ORM映射

  • 表查询中一律不要用*作为查询的字段表,必须明确字段。(1、增加查询分析器的解析成本。2、增减字段容易与resultMap配置不一致。3、无用字段增加网络开销,尤其是text类型的字段)
    在MPmappedStatements-对应dom标签-value-sqlSource-sql下的sql也是列出所有字段
  • 及时所有类属性名与数据库字段一一对应,也必须定义<resultMap>

工程结构

应用分层

分层领域模型规约

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

通俗来讲,DO就是可以通过逆向生成的Entity、VO就是传递给web渲染的对象、DTO就是DO(Entity)转VO的中间类、Query相当于项目中Param对象,接收前端传输的对象、BO则是跟DTO类似一般很少用。

二方依赖库

  • 起始版本都是从1.0.0开始
  1. 主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。
    2) 次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。
    3) 修订号:保持完全兼容性,修复 BUG、新增次要功能特性等。

服务器

  • 高并发服务器建议调小TCP协议的time_wait(默认240s)
    调大服务器所支持的最大文件句柄数File Descriptor(默认1024)
  • JVM加参数设置-XX:+HeapDumpOnOutOfMemoryError让JVM碰到OOM场景时输出 dump 信息。出错时的堆内信息对解决问题非常有帮助。在线上生产环境, JVM 的 Xms 和 Xmx 设置一样大小的内存容量,避免在 GC 后调整堆大小带来的压力。
    公司文档中的启动脚本:java -server -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -Xmx2048M -Xms2048M -Xmn768M -XX:MaxMetaspaceSize=256M -XX:MetaspaceSize=256M -Duser.timezone=Asia/shanghai -Djava.security.egd=file:/dev/urandom -jar /data/html/${params.K8S_SERVICE_NAME}.jar --server.port=8080 --logging.path=/data/logs --server.address=0.0.0.0 --etcd.watchDir=jdemo --service.index=jdemo

附录:专有名词解释

  1. POJO( Plain Ordinary Java Object ): 在本规约中,POJO 专指只有 setter/getter/toString 的简单类,包括 DO/DTO/BO/VO 等。
  2. DO( Data Object ):阿里巴巴专指数据库表一一对应的 POJO 类。此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
  3. DTO( Data Transfer Object ):数据传输对象,Service 或 Manager 向外传输的对象。
  4. BO( Business Object ):业务对象,可以由 Service 层输出的封装业务逻辑的对象。
  5. Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用Map 类来传输。
  6. VO( View Object ):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
  7. AO( Application Object ): 阿里巴巴专指 Application Object,即在 Service 层上,极为贴近业务的复用代码。
  8. CAS( Compare And Swap ):解决多线程并行情况下使用锁造成性能损耗的一种机制,这是硬件实现的原子操作。CAS 操作包含三个操作数:内存位置、预期原值和新值。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
  9. GAV( GroupId、ArtifactId、Version ): Maven 坐标,是用来唯一标识 jar 包。
  10. OOP( Object Oriented Programming ): 本文泛指类、对象的编程处理方式。
  11. AQS( AbstractQueuedSynchronizer ): 利用先进先出队列实现的底层同步工具类,它是很多上层同步实现类的基础,比如:ReentrantLock、CountDownLatch、Semaphore 等,它们通过继承 AQS 实现其模版方法,然后将 AQS 子类作为同步组件的内部类,通常命名为 Sync。
  12. ORM( Object Relation Mapping ): 对象关系映射,对象领域模型与底层数据之间的转换,本文泛指 iBATIS, mybatis 等框架。
  13. NPE( java.lang.NullPointerException ): 空指针异常。
  14. OOM( Out Of Memory ): 源于 java.lang.OutOfMemoryError,当 JVM 没有足够的内存来为对象分配空间并且垃圾回收器也无法回收空间时,系统出现的严重状况。
  15. 一方库: 本工程内部子项目模块依赖的库(jar 包)。
  16. 二方库: 公司内部发布到中央仓库,可供公司内部其它应用依赖的库(jar 包)。
  17. 三方库: 公司之外的开源库(jar 包)。
Last modification:June 5th, 2021 at 11:29 pm
喵ฅฅ