如何在Spring中选择性禁用OSIV过滤器?大型应用渐进式迁移
渐进式禁用Spring Boot的Open Session In View(OSIV)方案
如果直接全局关闭OSIV(设置spring.jpa.open-in-view=false),大型应用很容易因为懒加载问题全面崩溃。下面是几种按端点逐步禁用OSIV的落地方法,适配渐进式迁移需求:
方法一:自定义OSIV过滤器,按路径精准控制
Spring Boot默认的OSIV由OpenEntityManagerInViewFilter实现,我们可以自定义这个过滤器的逻辑,让它对指定路径跳过OSIV处理:
- 先保持全局OSIV开启(
spring.jpa.open-in-view=true),避免现有代码报错。 - 排除自动配置的OSIV过滤器,然后注册自定义版本:
@SpringBootApplication(exclude = OpenEntityManagerInViewAutoConfiguration.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
- 编写自定义OSIV过滤器:
@Configuration public class CustomOsivConfig { @Bean public FilterRegistrationBean<OpenEntityManagerInViewFilter> customOpenEntityManagerInViewFilter() { FilterRegistrationBean<OpenEntityManagerInViewFilter> registrationBean = new FilterRegistrationBean<>(); OpenEntityManagerInViewFilter customFilter = new OpenEntityManagerInViewFilter() { @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { // 匹配需要禁用OSIV的端点路径,比如/v2开头的接口 String requestUri = request.getRequestURI(); return requestUri.startsWith("/api/v2/") || requestUri.equals("/api/admin/dashboard"); } }; registrationBean.setFilter(customFilter); // 保持和默认OSIV过滤器相同的优先级 registrationBean.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return registrationBean; } }
注意:shouldNotFilter返回true时,该请求不会应用OSIV逻辑,即禁用OSIV;返回false则保持OSIV开启。
方法二:用AOP注解标记需要禁用OSIV的方法
如果需要更细粒度的控制(比如单个控制器方法),可以用AOP配合自定义注解实现:
- 定义一个标记用的注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DisableOsiv { }
- 编写AOP切面,拦截标记了注解的方法,手动关闭OSIV上下文:
@Aspect @Component public class OsivDisablingAspect { private final EntityManagerFactory entityManagerFactory; public OsivDisablingAspect(EntityManagerFactory entityManagerFactory) { this.entityManagerFactory = entityManagerFactory; } // 切点:匹配标记@DisableOsiv的方法,或者指定包下的所有控制器方法 @Pointcut("@annotation(com.example.annotation.DisableOsiv) || within(com.example.controller.v2..*)") public void osivDisabledMethods() {} @Around("osivDisabledMethods()") public Object handleOsivDisabled(ProceedingJoinPoint joinPoint) throws Throwable { // 解绑当前线程绑定的EntityManager(OSIV默认会绑定) EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(entityManagerFactory); boolean wasOsivActive = false; if (emHolder != null && emHolder.getEntityManager().isOpen()) { wasOsivActive = true; TransactionSynchronizationManager.unbindResource(entityManagerFactory); emHolder.getEntityManager().close(); } try { // 执行目标方法 return joinPoint.proceed(); } finally { // 如果之前OSIV是激活状态,请求结束后无需重新绑定,容器会自动清理 if (wasOsivActive) { EntityManager newEm = entityManagerFactory.createEntityManager(); TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(newEm)); } } } }
- 在需要禁用OSIV的控制器方法上标记注解:
@RestController @RequestMapping("/api/v2/users") public class UserV2Controller { @GetMapping("/{id}") @DisableOsiv public UserDTO getUser(@PathVariable Long id) { // 这里必须确保Service层已经加载完所有需要的关联数据,避免懒加载异常 return userService.getUserWithAllAssociations(id); } }
配套:修复禁用OSIV后的懒加载问题
不管用哪种方法禁用OSIV,都必须同步修改对应端点的业务代码,确保所有需要的关联数据在Service层提前加载完成:
- 在Repository查询中使用
JOIN FETCH强制加载关联实体:
@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id") User findByIdWithRoles(@Param("id") Long id);
- 避免在Controller/视图层调用实体的懒加载方法;
- 必要时使用DTO封装数据,在Service层完成实体到DTO的转换。
渐进式迁移步骤
- 先全局保持OSIV开启,避免现有代码崩溃;
- 选择上述一种方法,针对小范围端点(比如新开发的v2接口)禁用OSIV;
- 修复这些端点的懒加载问题,测试验证功能正常;
- 逐步扩大禁用范围,覆盖更多旧端点,每次验证后再推进;
- 所有端点都适配完成后,全局设置
spring.jpa.open-in-view=false,彻底关闭OSIV。
内容的提问来源于stack exchange,提问作者pakman
相关产品推荐
相关产品推荐

