聊聊spring data jpa的OpenSessionInView

本文主要研究一下spring data jpa的OpenSessionInViewjava

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.javagit

@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.javagithub

@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.javaweb

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

相關文章
相關標籤/搜索