延迟加载
延迟加载(Lazy Loading)指推迟对象的初始化或数据加载,直到第一次真正使用时才执行。Spring Boot 中有两个独立维度:Bean 的延迟初始化和 JPA/Hibernate 的关联数据延迟加载。
一、Bean 延迟初始化
@Lazy — 单个 Bean
// 默认:Spring 启动时立即创建所有单例 Bean
@Service
public class EagerService {} // 启动时创建
// 加 @Lazy:首次被注入或通过 ApplicationContext.getBean() 调用时才创建
@Service
@Lazy
public class LazyService {
public LazyService() {
System.out.println("LazyService 初始化"); // 首次使用时才打印
}
}@Lazy 也可以标注在注入点(不影响 Bean 本身的定义):
@Service
public class OrderService {
// 注入的是 HeavyReportService 的代理,首次调用方法时才真正初始化该 Bean
@Autowired
@Lazy
private HeavyReportService reportService;
}这种用法常见于打破循环依赖,注入代理对象而非真实 Bean。
全局延迟初始化(Spring Boot 2.2+)
spring:
main:
lazy-initialization: true # 所有 Bean 默认延迟初始化优点:显著缩短应用启动时间(测试环境/开发环境友好)。
缺点:
- 首次请求延迟高(冷启动开销转移到第一次调用)
- 配置错误(如数据库连接失败)在第一次使用时才暴露,而不是启动失败
- 不适合生产环境(预热策略要求 Bean 提前就绪)
选择性排除(全局懒加载 + 部分 Bean 仍然饥饿加载):
// 标注 @Lazy(false) 强制该 Bean 在启动时立即初始化
@Service
@Lazy(false)
public class DatabaseHealthChecker {
// 启动时就检查数据库连接
}@Lazy 与 @Configuration
@Configuration
@Lazy // 配置类内所有 @Bean 方法默认延迟
public class ThirdPartyConfig {
@Bean
public HeavyClient heavyClient() { // 延迟创建
return new HeavyClient(connectionString);
}
@Bean
@Lazy(false) // 单独覆盖:该 Bean 仍立即创建
public LightClient lightClient() {
return new LightClient();
}
}二、JPA / Hibernate 关联延迟加载
JPA 的延迟加载针对实体关联(@OneToMany、@ManyToOne 等),控制关联数据是否随主实体一同查询。
FetchType 对比
| 注解 | 默认 FetchType | 说明 |
|---|---|---|
@ManyToOne | EAGER | 立即加载关联对象 |
@OneToOne | EAGER | 立即加载关联对象 |
@OneToMany | LAZY | 延迟加载集合 |
@ManyToMany | LAZY | 延迟加载集合 |
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
// 默认 EAGER:每次查询 Order 都会 JOIN 查询 User
@ManyToOne(fetch = FetchType.LAZY) // 改为 LAZY:按需加载
@JoinColumn(name = "user_id")
private User user;
// 默认 LAZY:访问 items 属性时才发出 SELECT
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}LAZY 的底层原理
Hibernate 为懒加载属性生成 CGLIB/ByteBuddy 代理(或字节码增强的子类)。访问代理属性时,Hibernate 发出额外 SQL:
Order order = orderRepository.findById(1L).get();
// 此时 order.user 是代理对象,未加载真实数据
String username = order.getUser().getName();
// 首次访问 User 属性 → Hibernate 发出 SELECT * FROM users WHERE id = ?三、N+1 问题
N+1 是懒加载最常见的性能陷阱:查询 N 条主记录后,逐条加载关联数据,产生 1+N 条 SQL。
// 触发 N+1:查询 100 个订单,每个订单再查一次用户
List<Order> orders = orderRepository.findAll(); // SQL 1 条
orders.forEach(o -> System.out.println(o.getUser().getName())); // SQL N 条
// 总计 101 条 SQL解决方案一:JPQL JOIN FETCH
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// JOIN FETCH 一次查询同时加载关联数据
@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.status = :status")
List<Order> findByStatusWithUser(@Param("status") OrderStatus status);
// 加载多级关联
@Query("SELECT DISTINCT o FROM Order o " +
"JOIN FETCH o.user " +
"JOIN FETCH o.items i " +
"JOIN FETCH i.product")
List<Order> findAllWithDetails();
}解决方案二:@EntityGraph
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// 声明式定义需要一起加载的属性
@EntityGraph(attributePaths = {"user", "items", "items.product"})
List<Order> findByStatus(OrderStatus status);
// 也可以引用实体上定义的命名图
@EntityGraph("Order.withUserAndItems")
Optional<Order> findById(Long id);
}
@Entity
@NamedEntityGraph(
name = "Order.withUserAndItems",
attributeNodes = {
@NamedAttributeNode("user"),
@NamedAttributeNode(value = "items", subgraph = "items")
},
subgraphs = @NamedSubgraph(
name = "items",
attributeNodes = @NamedAttributeNode("product")
)
)
public class Order { ... }解决方案三:Batch Fetching(批量加载)
Hibernate 会将多个懒加载请求合并为一条 IN 查询:
// 在实体上配置批量加载大小
@Entity
public class Order {
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@BatchSize(size = 50) // 一次最多批量加载 50 个订单的 items
private List<OrderItem> items;
}# 全局配置
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100 # 全局 batch size解决方案四:DTO Projection
直接查询所需字段,不走实体关联:
// 接口 Projection
public interface OrderSummary {
Long getId();
String getStatus();
String getUserName(); // 关联字段直接映射(Spring Data 自动 JOIN)
}
@Query("SELECT o.id AS id, o.status AS status, o.user.name AS userName FROM Order o")
List<OrderSummary> findAllSummaries();数据访问完整用法参见 数据访问,JPA 详见 JPA与Hibernate。
四、LazyInitializationException
在 Session/事务关闭后访问懒加载属性,Hibernate 会抛出:
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
// 触发场景
@Service
public class OrderService {
@Transactional
public Order getOrder(Long id) {
return orderRepository.findById(id).get();
// 事务 / Session 在方法返回后关闭
}
}
// Controller 层
Order order = orderService.getOrder(1L);
order.getUser().getName(); // Session 已关闭 → LazyInitializationException解决方案
(推荐)在 Service 层用 JOIN FETCH / EntityGraph 一次性加载所需数据:
@Transactional(readOnly = true)
public OrderDTO getOrderDetail(Long id) {
Order order = orderRepository.findByIdWithUser(id); // JOIN FETCH
return OrderDTO.from(order); // 在事务内完成所有数据访问
}Open Session in View(不推荐,但需了解):
Spring Boot 默认开启 spring.jpa.open-in-view=true,将 Session 生命周期延伸到整个 HTTP 请求,允许在 Controller/View 层触发懒加载。
# 关闭 OSIV(生产环境推荐)
spring:
jpa:
open-in-view: false关闭后所有懒加载必须在 @Transactional 方法内完成,强制将数据访问收敛到 Service 层,架构更清晰,也避免了意外的 N+1 查询。
五、Spring Data REST 与懒加载
Spring Data REST 序列化实体时,懒加载属性若 Session 已关闭会报错。推荐改用 Projection 或 DTO:
// Projection 自动过滤懒加载属性
@Projection(name = "summary", types = Order.class)
public interface OrderProjection {
Long getId();
OrderStatus getStatus();
// 不暴露 items 等懒加载集合
}六、选择策略
Bean 延迟加载
| 场景 | 建议 |
|---|---|
| 开发 / 测试环境快速启动 | 全局 lazy-initialization: true |
| 生产环境 | 默认饥饿加载,按需对重量级 Bean 加 @Lazy |
| 打破循环依赖 | 注入点加 @Lazy |
JPA 延迟加载
| 场景 | 建议 |
|---|---|
| 大多数查询不需要关联数据 | 保持 LAZY(默认),按需 JOIN FETCH |
| 确保每次必须加载 | 改 EAGER(谨慎,易引发性能问题) |
| 列表查询 + 关联数据 | JOIN FETCH 或 @BatchSize |
| 查询特定字段 | DTO Projection,绕过实体关联 |