Files
homepage/docs/zh-hans/blog/java-development-cheatsheet.md
T
2026-05-21 02:11:31 -05:00

204 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Java 开发小技巧
tags:
- java
- tips
author:
name: Zihlu Wang
email: real@zihluwang.me
---
## BigDecimal 对比
在 Java 中对 `BigDecimal` 进行比较需要格外注意,因为 `equals``compareTo` 的行为并不相同。
### `equals` 与 `compareTo` 的区别
`BigDecimal#equals` 会同时比较**数值**和**标度(scale**,而 `BigDecimal#compareTo` 仅比较**数值**(忽略标度)。例如:
```java
var a = new BigDecimal("100"); // scale = 0
var b = new BigDecimal("100.00"); // scale = 2
a.equals(b); // false — 标度不同
a.compareTo(b); // 0 — 数学数值相同
var c = new BigDecimal("200");
a.compareTo(c); // -1(负数)— a 小于 c
c.compareTo(a); // 1(正数)— c 大于 a
```
### 为什么这很重要
当值来自不同来源时,标度不匹配的情况很常见——例如解析用户输入、从数据库(`DECIMAL(10,2)` 列)读取、或接收 JSON 数据。你可能会认为两个值相等,但 `equals` 却说它们不相等。
### 使用建议
- 使用 **`compareTo`** 进行数值相等判断:`a.compareTo(b) == 0`
- 仅在需要「完全相同的表示」(数值和标度都相同)时使用 **`equals`**
- 如果需要在 `equals` 前统一标度,可使用 **`stripTrailingZeros()`**
```java
a.stripTrailingZeros().equals(b.stripTrailingZeros()); // true
```
### 与零比较
避免使用 `==` 或 `.equals(BigDecimal.ZERO)` 来判断是否为零——推荐使用 `compareTo`
```java
if (value.compareTo(BigDecimal.ZERO) == 0) { ... }
```
## 如何从 BlockingQueue 中获得数据?
- 使用 `take()` 方法可以拿取数据,如果队列中没有数据会阻塞线程,一直等待;
- 使用 `poll()` 方法拿取数据,如果没有数据会返回 `null`
- 使用 `poll(long timeout, TimeUnit unit)` 方法拿去数据,将会在 `poll()` 的基础上等待特定的时间,如果时间到了还没有拿到数据则会返回 `null`
- 使用 `peek()` 拿取数据,但是这种方式不会将已经拿到的数据从队列中移除。
## Spring Cloud Alibaba 常见问题
### Nacos 在个人文件夹中创建 `nacos` 文件夹怎么办?
添加下面两个 Configuration properties 即可指定 nacos 的存储路径:
- `JM.LOG.PATH`
- `JM.SNAPSHOT.PATH`
### Sentinel 乱拉屎怎么办?
添加 Configuration property `csp.sentinel.log.dir` 即可修改 sentinel 的日志路径。
### 怎么在 JetBrains IntelliJ IDEA 中添加 Configuration Properties 配置?
在 JetBrains IntelliJ IDEA 中,在右上角的运行配置中,点击 **`Edit Configurations…`** ,即可看到配置页面。
点击页面中的 **`Modify options`** 按钮,并在下方附加上的 **`Override configuration properties`** 表格中添加需要重置的配置属性。
## Spring Data JPA 常见问题
### 使用 JPA 分页查询时提示 "Serializing `PageImpl` instances as-is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure" 怎么办?
在应用程序主启动类上添加下面的代码:
```java
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
```
### 使用 JPA 分页查询时查询第一页都没数据是为什么?
JPA 的分页页码是从 0 开始的,请求中的页码数据 = 你实际想查询页码数 - 1。
### 如何避免 N+1 查询问题?
N+1 问题是指 JPA 先执行 1 条查询获取父实体,再为每条父实体的关联执行 N 条额外的子查询。
**检测方法** — 观察日志中是否有大量重复的 SQL 查询,或者配置 `spring.jpa.properties.hibernate.generate_statistics=true` 来发现。
**修复方案:**
| 方式 | 适用场景 |
|---------------------------|----------------------|
| `@EntityGraph` | 声明式,适合为特定实体定制抓取计划 |
| `@Query` 中使用 `JOIN FETCH` | 按查询精细控制 |
| `@BatchSize` | 将 N+1 降低为 N/k+1,批量加载 |
```java
// 方式一:EntityGraph
@EntityGraph(attributePaths = {"roles", "permissions"})
Optional<User> findById(long id);
// 方式二:JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
Optional<User> findByIdWithRoles(@Param("id") long id);
```
### `findById` 与 `getReferenceById` 该用哪个?
- **`findById`** — 立即查询数据库,返回实体或 `Optional.empty()`。需要实际数据时使用。
- **`getReferenceById`** — 返回一个惰性代理(proxy),**不查询数据库**。只有当访问不存在的代理属性时才会抛出 `EntityNotFoundException`。只需要 ID 来设置外键关系时使用。
```java
// 推荐:只需要引用 User 来设置外键
Post post = new Post();
post.setAuthor(userRepository.getReferenceById(userId));
```
### 如何解决 `LazyInitializationException`
当你在持久化上下文之外(例如 Controller 或序列化器中,事务已关闭后)访问懒加载的关联属性时,就会抛出此异常。
**解决方案:**
1. **使用 `JOIN FETCH` 或 `@EntityGraph`** 提前加载所需的关联数据。
2. **使用 DTO 投影** — 只返回需要的字段,而不是整个实体:
```java
@Query("SELECT new com.example.UserDto(u.id, u.name) FROM User u WHERE u.id = :id")
UserDto findUserDtoById(@Param("id") long id);
```
3. **在 Service 方法上使用 `@Transactional(readOnly = true)`** — 使 Session 在整个方法作用域内保持打开。
### 什么时候该用 `@Transactional(readOnly = true)`
在**只读**的 Service 方法上使用 `@Transactional(readOnly = true)` 有三个好处:
- Hibernate 会跳过脏检查(无需快照,占用内存更少)。
- JDBC 驱动可能会将请求路由到只读副本。
- 清晰地表达了方法的意图。
```java
@Service
public class UserService {
@Transactional(readOnly = true)
public UserDto getUser(long id) { ... }
@Transactional
public UserDto createUser(CreateUserRequest request) { ... }
}
```
### `save()` 与 `saveAll()` — 批量插入哪个更快?
`saveAll()` 在单个事务中执行,可以受益于 JDBC 批处理。需要配置批处理大小:
```yaml
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
```
对于大量数据的批量插入(数千行),建议改用 `JdbcTemplate` 的批处理操作——在这种规模下 Hibernate 的实体管理开销会非常大。
### 如何用 `Specification` 实现动态查询?
对于带有多个可选筛选条件的复杂搜索表单,使用 `JpaSpecificationExecutor`
```java
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
// 使用方式
Specification<User> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (name != null) {
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
if (status != null) {
predicates.add(cb.equal(root.get("status"), status));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
Page<User> page = userRepository.findAll(spec, pageable);
```