聊聊spring data jpa的OpenSessionInView

5次阅读

共计 8450 个字符,预计需要花费 22 分钟才能阅读完成。


本文主要研究一下 spring data jpa 的 OpenSessionInView
Open Session In View
Open Session In View 简称 OSIV,是为了解决在 mvc 的 controller 中使用了 hibernate 的 lazy load 的属性时没有 session 抛出的 LazyInitializationException 异常;对 hibernate 来说 ToMany 关系默认是延迟加载,而 ToOne 关系则默认是立即加载
JpaProperties
spring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java
@ConfigurationProperties(prefix = “spring.jpa”)
public class JpaProperties {

/**
* Additional native properties to set on the JPA provider.
*/
private Map<String, String> properties = new HashMap<>();

/**
* Mapping resources (equivalent to “mapping-file” entries in persistence.xml).
*/
private final List<String> mappingResources = new ArrayList<>();

/**
* Name of the target database to operate on, auto-detected by default. Can be
* alternatively set using the “Database” enum.
*/
private String databasePlatform;

/**
* Target database to operate on, auto-detected by default. Can be alternatively set
* using the “databasePlatform” property.
*/
private Database database;

/**
* Whether to initialize the schema on startup.
*/
private boolean generateDdl = false;

/**
* Whether to enable logging of SQL statements.
*/
private boolean showSql = false;

/**
* Register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the
* thread for the entire processing of the request.
*/
private Boolean openInView;

//……
}
JpaProperties 有一个配置项为 openInView(默认为 true),用于决定是否注册 OpenEntityManagerInViewInterceptor,它会一个请求线程绑定一个 JPA EntityManager
JpaBaseConfiguration
spring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java
@Configuration
@EnableConfigurationProperties(JpaProperties.class)
@Import(DataSourceInitializedPublisher.Registrar.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {
//……

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
@ConditionalOnMissingBean({OpenEntityManagerInViewInterceptor.class,
OpenEntityManagerInViewFilter.class })
@ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
@ConditionalOnProperty(prefix = “spring.jpa”, name = “open-in-view”,
havingValue = “true”, matchIfMissing = true)
protected static class JpaWebConfiguration {

// Defined as a nested config to ensure WebMvcConfigurerAdapter is not read when
// not on the classpath
@Configuration
protected static class JpaWebMvcConfiguration implements WebMvcConfigurer {

private static final Log logger = LogFactory
.getLog(JpaWebMvcConfiguration.class);

private final JpaProperties jpaProperties;

protected JpaWebMvcConfiguration(JpaProperties jpaProperties) {
this.jpaProperties = jpaProperties;
}

@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
if (this.jpaProperties.getOpenInView() == null) {
logger.warn(“spring.jpa.open-in-view is enabled by default. ”
+ “Therefore, database queries may be performed during view ”
+ “rendering. Explicitly configure ”
+ “spring.jpa.open-in-view to disable this warning”);
}
return new OpenEntityManagerInViewInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(openEntityManagerInViewInterceptor());
}

}

}

//……
}
JpaBaseConfiguration 里头有个 JpaWebMvcConfiguration 配置,在 web application 的类型是 Type.SERVLET 的时候,且 spring.jpa.open-in-view 不是 false 的时候注册 OpenEntityManagerInViewInterceptor,然后添加到 mvc 的 webRequestInterceptor 中
OpenEntityManagerInViewInterceptor
spring-orm-5.1.6.RELEASE-sources.jar!/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java
public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {

/**
* Suffix that gets appended to the EntityManagerFactory toString
* representation for the “participate in existing entity manager
* handling” request attribute.
* @see #getParticipateAttributeName
*/
public static final String PARTICIPATE_SUFFIX = “.PARTICIPATE”;

@Override
public void preHandle(WebRequest request) throws DataAccessException {
String key = getParticipateAttributeName();
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
if (asyncManager.hasConcurrentResult() && applyEntityManagerBindingInterceptor(asyncManager, key)) {
return;
}

EntityManagerFactory emf = obtainEntityManagerFactory();
if (TransactionSynchronizationManager.hasResource(emf)) {
// Do not modify the EntityManager: just mark the request accordingly.
Integer count = (Integer) request.getAttribute(key, WebRequest.SCOPE_REQUEST);
int newCount = (count != null ? count + 1 : 1);
request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST);
}
else {
logger.debug(“Opening JPA EntityManager in OpenEntityManagerInViewInterceptor”);
try {
EntityManager em = createEntityManager();
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(emf, emHolder);

AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(emf, emHolder);
asyncManager.registerCallableInterceptor(key, interceptor);
asyncManager.registerDeferredResultInterceptor(key, interceptor);
}
catch (PersistenceException ex) {
throw new DataAccessResourceFailureException(“Could not create JPA EntityManager”, ex);
}
}
}

@Override
public void postHandle(WebRequest request, @Nullable ModelMap model) {
}

@Override
public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
if (!decrementParticipateCount(request)) {
EntityManagerHolder emHolder = (EntityManagerHolder)
TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
logger.debug(“Closing JPA EntityManager in OpenEntityManagerInViewInterceptor”);
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
}
}

private boolean decrementParticipateCount(WebRequest request) {
String participateAttributeName = getParticipateAttributeName();
Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
if (count == null) {
return false;
}
// Do not modify the Session: just clear the marker.
if (count > 1) {
request.setAttribute(participateAttributeName, count – 1, WebRequest.SCOPE_REQUEST);
}
else {
request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
}
return true;
}

@Override
public void afterConcurrentHandlingStarted(WebRequest request) {
if (!decrementParticipateCount(request)) {
TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
}
}

/**
* Return the name of the request attribute that identifies that a request is
* already filtered. Default implementation takes the toString representation
* of the EntityManagerFactory instance and appends “.FILTERED”.
* @see #PARTICIPATE_SUFFIX
*/
protected String getParticipateAttributeName() {
return obtainEntityManagerFactory().toString() + PARTICIPATE_SUFFIX;
}

private boolean applyEntityManagerBindingInterceptor(WebAsyncManager asyncManager, String key) {
CallableProcessingInterceptor cpi = asyncManager.getCallableInterceptor(key);
if (cpi == null) {
return false;
}
((AsyncRequestInterceptor) cpi).bindEntityManager();
return true;
}

}

OpenEntityManagerInViewInterceptor 继承了抽象类 EntityManagerFactoryAccessor,实现了 AsyncWebRequestInterceptor 接口 (定义了 afterConcurrentHandlingStarted 方法);AsyncWebRequestInterceptor 继承了 WebRequestInterceptor(定义了 preHandle、postHandle、afterCompletion 方法)
preHandle 方法会判断当前线程是否有 EntityManagerFactory,如果有的话则会在 request 的 attribute 中维护 count;如果没有的话则会创建 EntityManager(openSession),然后使用 TransactionSynchronizationManager.bindResource 进行绑定
afterCompletion 方法会先对 request attribute 中的 count 进行递减 (如果有的话),当 count 为 0 的时候移除该 attribute;如果 request 没有 count 则使用 TransactionSynchronizationManager.unbindResource 进行解绑,然后关闭 EntityManager;异步的 afterConcurrentHandlingStarted 方法也类似,主要是进行 unbind 操作

小结

对 hibernate 来说 ToMany 关系默认是延迟加载,而 ToOne 关系则默认是立即加载;而在 mvc 的 controller 中脱离了 persisent contenxt,于是 entity 变成了 detached 状态,这个时候要使用延迟加载的属性时就会抛出 LazyInitializationException 异常,而 Open Session In View 指在解决这个问题
JpaBaseConfiguration 里头有个 JpaWebMvcConfiguration 配置,在 web application 的类型是 Type.SERVLET 的时候,且 spring.jpa.open-in-view 不是 false 的时候注册 OpenEntityManagerInViewInterceptor,然后添加到 mvc 的 webRequestInterceptor 中
OpenEntityManagerInViewInterceptor 的 preHandle 方法会判断当前线程是否有 EntityManagerFactory,如果没有则会创建 EntityManager(openSession),然后使用 TransactionSynchronizationManager.bindResource 绑定到当前线程;afterCompletion 方法会使用 TransactionSynchronizationManager.unbindResource 进行解绑,然后关闭 EntityManager

通过 OSIV 技术来解决 LazyInitialization 问题会导致 open 的 session 生命周期过长,它贯穿整个 request,在 view 渲染完之后才能关闭 session 释放数据库连接;另外 OSIV 将 service 层的技术细节暴露到了 controller 层,造成了一定的耦合,因而不建议开启,对应的解决方案就是在 controller 层中使用 dto,而非 detached 状态的 entity,所需的数据不再依赖延时加载,在组装 dto 的时候根据需要显式查询
doc

Eager/Lazy Loading In Hibernate
Open Session in View
Open Session In View 模式的基本常识
The Open Session In View Anti-Pattern
Open Session In View Design Tradeoffs
Why is Hibernate Open Session in View considered a bad practice?
Log a warning on startup when spring.jpa.open-in-view is enabled but user has not explicitly opted in #7107
SPRING BOOT BEST PRACTICE – DISABLE OSIV TO START RECEIVING LAZYINITIALIZATIONEXCEPTION WARNINGS AGAIN

正文完
 0