LabHub
学习 学习路径 课程

Spring Boot — 数一数发了几条查询

省事多少,就看不见多少

在 LabHub 中继续学习

一句话总结

Spring的优点和缺点是一样的——**很多事情都会自动发生。**所以即使出错也会自动出错。

概念图: 很多事情都会自动发生。 · final不能使用 · 在测试中更换需要反射 · 看不到依赖性有多少

不使用场注入的原因

@Service
public class ItemService {
    @Autowired private ItemRepository repo;   // ❌
}

看起来很舒服,却失去了四样东西。

  1. final不能使用 — 成为生成后可能变更的字段
  2. 在测试中更换需要反射——如果是生成器的话,直接跳过就可以了。
  3. 看不到依赖性有多少——如果生成器参数有七个,就会看到“这个类做太多事”的字样。如果分散在字段中,就看不到。
  4. 循环引用在运行时之前被隐藏——如果是生成器注入,在启动时就会立即执行。
@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个

知道计税方法比知道如何改正更重要。

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家店,连接池耗尽不会在同时使用者只有一个人时发生,测试隔离问题在一次次运行测试时会隐形。

所以这三件事不是通过代码评论来抓取的,而是让它可以计算来抓取的。统计查询次数,以连接池使用率为指标输出,以随机顺序进行测试。让看不见的东西变得可见,这就是这个模块的全部。