001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.orm.hibernate5.support; 018 019import org.apache.commons.logging.Log; 020import org.apache.commons.logging.LogFactory; 021import org.hibernate.FlushMode; 022import org.hibernate.HibernateException; 023import org.hibernate.Session; 024import org.hibernate.SessionFactory; 025 026import org.springframework.dao.DataAccessException; 027import org.springframework.dao.DataAccessResourceFailureException; 028import org.springframework.lang.Nullable; 029import org.springframework.orm.hibernate5.SessionFactoryUtils; 030import org.springframework.orm.hibernate5.SessionHolder; 031import org.springframework.transaction.support.TransactionSynchronizationManager; 032import org.springframework.ui.ModelMap; 033import org.springframework.util.Assert; 034import org.springframework.web.context.request.AsyncWebRequestInterceptor; 035import org.springframework.web.context.request.WebRequest; 036import org.springframework.web.context.request.async.CallableProcessingInterceptor; 037import org.springframework.web.context.request.async.WebAsyncManager; 038import org.springframework.web.context.request.async.WebAsyncUtils; 039 040/** 041 * Spring web request interceptor that binds a Hibernate {@code Session} to the 042 * thread for the entire processing of the request. 043 * 044 * <p>This class is a concrete expression of the "Open Session in View" pattern, which 045 * is a pattern that allows for the lazy loading of associations in web views despite 046 * the original transactions already being completed. 047 * 048 * <p>This interceptor makes Hibernate Sessions available via the current thread, 049 * which will be autodetected by transaction managers. It is suitable for service layer 050 * transactions via {@link org.springframework.orm.hibernate5.HibernateTransactionManager} 051 * as well as for non-transactional execution (if configured appropriately). 052 * 053 * <p>In contrast to {@link OpenSessionInViewFilter}, this interceptor is configured 054 * in a Spring application context and can thus take advantage of bean wiring. 055 * 056 * <p><b>WARNING:</b> Applying this interceptor to existing logic can cause issues 057 * that have not appeared before, through the use of a single Hibernate 058 * {@code Session} for the processing of an entire request. In particular, the 059 * reassociation of persistent objects with a Hibernate {@code Session} has to 060 * occur at the very beginning of request processing, to avoid clashes with already 061 * loaded instances of the same objects. 062 * 063 * @author Juergen Hoeller 064 * @since 4.2 065 * @see OpenSessionInViewFilter 066 * @see OpenSessionInterceptor 067 * @see org.springframework.orm.hibernate5.HibernateTransactionManager 068 * @see TransactionSynchronizationManager 069 * @see SessionFactory#getCurrentSession() 070 */ 071public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor { 072 073 /** 074 * Suffix that gets appended to the {@code SessionFactory} 075 * {@code toString()} representation for the "participate in existing 076 * session handling" request attribute. 077 * @see #getParticipateAttributeName 078 */ 079 public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE"; 080 081 protected final Log logger = LogFactory.getLog(getClass()); 082 083 @Nullable 084 private SessionFactory sessionFactory; 085 086 087 /** 088 * Set the Hibernate SessionFactory that should be used to create Hibernate Sessions. 089 */ 090 public void setSessionFactory(@Nullable SessionFactory sessionFactory) { 091 this.sessionFactory = sessionFactory; 092 } 093 094 /** 095 * Return the Hibernate SessionFactory that should be used to create Hibernate Sessions. 096 */ 097 @Nullable 098 public SessionFactory getSessionFactory() { 099 return this.sessionFactory; 100 } 101 102 private SessionFactory obtainSessionFactory() { 103 SessionFactory sf = getSessionFactory(); 104 Assert.state(sf != null, "No SessionFactory set"); 105 return sf; 106 } 107 108 109 /** 110 * Open a new Hibernate {@code Session} according and bind it to the thread via the 111 * {@link TransactionSynchronizationManager}. 112 */ 113 @Override 114 public void preHandle(WebRequest request) throws DataAccessException { 115 String key = getParticipateAttributeName(); 116 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); 117 if (asyncManager.hasConcurrentResult() && applySessionBindingInterceptor(asyncManager, key)) { 118 return; 119 } 120 121 if (TransactionSynchronizationManager.hasResource(obtainSessionFactory())) { 122 // Do not modify the Session: just mark the request accordingly. 123 Integer count = (Integer) request.getAttribute(key, WebRequest.SCOPE_REQUEST); 124 int newCount = (count != null ? count + 1 : 1); 125 request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST); 126 } 127 else { 128 logger.debug("Opening Hibernate Session in OpenSessionInViewInterceptor"); 129 Session session = openSession(); 130 SessionHolder sessionHolder = new SessionHolder(session); 131 TransactionSynchronizationManager.bindResource(obtainSessionFactory(), sessionHolder); 132 133 AsyncRequestInterceptor asyncRequestInterceptor = 134 new AsyncRequestInterceptor(obtainSessionFactory(), sessionHolder); 135 asyncManager.registerCallableInterceptor(key, asyncRequestInterceptor); 136 asyncManager.registerDeferredResultInterceptor(key, asyncRequestInterceptor); 137 } 138 } 139 140 @Override 141 public void postHandle(WebRequest request, @Nullable ModelMap model) { 142 } 143 144 /** 145 * Unbind the Hibernate {@code Session} from the thread and close it). 146 * @see TransactionSynchronizationManager 147 */ 148 @Override 149 public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException { 150 if (!decrementParticipateCount(request)) { 151 SessionHolder sessionHolder = 152 (SessionHolder) TransactionSynchronizationManager.unbindResource(obtainSessionFactory()); 153 logger.debug("Closing Hibernate Session in OpenSessionInViewInterceptor"); 154 SessionFactoryUtils.closeSession(sessionHolder.getSession()); 155 } 156 } 157 158 private boolean decrementParticipateCount(WebRequest request) { 159 String participateAttributeName = getParticipateAttributeName(); 160 Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); 161 if (count == null) { 162 return false; 163 } 164 // Do not modify the Session: just clear the marker. 165 if (count > 1) { 166 request.setAttribute(participateAttributeName, count - 1, WebRequest.SCOPE_REQUEST); 167 } 168 else { 169 request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); 170 } 171 return true; 172 } 173 174 @Override 175 public void afterConcurrentHandlingStarted(WebRequest request) { 176 if (!decrementParticipateCount(request)) { 177 TransactionSynchronizationManager.unbindResource(obtainSessionFactory()); 178 } 179 } 180 181 /** 182 * Open a Session for the SessionFactory that this interceptor uses. 183 * <p>The default implementation delegates to the {@link SessionFactory#openSession} 184 * method and sets the {@link Session}'s flush mode to "MANUAL". 185 * @return the Session to use 186 * @throws DataAccessResourceFailureException if the Session could not be created 187 * @see FlushMode#MANUAL 188 */ 189 @SuppressWarnings("deprecation") 190 protected Session openSession() throws DataAccessResourceFailureException { 191 try { 192 Session session = obtainSessionFactory().openSession(); 193 session.setFlushMode(FlushMode.MANUAL); 194 return session; 195 } 196 catch (HibernateException ex) { 197 throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex); 198 } 199 } 200 201 /** 202 * Return the name of the request attribute that identifies that a request is 203 * already intercepted. 204 * <p>The default implementation takes the {@code toString()} representation 205 * of the {@code SessionFactory} instance and appends {@link #PARTICIPATE_SUFFIX}. 206 */ 207 protected String getParticipateAttributeName() { 208 return obtainSessionFactory().toString() + PARTICIPATE_SUFFIX; 209 } 210 211 private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) { 212 CallableProcessingInterceptor cpi = asyncManager.getCallableInterceptor(key); 213 if (cpi == null) { 214 return false; 215 } 216 ((AsyncRequestInterceptor) cpi).bindSession(); 217 return true; 218 } 219 220}