【问题标题】:Avoid hibernate session destruction between methods避免方法之间的休眠会话破坏
【发布时间】:2015-10-03 04:48:12
【问题描述】:

我正在编写一个 spring boot 应用程序(spring boot 1.2.2、spring 4.1.5),它本质上是一个 REST API。我已经设置了 spring security 以在每个 HTTP 请求时自动加载用户、检查授权等。用户存储在一个数据库中,我使用hibernate来访问这个数据库中的所有数据。

简而言之,问题是:我在 PreFilter 期间对每个 HTTP 请求使用休眠模式从我的数据库中加载一个 user 对象。我后来在我的 RestController 的参数中收到了这个对象,但它不再附加到任何休眠会话。因此,如果不先从数据库中重新读取它,我就无法访问用户对象中的任何惰性集合。

问题是:有没有办法在 PreFilter 期间启动休眠会话并保持它直到 HTTP 请求完成?

长版,带代码: 首先,我设置了 spring 安全性,因此它会加载我的 User 对象作为授权的一部分:
安全配置:

@Configuration
@Component
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    ...
}

用户详情服务,从数据库中加载用户:

@Service
@Transactional
public class DomainUserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null)
            throw new UsernameNotFoundException("User '" + username + "' not found");
        return user;
    }
}

我还想将 User 对象注入到控制器方法中,所以我这样做:

@PreAuthorize("hasAuthority('VIEW_ACCOUNT_INFO')")
@Transactional(readOnly = true)
@RequestMapping(value = "/user/api-keys/list", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public KeysResponse getApiKeys(
        @AuthenticationPrincipal User user
) {
    return new KeysResponse(user);
}

这很好用。但是,如果我尝试延迟加载与用户相关的集合,则会出现异常:

@Table(name = "users")
public class User implements UserDetails {
    ...
    @Column
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<ApiKey> keys;
    ...
}

user.getKeys(); <-- Exception here

failed to lazily initialize a collection of role: com.sebbia.pushwit.core.domain.User.keys, could not initialize proxy - no Session

发生这种情况是因为在 PreFilter 期间加载用户对象后关闭了 hiberante 会话,现在 user 变量已分离。为了再次附加它,我必须从数据库中重新加载它:

@PreAuthorize("hasAuthority('MANAGE_ACCOUNT_INFO')")
@Transactional(readOnly = false)
@RequestMapping(value = "/user/api-keys/add", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public KeysResponse addApiKey(
        @RequestParam(required = true) String name,
        @RequestParam(required = true) Role role,
        @AuthenticationPrincipal User user
) throws ApiKeyAlreadyExistsException {
    // I have to re-attach user object to new session
    Session session = (Session) entityManager.getDelegate();
    user = (User) session.merge(user);

    // Now I can change user or load any of it's collections
    ...
}

这可行,但这似乎容易出错且不必要。我实际上是从数据库中加载user 两次。考虑到我刚刚在之前的方法DomainUserDetailsService.loadUserByUsername 中加载了它,这似乎是一种浪费。

有没有办法在 PreFilter 期间启动休眠会话并保持它直到 HTTP 请求完成?

【问题讨论】:

  • 第一次加载用户时为什么不考虑显式加载api密钥集合?或者另一种选择是在需要时仅为给定用户加载 api 密钥集合。无论如何,您不必重新加载用户实体。如果您需要更多解释,请告诉我。
  • @PavlaNováková 感谢您的建议,这是解决问题的好方法。但是,我仍然更喜欢允许我以通常方式访问变量的解决方案,例如 user.getKeys()。
  • 使用我建议的解决方案,您当然可以作为 user.getKeys() 访问给定用户的 api 密钥集合 - 在第一种情况下,您可以初始化调用 user.getKeys().size 的集合() 例如在 loadUserByUsername 方法中,然后像往常一样访问集合 - user.getKeys() 没有得到 LazyInitializationException ,在第二种情况下(稍后加载显式密钥集合),方法逻辑将是:通过给定的用户 id 加载 api 密钥,然后只需调用 user.setKeys(loadedKeys) 并访问它们 user.getKeys()。在我看来,公开会议是不好的做法。

标签: java spring hibernate spring-security spring-boot


【解决方案1】:

有每个请求的会话模式。查看here 的描述。如果您使用的是 spring,则有一个 filter 实现。

请注意,它的使用是有争议的:Why is Hibernate Open Session in View considered a bad practice?

【讨论】:

    【解决方案2】:

    经过一番考虑,我选择了 user1675642 提供的答案:我决定对每个 HTTP 请求使用单个休眠会话(可能包含嵌套会话)。对于我的特殊情况,我没有看到这种方法有任何问题,因为我总是为每个请求打开数据库连接,所以打开堆栈跟踪的几个方法并没有什么区别(实际上它确实有区别,因为我不必打开它两次)。

    但是,我无法使用框架提供的OpenSessionInViewFilter。我收到“java.lang.IllegalArgumentException:ServletContext 不能为空”异常。我无法弄清楚我的配置的哪一部分导致了这个问题,所以我继续编写自己的实现:

    首先,您必须声明以下过滤器:

    @Component
    public class OpenSessionFilter extends OncePerRequestFilter {
        @Autowired
        OpenSessionService sessionService;
    
        @Override
        protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
            sessionService.doFilterInTransaction(request, response, filterChain);
        }
    }
    

    然后,您必须声明以下服务:

    @Service
    public class OpenSessionService {
        @Transactional
        public void doFilterInTransaction(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            filterChain.doFilter(request, response);
        }
    }
    

    但是,这回答了我的问题,但并没有解决我的问题。问题是,Spring Security 缓存了身份验证主体。这意味着身份验证过滤器只会偶尔从数据库加载用户。这意味着,来自 Spring Security 的 User 对象仍然可以不附加到休眠上下文(因为它是在先前的请求之一期间创建的)。这迫使我围绕用户对象创建一个包装器,该包装器将在必要时手动将用户对象重新附加到新的休眠会话。

    【讨论】:

    • 感谢您提供这些过滤器,它们与来自 stackoverflow.com/a/26697333 的 RefreshingUserDetailsS​​ecurityContextRepository 一起帮助解决了我们的要求。我们的应用程序非常以用户为中心,因此大多数控制器方法接收一个用户对象作为@AuthenticationPrincipal。在 RefreshingUserDetailsS​​ecurityContextRepository 的帮助下,它会在每次页面加载时刷新,从而允许实时权限更改。与这些过滤器一起,实体被正确绑定到正确的 Jpa-Session,并且可以直接在控制器或其视图中进行处理。
    猜你喜欢
    • 2011-02-03
    • 2023-03-11
    • 2014-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-17
    • 2012-06-02
    相关资源
    最近更新 更多