延迟加载

延迟加载(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说明
@ManyToOneEAGER立即加载关联对象
@OneToOneEAGER立即加载关联对象
@OneToManyLAZY延迟加载集合
@ManyToManyLAZY延迟加载集合
@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,绕过实体关联

相关链接