省事多少,就看不见多少
一句话总结
Spring的优点和缺点是一样的——**很多事情都会自动发生。**所以即使出错也会自动出错。
不使用场注入的原因
@Service
public class ItemService {
@Autowired private ItemRepository repo; // ❌
}
看起来很舒服,却失去了四样东西。
final不能使用 — 成为生成后可能变更的字段- 在测试中更换需要反射——如果是生成器的话,直接跳过就可以了。
- 看不到依赖性有多少——如果生成器参数有七个,就会看到“这个类做太多事”的字样。如果分散在字段中,就看不到。
- 循环引用在运行时之前被隐藏——如果是生成器注入,在启动时就会立即执行。
@Service
public class ItemService {
private final ItemRepository repo;
ItemService(ItemRepository repo) { this.repo = repo; } // ✅
}
如果有一个生成器的话@Autowired也不需要。Spring会自己写。
**是选择“在机动时爆炸”和“在运营中爆炸”的问题。**前面更好。
验证在控制器上完成
public record ItemRequest(@NotBlank String name, @Min(1) int price) {}
@PostMapping("/items")
public ResponseEntity<?> create(@Valid @RequestBody ItemRequest req) { ... }
@Valid如果没有,安纳泰森**什么都不做。**留着不挂的情况大部分就是这个。
如果遇到验证的话,SpringMethodArgumentNotValidException掷出后,基本400出来。亲自if (name == null)如果在使用的话,就是在抢走框架应该做的事情。
将例外转换为状态代码的位置
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(NotFoundException.class)
ResponseEntity<Map<String, String>> notFound(NotFoundException e) {
return ResponseEntity.status(404).body(Map.of("message", e.getMessage()));
}
}
**聚集在一个地方是重点。**如果每个控制器都分散了try-catch,就没有人知道哪个异常会以什么代码退出。
还有return ResponseEntity.ok(Map.of("error", ...))就像200不包含错误一样。状态代码是合同的一部分——客户端、代理、监控全部都根据它进行操作。
@Transactional — 界限在哪里
@Transactional
public void bulkCreate(List<ItemRequest> reqs) { ... }
这个方法结束时提交,如果出现运行时异常,就会回滚。需要知道三个。
1. 检查例外基本不会回滚。IOException扔的话就会被提交。@Transactional(rollbackFor = Exception.class)必须给。如果不知道,就会变成“出现了例外,但数据已保存”的状态。
2. 不吃自我呼叫(self-invocation)。 同一类的其他方法this.method()叫罗的话就不经过代理服务器。@Transactional被忽视。交易没有发生,也没有发生错误——这是最难找到的类别。
**3.private不会附加到方法上。**因为代理无法覆盖。
还有N+1
这就是这个课程的正文。
List<Shop> shops = shopRepo.findAll(); // 쿼리 1개
for (Shop s : shops) {
s.getItems().size(); // 가게마다 1개씩 더
}
延迟加载(FetchType.LAZY)在实际使用时会发送查询。如果商店有3家,就会发送4家,如果商店有300家,就会发送301家。
开发时数据少看不到,在运营中列表变大时突然变慢。而且应用程序日志上没有任何异常——每个查询都很快。
修理的方法。
@Query("select distinct s from Shop s join fetch s.items")
List<Shop> findAllWithItems();
一次性加入后拿过来。查询会变成1个。
join fetch— 最直接。但是如果同时fetch两个或多个集合的话,就会成为交集。@EntityGraph— 用安纳图里昂来做同样的事情@BatchSize— 自动将N个IN绑在一起N/배치크기减少到一个。收藏很多的时候比较安全。
知道计税方法比知道如何改正更重要。
spring.jpa.properties.hibernate.generate_statistics=true
而且在测试中Statistics.getPrepareStatementCount()读了之后可以断言查询次数。
assertThat(queries).isLessThanOrEqualTo(2);
如果这个进去的话,以后谁会join fetch即使删除也会**捕捉测试。**这是为数不多的阻止性能回归测试的方法。
将测试分为层
| 安纳提安 | 悬挂 | 什么时候 |
|---|---|---|
@SpringBootTest |
全部上下文 | 整合确认,仅少数人 |
@WebMvcTest |
仅限网络层 | 控制器·验证·状态代码 |
@DataJpaTest |
仅限JPA层 | 查询·映射 |
@WebMvcTest因为不显示服务空隙,所以@MockitoBean放入(在启动3.4以前的名字是@MockBean). 每次显示整个上下文的话,测试会变慢,变慢的话没有人会运行。
在运营中实际受到的压力
连接池的线程数比线程数少。 HikariCP的默认值是10,但Tomcat线程是200。如果一个慢查询把整个池都占用,剩下的全部会等待——这时症状不是“DB很慢”,而是“应用程序停止了”。
**open-in-view基本打开了。即使打开了永续性上下文,直到视图渲染,即使在控制器之外发生延迟加载,也会安静地运行。虽然方便,但在请求连接期间一直保持连接。**关闭(spring.jpa.open-in-view=false)最好把需要的东西都从服务中填满后发送出去。关闭的话,迄今为止隐藏着的LazyInitializationException这暴露出来了,那是本来就存在的问题。
不要直接打开液压执行器。/actuator/env哇/actuator/heapdump里面有秘密。转到内部端口或进行认证。
为什么这是问题?
Spring中错误保持安静的原因是自动的事情即使出错也会自动。字段注入既可以编译也可以执行,即使有循环引用也会隐藏在运行时。@Transactional在同一个班级里喊的话什么都不做,但这个事实却没有出现在任何地方——例外是我就是无法回溯。
所以上述节点的共同点只有一个。**在错误时发出声音。**生成器注入在操作点时爆发出循环引用,如果验证在控制器中结束,错误请求就不会传递到服务层,将异常转换为状态代码的地方集中在一个地方,只要看到那个地方就可以知道API的约定。
在现场相遇的样子
在运营中受到影响的东西一般都有一个共同点,即在开发环境中无法再现。N+1在开发DB中看不到有3家店,连接池耗尽不会在同时使用者只有一个人时发生,测试隔离问题在一次次运行测试时会隐形。
所以这三件事不是通过代码评论来抓取的,而是让它可以计算来抓取的。统计查询次数,以连接池使用率为指标输出,以随机顺序进行测试。让看不见的东西变得可见,这就是这个模块的全部。