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}