Java开发经验——阿里巴巴编码规范经验总结2

发布于:2025-05-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

摘要

这篇文章是关于Java开发中阿里巴巴编码规范的经验总结。它强调了避免使用Apache BeanUtils进行属性复制,因为它效率低下且类型转换不安全。推荐使用Spring BeanUtils、Hutool BeanUtil、MapStruct或手动赋值等替代方案。文章还指出不应在视图模板中加入复杂逻辑运算,应明确MVC架构各层的职责。此外,还涉及数据结构初始化应指定大小、正则表达式的预编译、避免通过catch处理某些RuntimeException异常、finally块中资源关闭的正确方式以及防止NPE的多种方法。

1. 【强制】避免用 ApacheBeanutils 进行属性的 copy。

不推荐使用 Apache Commons BeanUtils 工具来进行对象属性复制(如 BeanUtils.copyProperties),因为它效率低、性能差、类型转换不安全,在生产环境中容易成为性能瓶颈。

Apache BeanUtils 是通过反射 + 内省(Introspector)+ 字符串转换来做属性 copy,性能非常低,不适合在高并发或大量对象转换场景中使用。

1.1. 属性赋值推荐方案

方案

优势

场景

Spring BeanUtils

性能略优于 Apache,但仍是反射

适合小量级对象拷贝

Hutool BeanUtil

性能高,支持深拷贝、自定义字段映射

推荐在工具类中统一封装

MapStruct

编译期生成拷贝代码(无反射,极快)

推荐在 DDD 中的 DO <-> DTO 映射

手动赋值

最安全、最清晰

小对象或关键转换逻辑

ModelMapper / Dozer(不推荐)

仍是反射,配置复杂,性能低

不推荐使用

2. 【强制】不要在视图模板中加入任何复杂的逻辑运算。

在 MVC 架构的具体实现中,比如 Spring Boot 项目中,我们常见的结构包括:

  • Controller(控制器)
  • Service(服务/业务逻辑层)
  • DAO(数据访问层,也叫 Mapper、Repository)

下面是这三层的职责和理解方式,结合“不要在视图中写复杂逻辑”的那条建议,进一步深化层次的划分:

2.1. Controller:控制层

职责:

  • 接收 HTTP 请求参数;
  • 调用 Service 进行处理;
  • 封装和返回响应数据(Response);
  • 做参数校验、权限判断、日志记录等外围操作。

不要做的事:

  • 不要写业务逻辑;
  • 不要操作数据库;
  • 不要做复杂的流程判断或数据处理。

示例:

@PostMapping("/user/upgrade")
public Response<Void> upgradeUser(@RequestBody UserUpgradeRequest request) {
    userService.upgradeUserToVip(request.getUserId());
    return Response.success();
}

2.2. Service:业务逻辑层

职责:

  • 实现具体业务逻辑,如“升级用户为 VIP”、“扣减库存”、“发送通知”等;
  • 调用多个 DAO、封装业务判断流程;
  • 做事务控制(@Transactional);
  • 组装处理结果返回 Controller。

不要做的事:

  • 不要和 Web 框架(如 Servlet、HttpRequest)耦合;
  • 不要拼 SQL,不直接操作数据库。

示例:

public void upgradeUserToVip(Long userId) {
    UserDO user = userDao.findById(userId);
    if (user == null || user.isVip()) {
        throw new BizException("用户不存在或已是VIP");
    }
    user.setVip(true);
    userDao.update(user);
    notifyService.sendVipNotification(user);
}

2.3. DAO(Mapper/Repository):数据访问层

职责:

  • 直接与数据库交互;
  • 封装 SQL 查询(或通过 MyBatis/JPA 映射);
  • 只做增删改查操作;
  • 返回实体对象,不做业务判断。

不要做的事:

  • 不要处理业务逻辑;
  • 不要做流程判断;
  • 不要拼接复杂结果(如组装响应对象)。

示例:

@Mapper
public interface UserDao {
    UserDO findById(Long userId);
    int update(UserDO user);
}

2.4. 总结类比:工厂分工

层级

类比

职责

Controller

前台接待

接收客户请求,转交到内部处理

Service

经理

安排工人干活,处理流程,判断异常

DAO

工人

操作数据库,搬原材料,不决策

2.5. MVC 和模板逻辑的对应关系

  • 视图(View) = 页面模板、前端:只能展示数据,不参与 Controller、Service、DAO 的职责
  • 所以复杂判断、业务数据准备都应该在 Service/Controller 中完成,模板中直接展示结果即可

3. 【强制】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。

3.1. 这条建议的核心思想是

在使用如集合(List、Map、Set 等)这类可扩展数据结构时,应尽可能“预估其大小”并“显式设置初始容量”,从而避免它们在运行中频繁扩容、内存抖动,甚至 OOM(内存溢出)的问题。提前预估数据量,合理初始化集合容量,是性能优化与内存安全的重要实践。

ArrayList
HashMap
HashSet
ConcurrentHashMap
StringBuilder
自定义缓存、队列等

这些类的背后都依赖一个数组或哈希桶来存储数据,如果你没有指定容量,它们会用默认大小初始化,然后在插入过程中自动扩容(重新开数组、拷贝数据等)。

3.2. 为什么要指定大小?

3.2.1. 不指定容量的风险:

  • 频繁扩容: 每次容量不够都要重新分配数组,拷贝旧数据 ➜ 性能开销大;
  • 内存浪费: 扩容步长不是线性的,可能会分配远超实际需要的空间;
  • 内存溢出(OOM): 在循环里构造数据结构没有设置上限 ➜ 无限增长,吃光堆内存。

3.2.2. 指定容量的好处:

  • 减少扩容次数: 提高性能;
  • 控制内存: 限制最大容量,防止意外 OOM;
  • 体现程序边界意识: 编码更健壮。

3.3. 数据结构设置初始值示例对比

3.3.1. 不指定大小(有性能隐患):

List<String> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
    list.add("item" + i);
}

默认容量是 10,之后 1.5 倍扩容 ➜ 至少扩容 10+ 次,代价很高

3.3.2. 指定大小(性能友好):

List<String> list = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
    list.add("item" + i);
}

只创建一次内部数组,避免扩容

3.4. 延伸到场景

数据结构

默认容量

推荐用法

ArrayList

10

new ArrayList<>(预计数量)

HashMap

16

new HashMap<>(预计数量 / 负载因子 + 1)

StringBuilder

16

new StringBuilder(预计字符串长度)

ConcurrentHashMap

16

new ConcurrentHashMap<>(预计大小)

4. 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。

说明:不要在方法体内定义:Pattern pattern = Pattern.compile("规则");

正则表达式在使用时,如果每次都重新编译,会严重影响性能。应该使用预编译(Pattern.compile(...))方式,将正则表达式提前编译好并重复使用。

在 Java 中使用正则时,一般有两种方式:

4.1. 每次都编译(效率低)

boolean isMatch = "abc123".matches("\\w+");

内部其实相当于:

Pattern.compile("\\w+").matcher("abc123").matches();

这会每次调用都重新编译正则表达式,开销很大,尤其在循环或高并发下。

4.2. 预编译后复用(推荐)

private static final Pattern PATTERN = Pattern.compile("\\w+");

boolean isMatch = PATTERN.matcher("abc123").matches();

正则表达式只在类加载时编译一次,后续调用直接复用,提高性能。

4.3. 使用场景

场景

是否推荐预编译

单次用、不频繁

可以临时用 .matches()

多次校验、循环中用

必须预编译

高并发服务接口中

必须预编译

工具类/公共方法

强烈建议预编译并静态缓存

4.4. 总结

  • 编译正则是耗时操作
  • 多次使用时,一定要用 Pattern.compile(...) 并缓存起来;
  • 正则预编译 = 性能优化 + 好习惯。

5. 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。

try-catch 是用来处理不可预知的异常情况,不是用来“代替 if 判断”的。

对于 Java 类库中常见的 RuntimeException(运行时异常),如果我们可以在代码运行前通过逻辑“预检查”避免它的发生,就不应该依赖 try-catch 来处理它。

5.1. 举几个典型例子

5.1.1. 不推荐的做法(用 catch 捕获 NPE):

try {
    System.out.println(user.getName());
} catch (NullPointerException e) {
    // 捕获空指针异常
    System.out.println("user 为空");
}

5.1.2. 推荐的做法(用 if 判断提前规避):

if (user != null) {
    System.out.println(user.getName());
} else {
    System.out.println("user 为空");
}

5.2. 为什么不推荐用 catch 处理这些异常?

  1. 这类异常不是业务异常,而是代码逻辑错误:出现 NullPointer、数组越界等,说明你的代码逻辑写得有问题,不是正常的“可恢复”情况。
  2. catch 成本高,影响性能:try-catch 的异常捕获机制在 JVM 中性能是开销较大的(尤其是频繁抛异常的情况)。
  3. 可读性变差,调试困难:滥用 catch 会把真正的问题掩盖,调试困难,也不利于代码维护。

5.3. 适用的异常类型(不建议 catch)

异常类

说明

NullPointerException

空指针异常,应通过非空判断避免

IndexOutOfBoundsException

下标越界,应判断下标是否合法

ClassCastException

类型转换错误,应先 instanceof判断

IllegalArgumentException

参数非法,应通过参数校验处理

5.4. 异常捕获正确的原则

  • 能通过逻辑避免的异常,不要 try-catch
  • RuntimeException 更多是一种编码警告,不是业务流程的一部分
  • 只在顶层兜底或做日志监控时统一捕获这些异常

6. 【强制】finally 块必须对资源对象、 流对象进行关闭,有异常也要做 try-catch。

无论是否发生异常,finally 块中一定要确保资源被正确关闭,且关闭操作本身也要加 try-catch,避免二次异常导致资源未释放。

6.1. 正确的使用方式示例:

自 Java 7 起,Java 提供了 try-with-resources 语法,它能够自动关闭实现了 AutoCloseableCloseable 接口的资源(如 InputStream)。使用该语法,可以消除手动管理资源关闭的复杂性,并自动处理 close() 方法可能抛出的异常。

try (InputStream in = new FileInputStream("data.txt")) {
    // 读文件逻辑
} catch (IOException e) {
    e.printStackTrace(); // 异常处理
}

6.2. 错误的示例(不捕获关闭异常):

finally {
    in.close(); // 如果这里抛出 IOException,整个异常流程会被覆盖
}

6.3. 适用范围:

这条规范适用于所有需要关闭或释放的资源类,例如:

  • IO 流(InputStream、OutputStream、Reader、Writer 等)
  • 数据库连接(Connection、Statement、ResultSet)
  • 网络资源(Socket、HttpURLConnection)
  • 文件句柄
  • 线程池(ExecutorService 的 shutdown
  • 锁(Lock.unlock()

7. 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景

1)返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE,反例:public int method() { return Integer 对象; },如果为 null,自动解箱抛 NPE。
2)数据库的查询结果可能为 null。
3)集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4)远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5)对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6)级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。正例: 使用 JDK8 的 Optional 类来防止 NPE 问题。

7.1. 常见的 NPE 产生场景

7.1.1. 访问未初始化的对象

当你尝试访问一个未初始化的对象(即其值为 null)时,通常会抛出 NPE。

String str = null;
int length = str.length();  // NPE: str 是 null,无法调用 length()

7.1.2. 调用 null 对象的实例方法

如果对象为 null,直接调用其方法会导致空指针异常。

MyClass obj = null;
obj.someMethod();  // NPE: obj 是 null,无法调用 someMethod()

7.1.3. 尝试访问 null 数组元素

null 数组尝试访问元素时也会抛出 NPE。

String[] arr = null;
String element = arr[0];  // NPE: arr 是 null,无法访问元素

7.1.4. 传递 null 给不接受 null 的方法

有些方法要求传入非 null 的参数,如果传入 null,可能会触发 NPE。

public void printLength(String str) {
System.out.println(str.length());  // 如果 str 为 null,将引发 NPE
}

7.1.5. 链式调用中的空指针

在链式调用中,如果某一环节返回了 null,而后续还对其进行方法调用,就会导致 NPE。

Person person = getPerson();
int age = person.getAddress().getCity().getZipCode();  // 如果 person 或 address 为 null,则会 NPE

7.2. 如何防止NPE问题?

7.2.1. 避免使用 null

尽量避免使用 null,特别是在可能触发 NPE 的地方。可以使用 Optional 来表示可能为空的值。

Optional<String> optionalStr = Optional.ofNullable(str);
optionalStr.ifPresent(s -> System.out.println(s.length()));  // 安全访问

7.2.2. 空值检查

在调用对象的方法之前,先检查对象是否为 null

if (str != null) {
    System.out.println(str.length());  // 只有 str 非 null 时才调用方法
} else {
    System.out.println("str is null");
}

7.2.3. 使用默认值

如果方法或字段值可能为 null,考虑使用默认值或替代值。

String str = Optional.ofNullable(inputString).orElse("default value");

7.2.4. 适用断言和工具库

通过工具库如 Apache Commons Lang 提供的 StringUtilsObjectUtils 等可以避免手动编写空值检查代码,减少 NPE 风险。

StringUtils.isNotEmpty(str);  // 不会抛出空指针异常

7.2.5. 使用 @NonNull@Nullable 注解

通过注解可以清楚标明方法参数或返回值是否可以为空,这有助于避免因不清楚空指针约束导致的 NPE。

public void processString(@NonNull String str) {
    // str 必须不为 null
}

7.2.6. 避免深层嵌套的链式调用

通过设计合理的 API 接口或引入中间变量,避免深层次的链式调用,降低因某一环节为 null 导致的 NPE 风险。

Address address = person != null ? person.getAddress() : null;
if (address != null) {
    // 安全地访问 address
}

博文参考


网站公告

今日签到

点亮在社区的每一天
去签到