阿里巴巴java开发手册学习

本文详细记录了遵循阿里巴巴Java开发手册的要点,包括编程规约、并发处理、逻辑处理、异常处理和日志记录、数据库规约等方面,强调了如类名规范、集合操作注意事项、线程安全、参数校验、异常处理和日志记录的最佳实践,旨在提高代码质量和可维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文对阿里巴巴java开发手册中需要注意的点予以记录

1.编程规约

  • 类名中包含领域模型如DO/BO/DTO/VO时要 全部大写,如UserDTO.

  • 抽象类要以Abstract或Base开头,异常类以Exception结尾,测试类要以所测试的类开头,Test结尾。

  • 杜绝不规范的缩写

  • 将设计模式体现在类名中,有利于阅读者快速理解架构,如OrderFactory LoginProxy ResourceObsever

  • service和dao层的方法命名推荐:get list count save/insert remove/delete update作为前缀

  • 不要使用一个常量类来维护所有常量,应该分类分开维护

  • 任何运算符前后都应该有一个空格

  • 所有覆写方法,必须添加@Override注解

  • POJO类必须写toString方法,便于排查问题(抛出异常时会调用该类的toString打印属性值)

  • final可以提高程序效率,多考虑属性,方法,类是否可以定义成final

  • 关于 hashCode 和 equals 的处理,遵循如下规则:
    1) 只要重写 equals ,就必须重写 hashCode 。
    2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的
    对象必须重写这两个方法。
    3) 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals 。
    正例: String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象
    作为 key 来使用。

  • ArrayList 的 subList 结果不可强转成 ArrayList ,否则会抛出 ClassCastException异常: java . util . RandomAccessSubList cannot be cast to java . util . ArrayList ;说明: subList 返回的是 ArrayList 的内部类 SubList ,并不是 ArrayList ,而是ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生 ConcurrentModificationException 异常。但实际开发中subList基本不会使用。

  • 使用集合转数组的方法,必须使用集合的 toArray(T[] array) ,传入的是类型完全一样的数组,大小就是 list . size() 。
    反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[] 类,若强转其它类型数组将出现 ClassCastException 错误。
    正例:

List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);

说明:使用 toArray 带参方法,入参分配的数组空间不够大时, toArray 方法内部将重新分配内存空间,并返回新数组地址 ; 如果数组元素大于实际所需,下标为 [ list . size() ] 的数组元素将被置为 null ,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。

  • 使用工具类 Arrays . asList()数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常。
    说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。 Arrays . asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
    String[] str = new String[] { “a”, “b” };
    List list = Arrays.asList(str);
    第一种情况: list.add(“c”); 运行时异常。
    第二种情况: str[0]= “gujin”; 那么 list.get(0) 也会随之修改。
    如图:返回的是Arrays的内部类
/**
     * Returns a fixed-size list backed by the specified array.  (Changes to
     * the returned list "write through" to the array.)  This method acts
     * as bridge between array-based and collection-based APIs, in
     * combination with {@link Collection#toArray}.  The returned list is
     * serializable and implements {@link RandomAccess}.
     *
     * <p>This method also provides a convenient way to create a fixed-size
     * list initialized to contain several elements:
     * <pre>
     *     List&lt;String&gt; stooges = Arrays.asList("Larry", "Moe", "Curly");
     * </pre>
     *
     * @param <T> the class of the objects in the array
     * @param a the array by which the list will be backed
     * @return a list view of the specified array
     */
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
  • 【强制】不要在 foreach 循环里进行元素的 remove / add 操作。 remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
    反例:

List< String>  a = new ArrayList<>();
	a.add("1");
	a.add("2");
for (String temp : a) {
	if("1".equals(temp)){
		a.remove(temp);
	}
}

说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?会报ConcurrentModificationException并发修改异常。
正例:

Iterator< String> it = a.iterator();
while(it.hasNext()){
	String temp = it.next();
	if(删除元素的条件){
		it.remove();
	}
}
  • 集合初始化时,尽量指定集合初始值大小。
    说明: ArrayList 尽量使用 ArrayList(int initialCapacity) 初始化
  • 使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。
    说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出key 所对应的 value 。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。 values() 返回的是 V 值集合,是一个 list 集合对象 ;keySet() 返回的是 K 值集合,是一个 Set 集合对象 ;entrySet() 返回的是 K - V 值组合集合。
    map的四种遍历方式
    如果是 JDK 8,使用 Map . foreach 方法
Map<String,Integer> m = new HashMap<>();
m.put("hello", 11);
m.put("world", 11);
m.put("nihao", 21);
m.put("shijie", 22);
m.forEach((key,value)-> System.out.println(key+"  "+value));
  • 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,注意存储null 值时会抛出 NPE 异常
集合类keyvaluesuper说明
Hashtable不允许为 null不允许为 nullDictionary线程安全
ConcurrentHashMap不允许为 null不允许为 nullAbstractMap分段锁技术
TreeMap不允许为 null允许为 nullAbstractMap线程不安全
HashMap不允许为 null不允许为 nullAbstractMap线程不安全

2.并发处理

  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明: Executors 返回的线程池对象的弊端如下:
    1) FixedThreadPool 和 SingleThreadPool :
    允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
    2) CachedThreadPool 和 ScheduledThreadPool :
    允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM
    而且注意自定义的线程池一定要定义有意义的线程池名称,便于查看日志排查问题。
 @Bean
    public TaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //如果池中的实际线程数小于corePoolSize,无论是否其中有空闲的线程,都会给新的任务产生新的线程
        taskExecutor.setCorePoolSize(5);
        //连接池中保留的最大连接数。
        taskExecutor.setMaxPoolSize(15);
        //queueCapacity 线程池所使用的缓冲队列
        taskExecutor.setQueueCapacity(6000);
        //强烈建议一定要给线程起一个有意义的名称前缀,便于分析日志
        taskExecutor.setThreadNamePrefix("demo Thread-");
        taskExecutor.initialize();
        return taskExecutor;
    }
  • SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为static ,必须加锁,或者使用 DateUtils 工具类。
    正例:注意线程安全,使用 DateUtils 。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
	@ Override
	protected DateFormat initialValue() {
		return new SimpleDateFormat("yyyy-MM-dd");
	}
};

说明:如果是 JDK 8 ,可以使用 Instant 代替 Date , LocalDateTime 代替 Calendar ,DateTimeFormatter 代替 Simpledateformatter ,官方给出的解释: simple beautiful strong immutable thread - safe 。

3.逻辑处理

  • 在一个 switch 块内,每个 case 要么通过 break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止 ; 在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有
  • 推荐尽量少用 else , if - else 的方式可以改写成:
    if(condition){

    return obj;
    }
    // 接着写 else 的业务逻辑代码;
    说明:如果非得使用 if()…else if()…else… 方式表达逻辑,【强制】请勿超过 3 层,超过请使用状态设计模式。
    正例:逻辑上超过 3 层的 if-else 代码可以使用switch语句,或者状态模式来实现
  • 方法中需要进行参数校验的场景:
    1 ) 调用频次低的方法。
    2 ) 执行时间开销很大的方法,参数校验时间几乎可以忽略不计,但如果因为参数错误导致
    中间执行回退,或者错误,那得不偿失。
    3 ) 需要极高稳定性和可用性的方法。
    4 ) 对外提供的开放接口,不管是 RPC / API / HTTP 接口。
    5) 敏感权限入口
  • 方法中不需要参数校验的场景:
    1 ) 极有可能被循环调用的方法,不建议对参数进行校验。但在方法说明里必须注明外部参数检查。
    2 ) 底层的方法调用频度都比较高,一般不校验。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。
    3 ) 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数
  • 所有的枚举类型字段必须要有注释,说明每个数据项的用途.但一般枚举都有description属性来描述意义。
  • 获取当前毫秒数 System . currentTimeMillis(); 而不是 new Date() . getTime();
    说明:如果想获取更加精确的纳秒级时间值,用 System . nanoTime() 。在 JDK 8 中,针对统计时间等场景,推荐使用 Instant 类。

4.异常处理和日志记录

  • 有 try 块放到了事务代码中, catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。
  • 不能在 finally 块中使用 return , finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句
  • 方法的返回值可以为 null ,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题。
    说明:本规约明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回 null 的情况
  • 【推荐】防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景:
    1 ) 返回类型为包装数据类型,有可能是 null ,返回 int 值时注意判空。
    反例: public int f() { return Integer 对象}; 如果为 null ,自动解箱抛 NPE 。
    2 ) 数据库的查询结果可能为 null 。注:其实现在很多框架在返回查询结果时已经进行处理不会返回null
    3 ) 集合里的元素即使 isNotEmpty ,取出的数据元素也可能为 null 。
    4 ) 远程调用返回对象,一律要求进行 NPE 判断。
    5 ) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
    6 ) 级联调用 obj . getA() . getB() . getC(); 一连串调用,易产生 NPE
  • 在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http / api 开放接口必须使用错误码 ; 而应用内部推荐异常抛出 ; 跨应用间 RPC 调用优先考虑使用 Result 方式,封装success 、code、msg。
    说明:关于 RPC 方法返回方式使用 Result 方式的理由:
    1 ) 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
    2 ) 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message ,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题
  • 应用中不可直接使用日志系统 (Log 4 j 、 Logback) 中的 API ,而应依赖使用日志框架SLF 4 J 中的 API ,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class)

注:这里变量名logger也并没有大写。

  • 日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点
  • 应用中的扩展日志 ( 如打点、临时监控、访问日志等 ) 命名方式:appName _ logType _ logName . log 。 logType :日志类型,推荐分类有stats / desc / monitor / visit 等 ;logName :日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
    正例: mppserver 应用中单独监控时区转换异常,如:mppserver _ monitor _ timeZoneConvert . log
    说明:推荐对日志进行分类,错误日志和业务日志尽量分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
  • 对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
    说明: logger . debug( " Processing trade with id : " + id + " symbol : " + symbol);**如果日志级别是 warn ,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,会执行 toString() 方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。**原来如此
    正例: ( 条件 )
if (logger.isDebugEnabled()) {
	logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}

正例: ( 占位符 )

logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
  • 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。
    正例: logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e); e会显示堆栈信息。
  • 谨慎地记录日志。生产环境禁止输出 debug 日志 ; 有选择地输出 info 日志 ; 如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
    说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

5.数据库规约

  • 单表行数超过 500 万行或者单表容量超过 2 GB ,才推荐进行分库分表。
    说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表
  • 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
    说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的 ; 另外,即使在应用层做了非常完善的校验和控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。注:这个值得考虑
  • 超过三个表禁止 join 。需要 join 的字段,数据类型保持绝对一致 ; 多表关联查询时,保证被关联的字段需要有索引。
    说明:即使双表 join 也要注意表索引、 SQL 性能。注:实际上,现在很多都全部采取单表,禁止关联,简化数据库维护。
  • 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
    说明:索引文件具有 B - Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。这个很难做到,如设备名称。
  • 创建索引时避免有如下极端误解:
    1 ) 误认为一个查询就需要建一个索引。
    2 ) 误认为索引会消耗空间、严重拖慢更新和新增速度。
    3 ) 误认为唯一索引一律需要在应用层通过“先查后插”方式解决
  • 在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句
  • 不得使用外键与级联,一切外键概念必须在应用层解决。
    说明: ( 概念解释 ) 学生表中的 student _ id 是主键,那么成绩表中的 student _ id 则为外键。
    如果更新学生表中的 student _ id ,同时触发成绩表中的 student _ id 更新,则为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群 ; 级联更新是强阻塞,存在数据库更新风暴的风险 ; 外键影响数据库的插入速度。现在基本都设计成单表,在应用中处理关联逻辑。
  • 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性
  • 数据订正时,删除和修改记录时,要先 select ,避免出现误删除,确认无误才能执行更新语句。没毛病更新一般是先根据id查询出值,再去修改。
  • 不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义 ; 反过来,每一个表也必然有一个与之对应。
    说明:配置映射关系,使字段与 DO 类解耦,方便维护。不能将entity直接作为返回值!
  • 更新数据表记录时,必须同时更新记录对应的 gmt _ modified 字段值为当前时间。规范意思是所有表都应有创建时间和修改时间两个字段。
  • @ Transactional 事务不要滥用。事务会影响数据库的 QPS ,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等

其他

  • 所有 pom 文件中的依赖声明放在< dependencies >语句块中,所有版本仲裁放在< dependencyManagement >语句块中。
    说明:< dependencyManagement >里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖, version 和 scope 都读取自父 pom 。而< dependencies >所有声明在主 pom 的< dependencies >里的依赖都会自动引入,并默认被所有的子项目继承。
Java 开发手册》是阿里巴巴集团技术团队的集体智慧结晶和经验总结,经历了多次大规模一 线实战的检验及不断完善,公开到业界后,众多社区开发者踊跃参与,共同打磨完善,系统化地整理成册,当前的版本是泰山版。现代软件行业的高速发展对开发者的综合素质要求越来越高,因为不仅 是编程知识点,其它维度的知识点也会影响到软件的最终交付质量。比如:数据库的表结构和索引设 计缺陷可能带来软件上的架构缺陷或性能风险;工程结构混乱导致后续维护艰难;没有鉴权的漏洞代 码易被黑客攻击等等。所以本手册Java 开发者为中心视角,划分为编程规约、异常日志、单元测 试、安全规约、MySQL 数据库、工程结构、设计规约七个维度,再根据内容特征,细分成若干二级 子目录。另外,依据约束力强弱及故障敏感性,规约依次分为强制、推荐、参考三大类。在延伸信息 中,“说明”对规约做了适当扩展和解释;“正例”提倡什么样的编码和实现方式;“反例”说明需 要提防的雷区,以及真实的错误案例。 手册的愿景是码出高效,码出质量。现代软件架构的复杂性需要协同开发完成,如何高效地协 同呢?无规矩不成方圆,无规范难以协同,比如,制订交通法规表面上是要限制行车权,实际上是保 障公众的人身安全,试想如果没有限速,没有红绿灯,谁还敢上路行驶?对软件来说,适当的规范和 标准绝不是消灭代码内容的创造性、优雅性,而是限制过度个性化,以一种普遍认可的统一方式一起 做事,提升协作效率,降低沟通成本。代码的字里行间流淌的是软件系统的血液,质量的提升是尽可 能少踩坑,杜绝踩重复的坑,切实提升系统稳定性,码出质量。 我们已经在 2017 杭州云栖大会上发布了配套的 Java 开发规约 IDE 插件,下载量达到 152 万人次,阿里云效也集成了代码规约扫描引擎。次年,发布 36 万字的配套详解图书《码出高效》,本书 秉持“图胜于表,表胜于言”的理念,深入浅出地将计算机基础、面向对象思想、JVM 探源、数据 结构与集合、并发与多线程、单元测试等知识客观、立体地呈现出来。紧扣学以致用、学以精进的目 标,结合阿里巴巴实践经验和故障案例,与底层源码解析融会贯通,娓娓道来。《码出高效》和《Java开发手册》书籍版所得收入均捐赠公益事情,希望用技术情怀帮助更多的人。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值