聊聊spring data jpa的OpenSessionInView


本文主要研究一下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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理