当前位置: > > > Java - 《阿里Java开发手册》代码规范学习笔记1(编程规约)

Java - 《阿里Java开发手册》代码规范学习笔记1(编程规约)

    《Java 开发手册》是集合阿里巴巴集团技术团队的集体智慧结晶和经验总结写出来的编程规范。手册以 Java 开发者为中心视角,划分为编程规约、异常日志、单元测试、安全规约、MySQL 数据库、工程结构、设计规约七个维度。
    本文摘选的其中一些日常需要特别注意的地方。完整的手册可以在其 GitHub 主页上下载(点击打开),同时还提供了配套的配套的 Java 开发规约 IDE 插件,有兴趣的小伙伴可以一起下载。

一、 编程规约

1,包名统一使用小写

(1)包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
下面是一个简单的样例:
  • 应用工具类包名为 com.alibaba.ei.kunlun.aap.util
  • 类名为 MessageUtils(此规则参考 spring 的框架结构)

(2)如果包名实在没办法有且仅有一个自然语义的英语单词,那么也必须全小写:

2,接口类中的方法和属性不要加任何修饰符号

(1)接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。
(2)尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。
正例:
  • 接口方法签名 void commit();
  • 接口基础常量 String COMPANY = "alibaba";
反例:
  • 接口方法定义 public abstract void f();
说明
  • JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。

3,接口和实现类的命名规则

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

(2)如果是形容能力的接口名称,取对应的形容词为接口名(通常是 -able 的形容词)。
接口Translatable
实现类AbstractTranslator

4,枚举

枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
提示:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。
// 枚举样例1
public enum ProcessStatusEnum {
    SUCCESS, PARAM_EMPTY, UNKNOWN_REASON
}

// 枚举样例2
public enum ProcessStatusEnum {
    SUCCESS("0000", "成功"),
    PARAM_EMPTY("1001", "必选参数为空"),
    UNKNOWN_REASON("9999", "未知原因成功");

    private String code;
    private String message;

    ProcessStatusEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return this.code;
    }
    
    public String getMessage() {
        return this.message;
    }
}

5,各层命名规约

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

6,变量、常量定义

(1)在 long 或者 Long 赋值时,数值后使用大写字母 L,不能是小写字母 l,小写容易跟数字混淆,造成误解。
正例Long a = 2L
反例Long a = 2l

(2)不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
说明
  • 大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解,也不利于维护。
正例
  • 缓存相关常量放在类 CacheConsts
  • 系统配置相关常量放在类 SystemConfigConsts 下。

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

(4)如果变量值仅在一个固定范围内变化用 enum 类型来定义。
  • 如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。
public enum SeasonEnum {
    SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);

    private int seq;

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

    public int getSeq() {
        return seq;
    }
}

7,采用 4 个空格缩进

(1)禁止使用 Tab 字符缩进,宜采用 4 个空格缩进,原因如下:
  • 在不同的编辑器里 Tab 的长度可能会不一致,这会导致有 Tab 的代码,用不同的编辑器打开时,格式可能会乱。4 个空格在不同编辑器下宽度看起来是一致的。
  • 代码压缩时,空格会有更好的压缩率。这里面是信息量的问题,使用了 Tab 的代码,仍然会有空格,比如代码注释、运算符之间的间隔等等,但使用了空格的代码,是可以没有 Tab 的。Tab 也是一个字符,这就决定了,用 Tab 的代码虽然不压缩的时候更小,但熵更高,因此压缩率会较差,压缩之后反而更大。
(2)如果使用 Tab 缩进,必须设置 1 Tab 4 个空格:
  • IDEA 设置 Tab 4 个空格时,请勿勾选 Use tab character
  • Eclipse 中,必须勾选 insert spaces for tabs

8,所有的覆写方法,必须加 @Override 注解。

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

9,equals 比较

(1)Object equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals
正例"test".equals(object);
反例object.equals("test");

(2)所有整型包装类对象之间值的比较(其他包装类对象也一样),全部使用 equals 方法比较。
说明:
  • 对于 Integer var = ?-128 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生, 会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断。
  • 但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

10,浮点数之间的等值判断

    浮点数之间的等值判断,不能直接使用 == 或者 equals 来判断。因为浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。
(1)要进行两个浮点数的等值判断,一种方式是指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的:
float a = 1.0F - 0.9F;
float b = 0.9F - 0.8F;
float diff = 1e-6F;

if (Math.abs(a - b) < diff) {
    System.out.println("true");
}

(2)另一种方式是使用 BigDecimal 来定义值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

if (x.compareTo(y) == 0) {
    System.out.println("true");
}

11,BigDecimal 规范

(1)禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。
BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
  • 比如:BigDecimal g = new BigDecimal(0.1F); 实际的存储值为:0.10000000149
(2)优先推荐入参为 String 的构造方法,或使用 BigDecimal valueOf 方法,此方法内部其实执行了 Double toString,而 Double toString double 的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);

12,基本数据类型与包装数据类型的使用标准

(1)所有的 POJO 类属性必须使用包装数据类型。
POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
  • 比如:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
(2)RPC 方法的返回值和参数必须使用包装数据类型。
    某业务的交易报表上显示成交总额涨跌情况,即正负 x%x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线 -。所以包装数据类型 的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
(3)所有的局部变量使用基本数据类型。

13,POJO 类必须写 toString 方法

    POJO 类写 toString 方法的好处是:在方法执行抛出异常时,可以直接调用 POJO toString() 方法打印其属性值,便于排查问题。
注意:使用 IDE 中的工具生成 toString 方法时(source> generate toString),如果继承了另一个 POJO 类,注意在前面加一下 super.toString

14,循环体内使用 append 方法进行字符串连接

(1)循环体内,字符串的连接方式,使用 StringBuilder append 方法进行扩展。
(2)下面是一个反例,其反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
// 这是一个错误示范
String str = "start";
for (int i = 0; i < 100; i++) {
    str = str + "hello"; 
}

15,使用 final 关键字的情况

final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字:
  • 不允许被继承的类,如:String 类。
  • 不允许修改引用的域对象,如:POJO 类的域变量。
  • 不允许被覆写的方法,如:POJO 类的 setter 方法。
  • 不允许运行过程中重新赋值的局部变量。
  • 避免上下文重复使用一个变量,使用 final 关键字可以强制重新定义一个变量,方便更好地进行重构。

16,类成员与方法访问控制从严

说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。
  • 比如:如果是一个 private 的方法,想删除就删除,可是一个 public service 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。
(1)如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private
(2)工具类不允许有 public default 构造方法。
(3)类非 static 成员变量并且与子类共享,必须是 protected
(4)类非 static 成员变量并且仅在本类使用,必须是 private
(5)类 static 成员变量如果仅在本类使用,必须是 private
(6)若是 static 成员变量,考虑是否为 final
(7)类成员方法只供类内部调用,必须是 private
(8)类成员方法只对继承类公开,那么限制为 protected

17,hashCode 和 equals 的处理规则

(1)只要覆写 equals,就必须覆写 hashCode
说明:因为 Set 存储的是不重复的对象,依据 hashCode equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。

(2)如果自定义对象作为 Map 的键,那么必须覆写 hashCode equals
说明String 因为覆写了 hashCode equals 方法,所以可以愉快地将 String 对象作为 key 来使用。

18,集合转数组规范

(1)使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
说明:使用 toArray 带参方法,数组空间大小的 length
  • 等于 0,动态创建与 size 相同的数组,性能最好。
  • 大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
  • 等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。
  • 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。
List<String> list = new ArrayList<>(2); 
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);

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

19,数组转集合规范

(1)使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
说明asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。

(2)下面是一个简单样例:
String[] str = new String[] { "chen", "yang", "hao" };
List list = Arrays.asList(str); 

list.add("yangguanbao");  // 会出现运行时异常

str[0] = "change";  // list里对应的值也会随之修改,反之亦然。

20,<? extends T> 与 <? super T>

(1)泛型通配符 <? extends T> <? super T> 在接口调用赋值的场景中容易出错,它们区别是:
  • 使用 <? extends T> 写法的泛型集合不能使用 add 方法
  • 而使用 <? super T> 写法的泛型集合不能使用 get 方法

(2)根据 PECSProducer Extends Consumer Super)原则:
  • 频繁往外读取内容的,适合用 <? extends T>
  • 经常往里插入的,适合用 <? super T>

21,不要在 foreach 循环里进行元素的 remove/add 操作

    不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
//正确的做法
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();
    }
}

//错误的做法
for (String item: list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}

22,Comparator 实现类规范

(1)在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sortCollections.sort 会抛 IllegalArgumentException 异常:
  • xy 的比较结果和 yx 的比较结果相反。
  • x>yy>z,则 x>z
  • x=y,则 xz 比较结果和 yz 比较结果相同。

(2)下例中没有处理相等的情况,交换两个对象判断结果并不互反,不符合第一个条件,在实际使用中可能会出现异常。
new Comparator < Student > () {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getId() > o2.getId() ? 1 : -1;
    }
};

23,集合泛型定义规范

(1)集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略。
菱形泛型:即 diamond,直接使用 <> 来指代前边已经指定的类型。
// diamond 方式,即<>
HashMap<String, String> userCache = new HashMap<>(16); 

// 全省略方式
ArrayList<User> users = new ArrayList(10);

(2)集合初始化时,指定集合初始值大小。
  • 说明HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。
  • 正例initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
  • 反例HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,resize() 方法总共会调用 8 次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。

24,遍历 Map 类集合 KV

注意values() 返回的是 V 值集合,是一个 list 集合对象;keySet() 返回的是 K 值集合,是一个 Set 集合对象;entrySet() 返回的是 K-V 值组合集合。
(1)使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历:
  • keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value
  • entrySet 只是遍历了一次就把 key value 都放到了 entry 中,效率更高。
Set<Map.Entry<String, String>> entryseSet=map.entrySet();
for (Map.Entry<String, String> entry:entryseSet) {
    System.out.println(entry.getKey()+","+entry.getValue());
}

(2)如果是 JDK8,使用 Map.forEach 方法。
infoMap.forEach((key, value) -> {
    System.out.println(key + ":" + value);
});

25,Map 类集合 K/V 能不能存储 null 值的情况

注意:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE 异常。
集合类 Key Value Super 说明
Hashtable 不允许为null 不允许为null Dictionary 线程安全
ConcurrentHashMap 不允许为null 不允许为null AbstractMap 锁分段技术(JDK8:CAS)
TreeMap 不允许为null 允许为null AbstractMap 线程不安全
HashMap 允许为null 允许为null AbstractMap 线程不安全

26,使用 Set 去重

利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List contains() 进行遍历去重或者判断包含操作。

27,创建线程或线程池时指定有意义的名称

创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
  • 比如自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给 whatFeatureOfGroup
public class UserThreadFactory implements ThreadFactory {
	private final String namePrefix;
	private final AtomicInteger nextId = new AtomicInteger(1);

	// 定义线程组名称,在利用 jstack 来排查问题时,非常有帮助
	UserThreadFactory(String whatFeatureOfGroup) {
		namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
	}

	@Override
	public Thread newThread(Runnable task) {
		String name = namePrefix + nextId.getAndIncrement();
		Thread thread = new Thread(null, task, name, 0);
		System.out.println(thread.getName());
		return thread;
	}
}

28,线程资源必须通过线程池提供

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

29,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式

这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下:
  • FixedThreadPool SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
  • CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

30,SimpleDateFormat 使用规范

(1)SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。或者如下处理,确保线程安全:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};

(2)如果是 JDK8 的应用,可以使用 Instant 代替 DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat

31,必须回收自定义的 ThreadLocal 变量

    必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用, 如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用 try-finally 块进行回收。
objectThreadLocal.set(userInfo);
try {
    // ...
} finally {
    objectThreadLocal.remove();
}

32,锁的使用规范

    高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

33,并发修改同一记录时需要加锁

并发修改同一记录时,避免更新丢失,需要加锁:
  • 要么在应用层加锁
  • 要么在缓存加锁
  • 要么在数据库层使用乐观锁,使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。

34,Timer&TimerTask 与 ScheduledExecutorService 比较

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

35,避免 Random 实例被多线程使用

(1)避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。
说明Random 实例包括 java.util.Random 的实例或者 Math.random() 的方式。
(2)在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个单独的 Random 实例。

36,多线程统计次数问题(count++)

(1)count++ 不是原子操作,也就是说是线程不安全的。多线程下我们可以使用 AtomicIntegerAtomicLong 来实现技术:
AtomicInteger count = new AtomicInteger();
count.addAndGet(1);

(2)如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)
LongAdder count = new LongAdder();
count.increment(); // count.add(1L);

37,ThreadLocal 对象使用 static 修饰

    这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

38,避免用 Apache Beanutils 进行属性的 copy

Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtilsCglib BeanCopier
注意:它们均是浅拷贝。

39,获取整数类型的随机数

(1)Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够取到零值,注意除零异常)。
(2)如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。
Random random = new Random();
System.out.println(random.nextInt(4)); // 获取 0~3 范围内(包括 0 和 3 )的 int 类型的随机数

40,获取当前毫秒数

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

41,日期格式化表达式的年份统一使用小写 y

    日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which yearJDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年。
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

42,日志输出时,字符串变量间使用占位符方式拼接

    因为 String 字符串的拼接会使用 StringBuilder append() 方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);

43,对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断

    虽然在 debug(参数) 的方法体内第一行代码 isDisabled(Level.DEBUG_INT) 为真时(Slf4j 的常见实现 Log4j Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName()) 这种参数内有 getName() 方法调用,无谓浪费方法调用的开销。
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
    logger.debug("Current ID is: {} and name is: {}", id, getName());
}

44,生产环境日志、异常输出规范

生产环境禁止直接使用 System.out System.err 输出日志或使用 e.printStackTrace() 打印异常堆栈。
说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制

附:工程结构

1,分层领域模型规约

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

2,二方库版本号命名规范

二方库版本号命名方式:主版本号.次版本号.修订号
  • 主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。
  • 次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。
  • 修订号:保持完全兼容性,修复 BUG、新增次要功能特性等。
注意:起始版本号必须为 1.0.0,而不是 0.0.1

3,禁止在子项目的 pom 依赖中出现相同的 GroupId 和 ArtifactId,但是不同的 Version

    在本地调试时会使用各子项目指定的版本号,但是合并成一个 war,只能有一个版本号出现在最后的 lib 目录中。曾经出现过线下调试是正确的,发布到线上却出故障的先例。

4,高并发服务器建议调小 TCP 协议的 time_wait 超时时间

    操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服务器端会因为处于 time_wait 的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值。
linux 服务器上请通过变更 /etc/sysctl.conf 文件去修改该缺省值(秒):
  • net.ipv4.tcp_fin_timeout = 30

5,调大服务器所支持的最大文件句柄数

(1)主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd(FileDescriptor)
(2)主流的 linux 服务器默认所支持最大 fd 数量为 1024,当并发连接数很大时很容易因为 fd 不足而出现“open too many files”错误,导致新的连接无法建立。
(3)建议将 linux 服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)。

6,让 JVM 碰到 OOM 场景时输出 dump 信息

JVM 环境参数设置 -XX:+HeapDumpOnOutOfMemoryError 参数,让 JVM 碰到 OOM 场景时输出 dump 信息。
说明OOM 的发生是有概率的,甚至相隔数月才出现一例,出错时的堆内信息对解决问题非常有帮助。

7,将 JVM 的 Xms 和 Xmx 设置成一样大小

在线上生产环境,JVM XmsXmx 设置一样大小的内存容量,避免在 GC 后调整堆大小带来的压力。
评论0