refactor: re-organise doc structure
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
---
|
||||
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);
|
||||
```
|
||||
Reference in New Issue
Block a user