1. 程式人生 > >聊聊spring data jpa的OpenSessionInView

聊聊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