双11买了本Java开发手册,通读了一遍,学习了较多规范问题,这些规范自己慢慢经历可能需要较长一段时间。
这里记下笔记,以后随时也有的学习。
序
一致性很重要,无边无际争论的时间成本与最后的收益是成反比的。
- 缩进使用四个空格,不使用tab
- if单语句必须加大括号,换行
- 左大括号不换行
编程规约
以下编程规约
命名风格
强制部分
- 代码中的命名均不能以下画线或美元符号开始,也不能以下画线或美元符号结束
- 代码命名禁止使用拼音与英文混合的方式,更不能直接使用中文方式
- 类名使用UpperCamelCase风格,但DO/BO/DTO/VO/AO/PO等情形例外
- 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格,必须遵从驼峰形式
- 常量名全部大写,单词用下画线隔开,力求语义表达完整,不要嫌名字长
- 抽象类名使用Abstract或Base开头;异常类名使用Exception结尾;测试类名以它要测试类名开始,以Test结尾
- 类型与中括号之间无空格相连定义数组。例如
String[] args
- POJO类中布尔类型的变量都不要假is前缀,否则部分框架解析会引起序列化错误。即
deleted
而不是isDeleted
- 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一用单数,但类名如果有复数含义,则用复数形式,例如
MessageUtils
- 杜绝完全不规范的缩写,避免词不达意,例如
AbstractClass
写成AbsClass
,而condition
缩写成condi
推荐部分 - 为了达到代码,任何自定义变量命名时,使用尽量完整单词来表达其意,不要随意使用
int a
等随意方式 - 如果模块、接口、类、方法使用了设计模式,应在命名时体现出具体模式,例如
public class OrderFactory
以及public class LoginProxy
、public class ResourceObserver
等。 - 接口类中的方法和属性不要加任何修饰符号(public也不要加),保持代码简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义,必须是与接口方法相关的,并且是整个应用的基础厂常量。
例如
正例
void commit();
String COMPANY = "alibaba";
反例
public abstract void commit();
- 接口和实现类命名规则:
- 如果是Service和DAO类,基于SOA理念,暴露出来的服务一定是接口,内部实现类用Impl后缀与接口区别,例如
CacheServiceImpl
实现CacheService
接口 - 如果是形容能力的接口名称,取对应的形容词为接口名(通常是-able形式)。例如
AbstractTranslator
实现Translatable
。
- 枚举类名建议带上Enum后缀,枚举成员需要全大写,单词间用下画线隔开。枚举其实就是特殊的常量类,切构造方法被默认强制为私有。
- 各层命名规约:
- Service/DAO层方法命名规约如下:
- 获取 单个对象的方法用get作为前缀
- 获取多个对象方法用list作为前缀
- 获取统计值方法用count作为前缀
- 插入方法用save/insert作为前缀
- 删除的方法用remove/delete作为前缀
- 修改的方法用update作为前缀
- 领域模型命名规约如下:
- 数据对象:xxxDO,xxx为数据表名
- 数据传输对象:xxxDTO,xxx为业务领域相关名称
- 展示对象:xxxVO,xxx一般为网页名称
- POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO
常量定义
强制
- 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中,例如
String key = "Id#taobao_"+tradeId; //此处Id#taobao来的好随意
cache.put(key, value);
-
long或者Long初始赋值时,使用大写的L,不能使用小写的l。小写的l容易跟数字1混淆。
推荐
- 不要使用一个常量类维护所有常量,要按照常量功能分类,分开维护,例如缓存
CacheConsts
而配置ConfigConsts
- 常量服用层有5层:
- 跨应用共享常量:放在二方库中,通常在
client.jar
中的constant
目录下 - 应用内共享常量:纺织在一方库中,通常在子模块中的
constant
目录下 - 子工程内部共享常量:即在当前子工程的
constant
目录下 - 包内共享常量:即在当前包下单独的
constant
目录下 - 类内共享常量:直接在类内部
private static final
定义
- 如果变量值仅在一个范围内变化,则用enum类型来定义。
public enum SeasonEnum{
SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
int seq;
SeasonEnum(int seq){
this.seq = seq;
}
}
代码格式
强制
- 大括号使用约定,如果大括号内为空,则简洁的写成{} 即可,不需要换行
- 左小括号和字符之间不能出现空格,右小括号和字符之间也不能出现空格。
- if /for /while /switch /do 等保留子与括号之间必须加空格
- 任何二目、三目运算符的左右两边都需要加一个空格,例如
=
、&&
、加减乘除等 - 采用四个空格缩进,禁止使用Tab控制符
- 注释的双鞋线与注释内容之间有且仅有一个空格
- 单行字符数不超过120个,超出则需要换行
- 第二行相对于第一行缩进四个空格,从第三行开始,不再持续缩进
- 运算符与下文一起换行
- 方法调用的点符号与下文一起换行
- 方法调用中多个参数需要换行时,在逗号后进行
- 在括号前不要换行
- 方法参数在定义和传入时,多个参数逗号后必须加空格
- IDE的text file encoding设置为UTF-8,IDE中文件换行符使用UNIX格式,不要使用Windows格式
- 没必要增加若干空格来使某一行的字符和上一行对应位置的字符对齐
- 不同逻辑、不同语义、不同业务代码之间插入一个空行分割开,以提升可读性,没必要插入多行
OOP规约
- 通过避免一个类的对象引用访问此类的静态变量或静态方法,造成无谓编译器解析成本,直接用类名访问即可
- 所有覆写方法,必须加@Override注解
- 相同参数类型,相同业务含义,才可以使用可变参数,避免使用Object
- 对外部正在调用或者二方库依赖的接口,不允许修改方法签名,以避免对接口调用方产生影响。若接口过时,必须加@Deprecated注解,并清晰说明采用的新接口或者新服务是什么。
- 不能使用过时的类或方法
- Object的equals方法容易抛出空指针异常,应该使用常量或者有值对象来调用equals
"test".equals(object) // 推荐
object.equals("test") // 不推荐,反例
- 所有相同的包装类对象之间值的比较,全部使用equals方法
- 关于基本类型与包装类使用标准:
- 所有的POJO类属性必须使用包装数据类型(数据库查出null,基本类型会有接受NPE问题)
- RPC方法调用返回值和参数必须使用包装类型数据
- 局部变量使用基本数据类型
- 在DO/DTO/VO等POJO类时,不要设定任何属性默认值
- 当序列化类新增属性时,请不要修改
serialVersionUID
属性,以避免反序列化失败;如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID
值 - 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在
init
方法中 - POJO类必须写
toString
方法,如果继承了另一个POJO类,注意在前面加一下super.toString()
推荐 - 当使用索引访问
String
的split
方法得到的数组时,需要在最后一个分隔符后做有无内容的检查,否则会有抛出IndexOutOfBoundsException
的风险。 - 当一个类有多个构造方法,或者多个同名方法时,这些方法应该按顺序放置在一起,便于阅读
- 类内定义方法的顺序:公有方法或保护方法>私有方法>getter/setter方法
- 在setter方法中,参数名称与类成员变量名称一致,使用this赋值。在getter/setter方法中,不要增加业务逻辑,否则会增加排查问题难度
- 在循环体内,字符串的连接方式使用
StringBuilder
的append
方法进行扩展。
以下方式中,在反编译出的字节码文件显示每次循环都会new出一个StringBuilder对象,然后进行append操作,最后通过toString返回,造成资源浪费
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
- final可以声明类、成员变量 、方法及本地变量,下列情况使用final关键字:
- 不允许被继承的类,例如String
- 不允许被修改的引用的域对象,比如POJO的域对象
- 不允许被重写的方法,例如POJO的setter方法
- 不允许运行过程中重新赋值的变量
- 避免上下文重复使用一个变量,使用final描述可以强制重新定义一个变量,方便更好的重构
- 慎用Object的clone方法进行拷贝
关于深拷贝浅拷贝可以看原型模式–自我复制(结合Java浅复制与深复制) - 类方法与成员控制从严:
- 如果不允许外部直接通过new来创建对象,那么构造方法必须为private
- 工具类不允许有public或default构造方法
- 类非static成员变量并且与子类共享,必须限制为protected
- 成员变量(或static成员变量)如果仅为本类使用,必须限制为private
- 只对内部调用,必须限制为private,类成员方法只对继承类公开,限制为protected
对任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解藕。思考,如果是一个private的方法,想删除就删除;可如果是一个public的service方法,或者一个public的成员变量,删除时手心不得冒点汗吗?变量像自己的小孩,应尽量让它在自己的视线范围内。作用于太大,如果任其无限制到处跑,你会担心的。
集合处理
- 关于hashCode和equals的处理,遵循:
- 只要重写equals,必须重写hashCode
- 因为Set存储的是不重复对象,依据hashCode和equals进行判断,所以Set存储对象必须重写这两种方法
- 如果自定义对象作为Map的键,那么必须重写hashCode和equals方法
- ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常。
subList返回的是ArrayList的内部类SubList,并不是ArrayList,而是ArrayList的一个视图,对于SubList子列表的所有操作,都最终会反映到原列表上 - 在subList场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生
ConcurrentModificationException
异常 - 使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入类型完全一样的数组,大小就是list.size()。
当使用toArray带参方法,入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标[list.size()]的数组元素将置为null,其他则保持原值,因此最好初啊入与集合大小一致的数组。 - 在使用Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出
UnsupportedOperationException
。
asList
的返回对象是一个Arrays内部类,并没有实现集合的修改方法,体现的是适配器模式,只是转化接口,后台仍是数组。 - 泛型通配符
<? extends T>
用来接受返回的数据,此写法不能使用add方法,而<? super T>
不能使用get方法,因为作为其接口赋值时,容易出错。
根据PECS(Producer Extends Consumer Super)原则:第一,频繁网外读取内容的,适<? extends T>
;第二,经常往里插入,适用<? super T>
- 不要在foreach循环里使用remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁
- 在JDK7即以上版本中,Comparator要满足以下三个条件,否则
Array.sort,Collections.sort
会报IllegalArgumentException
- x, y的比较和y,x的比较结果相反
- x>y,y>z,则x>z
- x=y,则x,z比较结果和y,z比较结果相同
**推荐
- 在集合初始化时,指定集合初始值大小
- 使用entrySet遍历Map类集合K/V,而不是用keySet方式遍历。
keySet其实遍历2次,一次转为Iterator对象,另一次是从hashMap中去除key所对应的value,如果是JDK8,则使用Map.foreach方法。 - 高度注意Map类集合K/V能不能存储null值的情况
集合类 | Key | Value | Super | 说明 |
---|---|---|---|---|
HashTable | 不允许为null | 不允许为null | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为null | 不允许为null | AbstractMap | 锁分段,Java8使用CAS |
TreeMap | 不允许为null | 允许为null | AbstractMap | 线程不安全 |
HashTable | 允许为null | 允许为null | Dictionary | 线程不安全 |
- 合理利用集合的有序性(sort)和稳定性(order),避免集合的无序和不稳定性带来影响。
- ArrayList是order/susort
- HashMap是unorder/unsort
- TreeSet是order/sort
- 利用Set元素的唯一特性,可以快速对一个集合进行去重操作,避免使用List的contains方法进行遍历,对比去重操作。
并发处理
- 获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
资源驱动类,工具类,单例工厂类都需要注意 - 在创建线程或线程池时,请指定有意义名称,方便出错时回溯
- 线程资源必须通过线程池提供,不允许在应用中自行显示创建线程
在使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题 - 线程池不允许使用Executors创建,而是通过
ThreadPoolExecutor
的方式创建,这样的处理方式能让编写代码的工程师更加明确线程池的运行规则,避免资源耗尽的风险。
Executors返回的线程池的对象弊端如下:
- FixedThreadPool和SingleThreadPool,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM。
- CachedThreadPool和ScheduledThreadPool;,允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
- SimpleDateFormat是不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。
如果是JAVA8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat。 - 在高并发场景中,同步调用应该去考虑锁的性能损耗。能用无锁数据结构,就不要用锁,能锁区块,就不要锁整个方法体,能用对象锁,就不要用类锁
- 在对多个资源,数据库表,对象同时加锁时,需要保持一致的加锁顺序,否则肯呢个会造成死锁。
- 在并发修改同一记录时,为避免更新丢失,需要加锁。要么在应用层加索,要么在缓存层加锁,要么在数据库层使用乐观锁,使用version作为更新标志。
如果每次访问,冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁,乐观锁的重试次数不得小于3次。 - 对于多线程并行处理定时任务的情况,在Timer运行多个TimeTask时,只要其中之一没有捕获抛出异常,其他任务便会自动终止运行。如果在处理定时任务时,使用
ScheduledExecutorService
,则没有这个问题。 - 使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行,避免主线程无法执行至await方法,知道超时才返回结果。
子线程抛出异常堆栈,不能在主线程try-catch到 - 避免Random实例被多线程使用,虽然该共享实例是线程安全的,但会因竞争同一seed导致性能下降。
此处的Random包括java.util.Random
和Match.random()
方式,在JDK7后,可以直接使用API ThreadLocalRandom。 - 在并发场景下,通过双重检查锁(double-checked-locking)实现延迟初始化的优化问题隐患,推荐解决中较为简单一种,即目标属性声明为volatile型。
- volatile解决多线程内存不可见问题,对于一写多读,可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题,例如count++。推荐使用AtomicInteger(Atomic*)实现,如果是JDK8,推荐使用LongAdder对象,他比AtomicLong性能更好(减少乐观锁的重试次数)
- HashMap在容量不够进行resize时,由于高并发可能出现死链,导致cpu占用飙升,在开发过程中可以使用其他数据结构或加锁来规避此风险
- ThreadLocal无法解决共享对象的更新问题,ThreadLocal对象建议使用static修饰,这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有实例共享此静态变量,也就是说,在类第一次被使用时,只分配一块存储空间,所此类的对象(只要是这个线程内定义的)都可以操作这个变量。
控制语句
- 在一个switch块内,每个case要么通过break/return重要,要么注释说明程序讲继续执行到哪一个case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码也没有。
- 在if/else/for/while/do语句中,必须使用大括号,即使只有一行代码,也应避免采用单行的编码方式:
if (condition) statements
- 在高并发场景中,避免使用“等于”判断作为中断或者退出的条件。
如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,应使用大于或小于的区间判断条件来代替 - 在表达异常的分支时,尽量少使用
if - else
方式,这种方式可以改写成:
if (condition) {
return;
}
- 除常用方法(如getXxx/ isXxx)外,不要在条件判断中执行其他复杂语句,可以讲复杂的逻辑变量赋值给一个有意义的布尔变量名,以提高可读性。
例如:
final boolean existed = (file.open(name, "w") != null && (...) || (...));
if (existed) {
...
}
- 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象或性能,获取数据库连接,避免进行不必要的try-catch操作
- 避免采用去翻逻辑运算符
- 接口入参保护,这种场景常见的用作批量操作的接口。
- 下列情形,需要进行参数校验:
- 调用频次低的方法。
- 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。
- 需要极高稳定性和可用性的方法
- 对外提供的开放接口,不管是否为RPC/API/HTTP接口。
- 敏感权限入口。
- 下列情形,不需要进行参数校验
- 极有可能被循环调用的方法,但是方法说明里必须著名外部参数检查要求
- 底层调用频度比较高的方法,毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才暴露问题,一般DAO层与Service层在同一应用中,并部署在同一台服务器中,所以DAO的参数校验可以忽略
- 被声明成private只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,可以不校验参数。
注释规约
- 类、类苏醒,类方法的注释必须用Javadoc规范,使用
/**内从*/
,不得使用// xxx方式 - 所有抽象方法(包括接口中的方法)必须要用Javadoc注释,除了返回值,方法,异常说明外,还必须指出该方法做什么事,实现什么功能
- 所有类都必须添加创建者和创建日期
- 方法内部的单行注释,在被注释语句上方另起一行,使用
//
注释,方法内部的多行注释,使用/**/
注释,对齐 - 所有枚举类型字段必须有解释,说明每个数据项的用途。
- 与其使用“半吊子”英文来注释,不如使用中文注释把问题说清楚。专业名词与关键字保持英文
- 在修改代码的同时,要对注释进行相应修改,尤其是参数,返回值,异常,和性逻辑等修改。
- 谨慎注释掉代码,要在上方详细说明,而不是简单的注释掉,如果无用,请删除!
代码注释有两种可能:
- 后续会恢复此段代码逻辑
- 永久不用
- 对于注释代码的要求:
- 能够准确反映设计思想和代码逻辑
- 能够描述业务含义,是其他程序员能够迅速了解代码背后的信息
- 好的命名、代码结构是自解释的,注释力求精简准确
- 特殊注释标记,请注明标记人与标记事件。注意及时处理这些标记,通过标记扫描经常清理此类标记。有时候线上故障就来源于这些标记处的代码:
- 代办事宜 (TODO):(标记人,标记时间,[预计处理时间])表示需要实现,但是目前实现的功能。这些实际上是一个Javadoc的标签,虽然目前的Javadoc还没有实现,但已经广泛使用,且只能应用于类、接口和方法(因为它还是一个Javadoc)标签
- 错误,不能工作(FIXME):(标记人,标记事件,[预计处理事件])在注释中使用FIXME标记某代码是错误的,而且不能工作,需要及时纠正。
其他
- 在使用正则表达式时,利用好其预编译功能呢个,可以有效加快正则匹配速度。
不要在方法体内定义:
Pattern pattern = Pattern.compile(规则);
- 在velocity调用POJO类的属性时,建议直接使用属性名取值,模板引擎会自动按规范调用POJO的getXxx(),如果是boolean基本数据类型(boolean命名不需要加is前缀),会自动调用siXxx()方法。
- 后台输送给页面的变量必须加
$!{var}
–中间是感叹号。
如果var=null或者不存在,那么${var}
会直接显示也页面上 - 注意Math.random()这个方法返回的是double类型,取值的范围0<=x<1(能够取到零值,注意零异常),如果想获取整数类型的随机数,不要讲x放大10的若干倍然后取整,直接使用Random对象的nextInt或者nextLong方法。
- 获取当前毫秒数用
System.currentTimeMillis()
;而不是用new Date().getTime();
如果想获取更加精确的纳秒级时间值,则应使用System.nanoTime()的方式,在JDK8中,正对统计时间等场景,推荐使用Instant
类 - 不要在视图模板中加入任何的复杂的逻辑
- 任何数据结构的构造或者初始化,都应该指定大小,避免因数据结构无限增长而耗尽内存
- 及时清理不再使用的代码段或配置信息。
对于暂时被注释掉,后续可能恢复的代码段,在注释代码上方,统一规定使用三个斜杠(///)来说明被注释掉的理由
异常日志
异常处理是大部分程序员积攒多年的痛点。
异常处理
- Java类库中定义的可以通过预检查方式规避的
RuntimeException
不应该通过catch方式来处理,例如IndexOutOfBoundsExecption
,NullPointerException
等
无法通过预检查的异常不在此列,例如NumberFormatException
- 异常不要用来做流程控制,条件控制,例如通过捕获数据库抛出唯一值冲突,来判断值是否唯一
- catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何都uhui错误的代码,对于非稳定代码的catch,尽可能在异常类型的区分后,再作出对应的异常处理。
- 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃它,如果不像处理它,请将该异常跑给它的调用者。最外层的业务代码使用者必须处理异常,将其转化为用户可以理解的内容。
- 有try块放到了食物代码中,catch异常后,如果需要回滚事务,一定要注意使用rollback事物。
- finally块必须对资源对象,流对象进行关闭操作,如果有异常也要做try-catch操作。
对于JDK7以上版本,可以使用try-with-resources
方式 - 不能在finally块中使用return
- 捕获异常与抛异常必须完全匹配,或者捕获异常是抛异常的父类。
- 方法的返回值可以为null,不强制返回空集合或者空对象,必须添加注释充分说明在什么情况下会返回null值,调用方需要进行null判断以防止NPE操作。
- 防止NPE,是程序员的基本修养,注意NPE产生的场景:
- 当返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE
public int add () {
return Integer(xx); // 如果为null,自动拆箱NPE
}
- 数据库查询结果可能为null
- 集合里的元素即使isNotEmpty,取出元素也可能为null。
- 远程调用返回对象时,一律要求进行空指针判断,以防止NPE
- 对与Session中获取的数据,建议进行NPE检查,以免避免空指针
- 级联调用
obj.getA().getB().getC();
的一串调用,易产生NPE
- 定义时区unchecked/checked异常,避免直接抛出
new RuntimeException()
,更不允许抛出Exception或者Throwable,应使用有业务定义的异常,推荐业界已定义过的或者自定义的异常,如DAOException/ ServiceException
等 - 对于公司外的HTTP/API开放接口必须使用“错误码”;应用内部推荐异常抛出;跨应用RPC调用优先考虑使用Result方式,封装isSuccess()方法、错误码、错误简短信息等。
- 避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。
日志规约
- 应用中不可直接使用日志系统中的API(Log4j,Logback),而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个日志的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
- 日志文件推荐至少保存15天,因为有些异常具备以周为频次发生的特点。
- 应用中的扩展日志(如打点、临时监控、访问日志)等命名方式:
appName_logType_logName.log
。
logType为日志类型,推荐分类有status/monitor/visit等;
logName为日志描述:通过文件名就知道日志属于哪个应用,那种类型,有什么目的,这也有利于归类查找 - 对
trace/debug/info
级别的日志,必须使用条件输出形式或者占位符的方式
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
如果日志级别是warn,上述日志不会打印,但会执行字符串拼接操作,如果symbol是对象,会执行toString方法,浪费了系统资源,但日志却没有被打印。
logger.debug("Processing trade with id: {} and symbol : {}", id, symbol);
- 避免重复打印日志,否则会浪费磁盘空间。务必设置additivity=false;
- 异常信息应该包括两类:案发现场信息和异常堆栈信息,如果不处理,那么通过throws往上抛出
- 谨慎记录日志。生产环境禁止输出deug日志;
有选择的输出info日志;
如果使用warn记录刚上线的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并及时删除这些观察日志 - 可以使用warn日志级别记录用户输入参数错误情况,避免用户投诉时无所适从。
如非必要,请不要在此场景中打出error级别,注意日志输出的级别,error级别只记录系统逻辑出错,异常等重要错误信息。
单元测试
本章首次提出AIR原则和BCDE原则来进行相应的衡量。
- 好的单元测试应该遵守AIR原则:
- Automatic(自动化)
- Independent(独立性)
- Repeatable(可重复)
- 单元测试应该是全自动执行的,并且是非交互式的。测试用例通常被定期执行,执行过程必须完全自动化才有意义。需要人工检查输出结果的测试不是一个好的单元测试。单元测试中不准使用System.out进行人肉验证,必须使用assert验证
- 保持单元测试的独立性,为了保证单元测试稳定可靠且便于维护,单元测试用例之间绝不能相互调用,也不能以来执行的先后顺序
- 单元测试是可以重复执行的,不能收到外界影响。
为了不受外界环境影响,要求设计代码时,就把SUT的依赖改成注入,在测试时用Spring这样的DI框架注入一个bending内存或者实现Mock实现 - 对于单元测试,要保证测试粒度足够小,有助于精确定位问题,单元测试粒度至少是类级别,一般是方法级别
- 核心业务,核心应用,核心模块的增量代码确保单元测试通过
- 单元测试必须写在
src/test/java
工程目录下,不允许写在业务代码目录下 - 单元测试的基本目标:
- 语句覆盖率达到70%
- 核心模块的语句覆盖率和分支覆盖率都要达到100%
在工程规约的应用分层中提到的DAO层、Manager层、可重用度高的Service层,都应该进行单元测试
- 编写单元测试代码应遵守BCDE原则,以保证被测试模块的交付质量
- B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等
- C:Correct,正确的输入,并得到预期结果
- D:Design,与设计文档相结合,来编写单元测试
- E:Error,强制错误信息输入(如非法数据,异常流程,非业务允许输入等),并得到预期结果
- 对于数据库相关的查询、更新、删除操作,不能假设数据库里是存在的,或直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据
- 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据,或者对单元测试产生的数据有明确的前后缀标识。
例如:在RDC内部单元测试中,使用RDC_UNIT_TEST的前缀标识数据 - 对于不可测的代码建议做必要的重构,使代码变得可测,避免为达到测试要求而书写不规范的测试代码
- 在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例(UC)
- 单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,建议在项目提前测试完成单元测试。
- 为了更方便进行单元测试,业务代码应该避免出现以下情况:
- 构造方法做的事情过多
- 存在过多的全局变量和静态变量
- 存在过多的外部依赖
- 存在过多的条件语句
- 不要对单元测试有如下误解:
- 那是测试工程师干的事情!
- 单元测试代码是多余的!汽车的整体功能和各单元部件的测试正常是否是强相关的
- 单元测试代码不需要维护!
- 单元测试与线上故障没有辩证关系,好的单元测试能够最大限度规避线上故障
安全规约
说明编程中需要注意比较基础的安全准则
- 属于用户个人页面或者功能必须进行权限控制校验
- 用户敏感数据禁止直接展示,必须对展示内容进行脱敏
- 用户输入的sql参数严格使用参数绑定或者METADATA字段限定,防止sql注入,禁止字符串拼接sql访问数据库
- 用户请求传入的任何参数必须做有效性校验
忽略参数校验可能导致如下情况:
- page size 过大导致内存溢出
- 恶意order by导致数据库慢查询
- 任意重定向
- sql注入
- 反序列化注入
- 正则输入源拒绝服务ReDoS
- 禁止想HTML页面输出未经安全过滤或不正确转义的用户数据
- 表单、AJAX提交必须执行CSRF安全过滤
- 在使用平台资源时,譬如端性,邮件,电话,下单支付,必须实现正确的放重放限制,如数量限制,疲劳度控制,验证码校验,避免滥刷,资损
- 针对发帖,评论,发送即时消息等用户生成内容的场景,必须实现防刷,文本内容违禁词过滤等风控策略等。
MySQL数据库
底层数据库规范有助于减少软件实现的复杂度,降低沟通成本,主要说明建表规范、索引优化准则以及ORM层的处理约定。
建表规约
- 表达是否的概念,必须使用is_xxx的方式命名,数据类型是unsigned tinyint(1表示是,0表示否)
任何字段如果是非负数,则必须是unsigned
例如:表达逻辑删除的字段名为is_deleted,1表示删除,0表示未删除 - 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下画线中间只出现数字,数据库字段名的修改代价很大,因为无法进行预发布,所以字段名需要慎重考虑。
MySQL在windows下不区分大小写,但是在Linux下默认区分大小写,因此,数据库名,表名,字段名都不允许出现任何大写字母,避免节外生枝。 - 表名不使用复数名词
- 禁止使用保留子,如desc、range、match、delayed,请参考MySQL官方保留字
- 主键索引名为pk_字段名(primary key),唯一索引名为uk_字段名(unique_key),普通索引名为idx_字段名(index)。
- 小数类型为decimal,禁止使用float和double。
在存储的时候,float和double存在精度损失问题,很可能在比较值的时候,得不到正确结果,如果存储的数据范围超过decimal的范围,建议将数据拆成整数和小数分开存储。 - 如果存储的字符串长度几乎相等,应该使用char定长字符串类型。
- varchar是可变长字符串,不预先分配存储空间,长度不要超过5000个字符,如果存储长度大于此值,则应该定义类型为text,独立出一张表,用主键来对应,避免影响其他字段索引效率
- 表必备三字段,id,gmt_create,gmt_modified。
其中,id必为主键,类型为unsigned bigint,单表时自增,步长为1,gmt_create和gmt_modified的类型均为date_time类型,前者现在时表示主动创建,后者过去分词标识被动更新 - 表的命名最好加上“业务名称_表的作用”
- 库名与应用名称尽量一致
- 当修改字段含义或对字段标识状态追加时,需要及时更新字段注释
- 字段允许适当冗余,以提高查询性能,但必须考虑数据一致,冗余字段应该遵循:
- 不是频繁修改的字段
- 不是varchar超长字段,更不能是text字段
例如:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联表中冗余存储类目名称,避免关联查询
- 当单表行超过500万行或者单表容量超过2GB时,才推荐进行分库分表
如果预计三年后的数据量无法达到这个级别,请不要在创建时就分库分表 - 设置合适长度字符存储长度,不但可以节约数据库表空间和索引存储,更重要是能够提升检索速度
对象 | 年龄区间 | 类型 | 字节 | 表示范围 |
---|---|---|---|---|
人 | 150岁之间 | unsigned tinyint | 1 | 无符号值:0至255 |
龟 | 数百岁 | unsigned smallint | 2 | 无符号值:0至65535 |
恐龙化石 | 数千万年 | unsigned int | 4 | 无符号值:0至42.9亿 |
太阳 | 约50亿年 | unsigned bigint | 1 | 无符号值:0至1019 |
索引规约
- 业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。
不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但会明显提高查找速度;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生 - 超过三个表禁止用join,需要join字段,数据类型必须一致;当多表关联查询时,保证被关联字段需要有索引
即使双表进行join,也要注意表索引,sql性能 - 在varchar字段建立索引,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。
索引长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会达到90%以上,可以使用count(distinct left(列名,索引长度)/count(*))
的区分度来确定 - 页面搜索严禁左模糊或者全模糊,如需要请通过搜索引擎来解决。
索引文件具有B-Tree的最左前缀匹配特性,如果左边值未确定,那么无法使用此索引 - 如果有order by的场景,请注意利用索引的有序性,order by最后字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort情况,影响查询性能。
例如:
where a=? and b=? order by c;
索引a_b_c可用
where a>10 order by b
索引a_b无法排序,其有效性无法利用 - 利用覆盖索引进行查询操作,避免回表
例如:如果想知道一本书第11章是什么标题,我们没有必要翻开第11章对应那一页吗?只浏览一下目录即可,这个目录起到了覆盖索引的作用。
能够建立索引的种类分为主键索引,唯一索引,普通索引,而覆盖索引只是查询的一种效果,用explain的结果,extra列会出现using index - 利用延迟关联或者子查询优化超多分页场景
MySQL并不是跳过offset行,而是去offset+N行,然后返回放弃前offset行,返回N行,当offset特别大的时候,效率非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。
正例:先快速定位需要获取的id段,然后再关联:
select a.字段 from 表1 a, (select id from 表1 where 条件 limit 10000, 20) b where a.id = b.id - SQL性能优化的目标,至少要达到range级别,要求是ref级别,最好是consts。
- consts单表中最多只有一个匹配行(主键或唯一索引),在优化阶段即可读取到数据
- ref指使用普通的索引(normal index)
- range对索引进行范围检索
- 建立组合索引时候,区分度最高的在最左边。
如果where a = ? and b = ?, a列几乎接近于唯一值,那么只需要单建idx_a 索引即可。
如果存在非等号和等号混合的判断条件,在建立索引时,请把等号条件列前置,入where a > ? and b=?,那么即使a的区分度更高,也必须把b放在索引的最前列 - 应防止字段类型不同造成的隐式转换,导致索引失效
- 创建索引时,避免有如下极端误解:
- 宁滥勿缺,认为一个查询就需要建立一个索引
- 宁缺勿滥,认为索引会消耗空间,严重拖慢更新和新增的速度
- 抵制唯一索引,认为业务的唯一性一律需要在应用层“先查后插”的方式解决
SQL语句
- 不要使用
count(列名)
或count(常量)
来替代count(*)
,count(*)
是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关
count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行 - count(distinct col)计算该列除NULL外的不重复行数,注意,count(distinct col1, col2), 如果其中一列全为NULL,那么即使另一列有不同的值,也返回0
- 当某一列的值全为NULL时,count(col) 返回结果为0,但sum(col)的返回结果为NULL,因此,使用sum()时需要注意NPE问题
可以使用如下问题避免sum的NPE问题:
select if(isnull(sum(g)),0,sum(g)) from table
- 使用isnull()判断是否为null值。
- null与任何值比较窦唯null
- null<>null返回结果为null,而不是false
- null=null返回结果为null,而不是true
- null<>1的返回结果是null,而不是true
- 在代码中写分页查询逻辑时,若count为0应直接返回,避免执行后面的分页语句
- 不得使用外键与级联,一切外键概念必须在应用层解决。
- 禁止使用存储过程,存储过程难以调试和维护,更没有移植性
- 数据订正(特别是删除、修改记录操作),要首先select,避免出现误删除,确认无误后才能执行更新语句
- in操作能避免则避免,若实在避免不了,需仔细确认评估in后面元素数量,控制在1000以内。
- 如果有全球化需要,所有的字符存储与表示,均以utf-8编码,注意字符统计函数的区别。
select length(“轻松工作”); 返回12
select character_length(“轻松工作”); 返回为4。
如果需要存储表情,那么选择utf8mb4进行存储,注意它与utf-8编码的区别
mysql字符集 utf8 和utf8mb4 的区别 - truncate table 比delete快,且使用的系统和事务日志资源少,但truncate 无事物且不触发trigger,有可能造成事故,故不建议在开发代码中使用此语句。
ORM映射
- 在表查询中,一律不要使用* 作为查询字段里诶嗯,需要哪些字段明确写出
- 增加查询分析器解析成本
- 增减字段容易与resultMap不一致
- POJO类的布尔属性不能加is,而数据库字段必须加is_,要求在resultMap中进行字段名与属性之间的映射
如果使用MyBatisGenerator生成代码,则需要进行相应修改 - 不要使用resultClass作为返回参数,即使所有类属性名与数据库字段一一对应,也需要定义,反过来,每一个表必然有一个属性与之对应。
配置映射关系,使字段与DO类解藕,方便维护 - sql.xml配置参数使用#{},#param#,不要使用${},此方式容易出现sql注入
- iBATIS自带的
queryForList(String statementName, int start, int size)
不推荐使用
其实现方式是在数据库取到statementName对应的SQL语句的所有记录,再通过subList取start,size子集合 - 不允许直接拿HashMap与HashTable作为查询结果集的输出
resultClass=“HashTable”,会置入字段名和属性值,但是值的类型不可控 - 更新数据表记录时,必须同时更新记录对应的修改时间,即gmt_modified字段值为当前事件
- 不要写一个大而全的数据更新接口,传入POJO类,如果不管是否为自己的目标更新字段都进行update 操作,是不对的。执行SQL时,不要更新无改动的字段,一是容易出错,而是效率低,三是会增加binlog存储。
- @
Transactional
事务不要滥用,事物会影响数据库的QPS,另外,使用事物的地方需要考虑各方面的回滚方案,包括缓存回滚,搜索引擎回滚,消息补偿,统计修正等。 <isEqual>
中的compareValue是与属性值对比的常量,表示相等时执行的SQL语句;isNotEmpty
表示不为空且不为null时执行;isNotNull
表示null值时执行。
工程结构
说明应用工程分层思想,二方库约定及基本的服务器知识
- 图6-1中默认上层依赖于下层,箭头关系标识可直接依赖,如开放接口层依赖于Web层,也可以直接依赖于Service层,以此类推 。
- (分层异常处理规约)在DAO层,产生的异常类型有很多,无法细粒度的异常执行catch,使用
catch (Exception e)
方式,并throw new DAOException(e)
。不需要打印日志,因为日志在Manager/Service层,一定需要捕获并写到日志文件中去,如果同太服务器再写日志,会浪费性能和存储。在Service层出现异常时,必须讲出错日志记录到磁盘,尽可能带上参数信息,相当于保护案发现场。如果Manager层与Service同机部署,则日志与DAO层处理一致,如果是单独部署,则采用与Service层一致的处理方式。Web层绝不应该继续网上抛异常,因为已经处于顶层,如果意识到该异常将导致页面无法正常渲染,则应该直接跳到友好的错误页面,加上用户容易理解的错误提示信息。开放接口层需要讲异常处理成错误码和错误信息返回。 - 分层领域模型规约:
- DO (Data Object):与数据库表结构一一对应,通过DAO层向上传输数据源对象。
- DTO(Data Transfer Object):数据传输对象,Service 或Manager向外传输对象
- BO(Business Object):业务对象,有Service层输出的封装业务逻辑对象
- AO(Application Object):应用对象。在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
- VO(View Object):显示层对象,通常Web向模板渲染层传输的对象。
- Query:数据查询对象,各层次接收上层的查询请求,注意,如果是超过两个参数的查询封装,则禁止使用Map类来传输
二方库依赖
- 定义GAV遵从以下准则:
- GroupID格式:com.{公司/BU}.业务线.[子业务线],最多四级
- ArtifactID格式:产品线名-模块名。语义不重复不
- Version:
- 二方库版本号命名格式:主板本号.次版本号.修订版本号
- 主版本号:产品方向改变,或者大规模API不兼容,或者架构不兼容升级
- 次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的API不兼容修改
- 修订号:保持完全的兼容性,修改Bug,新增次要功能特性等
注意,起始版本号必须为1.0.0,而不是0.0.1。正式发布的类库必须先去中央仓库进行查证,使版本号有延续性,正式版本号不允许覆盖升级。例如当前版本号:1.3.3,那么下一个合理版本号1.3.4或者1.4.0或者2.0.0
- 线上应用不要依赖SNAPSHOT版本(安全包除外)。
不依赖SNAPSHOT版本是保证应用发布的幂等性。另外,也可以加快编译时的打包构建 - 二方库的新增或者升级,保持除功能点外的其他jar包仲裁结果不变。如果有改变,则必须明确评估和验证,建议进行dependency:resolve前后信息比对。如果仲裁结果完全不一致,则通过dependency:tree命令找出差异点,进行
<excludes>
排除jar包。 - 二方库里可以定义枚举类型,类型可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的POJO对象。
- 依赖于一个二方库群时,必须定义一个统一的版本变量,避免版本不一致。
- 禁止在子项目的pom依赖中出现相同的GroupId,相同的ArtifactId,但是不同的Version。
在本地调试时会使用各子项目指定的版本号,但是若合并成一个war,则只能有一个版本号出现在最后的lib目录中,可能会出现线下调试是正确的,发布到线上缺出故障的问题。 - 所有pom文件中的依赖声明放在dependencies语句块中,所有版本仲裁放在dependencyManagement语句块中。
dependencyManagement里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖,version和scope都取自父pom。而dependencies所有声明在主pom的dependencies里的依赖都会自动引入,并默认被所有的子项目继承。 - 二方库不要有配置项,最低限度不要再增加配置项。
- 为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则:
- 精简可控原则。移除一切不必要的API依赖,只包含Service API,必要领域模型对象,Utils类,常量、枚举等。如果依赖其他二方库,尽量provided引入,让二方库使用者依赖具体版本号;无log具体实现
- 稳定可追溯原则。每个版本的变化都应该被记录,二方库版本由谁维护,源码在哪里,都需要能方便的查到。除非用户主动升级版本,否则公共二方库的行为不应该发生变化。
服务器
- 并发服务器建议调笑TCP协议的time_wait超时事件。
操作系统默认240s后,才会关闭time_wait状态的连接。在高并发访问下,服务器会因为处于time_wait的连接数过多,而无法建立新的连接,所以需要在服务器上调小此等待值。
在linux服务器上可通过变更/etc/sysctl.conf文件去修改默认值(s)。
net.ipv4.tcp_fin_timeout=30
- 调大服务器所支持的最大文件句柄数(FileDescriptor,简写为fd)。
主流操作系统的设计是将TCP/UDP连接采用与文件一样的方式管理,即一个连接对应于一个fd。主流的Linux服务器默认支持最大fd数量为1024,当并发连接数很大时很容易因为fd不足而“open too many files”错误,导致新的连接无法建立。建议架将linux服务器所支持的最大句柄调高数倍(与服务器的内存数量相关) - 给JVM设置 -XX:+HeapDumpOnOutOfMemoryError参数,让JVM碰到OOM场景时输出dump信息。
OOM的发生是有概率的,甚至有规律地相隔数月才出现一例,出现时现场信息对查错非常有价值 - 在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力。
- 服务器内部重定向使用forward;外部重定向地址使用URL拼装工具类来生成,否则会带来URL维护不一致的问题和潜在的安全风险
设计规约
说明软件设计过程UML设计准则以及基本交狗理念
- 存储方案和底层数据结构的设计获得评审一致通过,并沉淀为文档。
- 有缺陷的底层数据结构容易导致系统风险高、可扩展性低,重构成本因历史数据迁移、系统平滑过度的需要也会陡然增加。所以,对存储方案和数据结构需要进行认真的设计和评审,生产环境提交执行后,团队成员需要进行double check。
- 在需求分析阶段,如果与系统交互的User超过1个类,并且相关的User Case超过5个,那么使用用例图来表达结构化需求会更加清晰。
- 如果某个业务对象的状态超过3个,那么应使用状态图来表达并且明确状态变化的各个触发条件。
状态图的核心是对象状态,首先明确对象有多少种状态,然后明确两两状态之间是否存在直接转换关系,再明确触发状态转换的条件是什么。 - 如果系统中某个功能的调用链路上设计的对象超过3个,则应该使用时序图来表达并明确各个环节的输入与输出。
时序图反映了一系列对象间的交互与协作关系,可以清晰立体地反映系统的调用的纵深链路 - 如果系统中模型超过5个,并且存在复杂的依赖关系,则应该使用类图来表达并明确类之间的关系
类图就像建筑领域的施工图,如果搭平房,可能不需要,但如果建造“蚂蚁Z空间”大楼,则肯定需要详细的施工图 - 如果系统中超过2个对象之间存在协作关系,并且需要表示复杂的处理流程,则使用活动图来表示。
活动图是流程图的扩展,增加了能够体现协作关系的对象泳道,支持并发表示等 - 考虑主干功能的同时,需求分析与系统设计需要充分评估异常流程与业务边界。
- 类在设计与实现要符合单一原则。
单一原则是最易理解却又最难实现的一条规则,随着系统的演进,工程师很多时候会忘记类设计的初衷 - 谨慎使用继承的方式进行扩展,优先使用聚合或组合的方式来实现。
若一定要使用继承,则必须符合里式代换原则,即父类能够出现,子类一定能够出现。 - 在系统设计时,根据依赖倒置原则,尽量依赖抽象类与接口,有利扩展与维护
低层次模块依赖于高层次模块的抽象,方便系统间的解藕 - 在设计系统时,注意对扩展开放,对修改闭合。
在极端情况下,交付的代码是不修该的,同一业务域内的需求变化,应通过模块或类的扩展来实现 - 在系统设计阶段,共性业务或公共行为抽取出来公共模块,公共配置,公共类,公共方法等,在系统中不应该出现重复代码的情况。
- 系统设计的摸底是明确需求,理顺逻辑,后期维护,次要目的是用于指定编码
避免为了设计而设计,系统设计文档有助于后期的系统维护,所以设计结果需要进行分类归档保存 - 设计的本质是识别和表达系统难点,并找到系统的变化点,并隔离变化点。
- 系统架构设计的目的:
- 确定系统边界,确定系统在技术层面上的做与不做
- 确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。
- 确定指导后续设计与演化的原则。使后续的子系统或模块设计在一个规定的框架内继续演化
- 确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性
- 避免发生如下误解:敏捷开发 = 讲故事 + 编码 + 发布
敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摈弃传统的审批流程,但需要核心关键点上必要设计和文档沉淀。
专有名词
- POJO(Plain Ordinary Java Object):在本手册中,POJO专指只有
setter/getter/toString
的简单类,包括DO/DTO/BO/VO
等 - GAV(GroupId、ArtifactId、Version):Maven坐标,用来唯一标识jar包
- OOP(Object Oriented Programming):本开发手册泛指类、对象的基本编程处理方式
- ORM(Object Relation Mapping):对象关系映射,对象领域模型与底层数据之间的转换,本文泛指iBATIS,mybatis等框架
- NPE(java.lang.NullPointerException):即空指针异常
- SOA(Service-OrientedArchitecture):面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用,有利于提升组件可重用性和可维护性。
- 一方库:本工程内部子项目模块依赖的库(jar包)
- 二方库:公司内部发布到中央仓库,可供公司内部其他应用依赖的库(jar包)
- 三方库:公司之外的开源库(jar包)
- IDE(Integrated Development Environment):用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具。
来源 :
- 阿里巴巴-Java开发手册