001/*
002 * Copyright 2002-2016 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 java.io.IOException;
020import javax.servlet.FilterChain;
021import javax.servlet.ServletException;
022import javax.servlet.http.HttpServletRequest;
023import javax.servlet.http.HttpServletResponse;
024
025import org.hibernate.FlushMode;
026import org.hibernate.HibernateException;
027import org.hibernate.Session;
028import org.hibernate.SessionFactory;
029
030import org.springframework.dao.DataAccessResourceFailureException;
031import org.springframework.orm.hibernate5.SessionFactoryUtils;
032import org.springframework.orm.hibernate5.SessionHolder;
033import org.springframework.transaction.support.TransactionSynchronizationManager;
034import org.springframework.web.context.WebApplicationContext;
035import org.springframework.web.context.request.async.WebAsyncManager;
036import org.springframework.web.context.request.async.WebAsyncUtils;
037import org.springframework.web.context.support.WebApplicationContextUtils;
038import org.springframework.web.filter.OncePerRequestFilter;
039
040/**
041 * Servlet Filter that binds a Hibernate Session to the thread for the entire
042 * processing of the request. Intended for the "Open Session in View" pattern,
043 * i.e. to allow for lazy loading in web views despite the original transactions
044 * already being completed.
045 *
046 * <p>This filter makes Hibernate Sessions available via the current thread, which
047 * will be autodetected by transaction managers. It is suitable for service layer
048 * transactions via {@link org.springframework.orm.hibernate5.HibernateTransactionManager}
049 * as well as for non-transactional execution (if configured appropriately).
050 *
051 * <p><b>NOTE</b>: This filter will by default <i>not</i> flush the Hibernate Session,
052 * with the flush mode set to {@code FlushMode.NEVER}. It assumes to be used
053 * in combination with service layer transactions that care for the flushing: The
054 * active transaction manager will temporarily change the flush mode to
055 * {@code FlushMode.AUTO} during a read-write transaction, with the flush
056 * mode reset to {@code FlushMode.NEVER} at the end of each transaction.
057 *
058 * <p><b>WARNING:</b> Applying this filter to existing logic can cause issues that
059 * have not appeared before, through the use of a single Hibernate Session for the
060 * processing of an entire request. In particular, the reassociation of persistent
061 * objects with a Hibernate Session has to occur at the very beginning of request
062 * processing, to avoid clashes with already loaded instances of the same objects.
063 *
064 * <p>Looks up the SessionFactory in Spring's root web application context.
065 * Supports a "sessionFactoryBeanName" filter init-param in {@code web.xml};
066 * the default bean name is "sessionFactory".
067 *
068 * @author Juergen Hoeller
069 * @since 4.2
070 * @see #lookupSessionFactory
071 * @see OpenSessionInViewInterceptor
072 * @see OpenSessionInterceptor
073 * @see org.springframework.orm.hibernate5.HibernateTransactionManager
074 * @see TransactionSynchronizationManager
075 * @see SessionFactory#getCurrentSession()
076 */
077public class OpenSessionInViewFilter extends OncePerRequestFilter {
078
079        public static final String DEFAULT_SESSION_FACTORY_BEAN_NAME = "sessionFactory";
080
081        private String sessionFactoryBeanName = DEFAULT_SESSION_FACTORY_BEAN_NAME;
082
083
084        /**
085         * Set the bean name of the SessionFactory to fetch from Spring's
086         * root application context. Default is "sessionFactory".
087         * @see #DEFAULT_SESSION_FACTORY_BEAN_NAME
088         */
089        public void setSessionFactoryBeanName(String sessionFactoryBeanName) {
090                this.sessionFactoryBeanName = sessionFactoryBeanName;
091        }
092
093        /**
094         * Return the bean name of the SessionFactory to fetch from Spring's
095         * root application context.
096         */
097        protected String getSessionFactoryBeanName() {
098                return this.sessionFactoryBeanName;
099        }
100
101
102        /**
103         * Returns "false" so that the filter may re-bind the opened Hibernate
104         * {@code Session} to each asynchronously dispatched thread and postpone
105         * closing it until the very last asynchronous dispatch.
106         */
107        @Override
108        protected boolean shouldNotFilterAsyncDispatch() {
109                return false;
110        }
111
112        /**
113         * Returns "false" so that the filter may provide a Hibernate
114         * {@code Session} to each error dispatches.
115         */
116        @Override
117        protected boolean shouldNotFilterErrorDispatch() {
118                return false;
119        }
120
121        @Override
122        protected void doFilterInternal(
123                        HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
124                        throws ServletException, IOException {
125
126                SessionFactory sessionFactory = lookupSessionFactory(request);
127                boolean participate = false;
128
129                WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
130                String key = getAlreadyFilteredAttributeName();
131
132                if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
133                        // Do not modify the Session: just set the participate flag.
134                        participate = true;
135                }
136                else {
137                        boolean isFirstRequest = !isAsyncDispatch(request);
138                        if (isFirstRequest || !applySessionBindingInterceptor(asyncManager, key)) {
139                                logger.debug("Opening Hibernate Session in OpenSessionInViewFilter");
140                                Session session = openSession(sessionFactory);
141                                SessionHolder sessionHolder = new SessionHolder(session);
142                                TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
143
144                                AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(sessionFactory, sessionHolder);
145                                asyncManager.registerCallableInterceptor(key, interceptor);
146                                asyncManager.registerDeferredResultInterceptor(key, interceptor);
147                        }
148                }
149
150                try {
151                        filterChain.doFilter(request, response);
152                }
153
154                finally {
155                        if (!participate) {
156                                SessionHolder sessionHolder =
157                                                (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory);
158                                if (!isAsyncStarted(request)) {
159                                        logger.debug("Closing Hibernate Session in OpenSessionInViewFilter");
160                                        SessionFactoryUtils.closeSession(sessionHolder.getSession());
161                                }
162                        }
163                }
164        }
165
166        /**
167         * Look up the SessionFactory that this filter should use,
168         * taking the current HTTP request as argument.
169         * <p>The default implementation delegates to the {@link #lookupSessionFactory()}
170         * variant without arguments.
171         * @param request the current request
172         * @return the SessionFactory to use
173         */
174        protected SessionFactory lookupSessionFactory(HttpServletRequest request) {
175                return lookupSessionFactory();
176        }
177
178        /**
179         * Look up the SessionFactory that this filter should use.
180         * <p>The default implementation looks for a bean with the specified name
181         * in Spring's root application context.
182         * @return the SessionFactory to use
183         * @see #getSessionFactoryBeanName
184         */
185        protected SessionFactory lookupSessionFactory() {
186                if (logger.isDebugEnabled()) {
187                        logger.debug("Using SessionFactory '" + getSessionFactoryBeanName() + "' for OpenSessionInViewFilter");
188                }
189                WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
190                return wac.getBean(getSessionFactoryBeanName(), SessionFactory.class);
191        }
192
193        /**
194         * Open a Session for the SessionFactory that this filter uses.
195         * <p>The default implementation delegates to the {@link SessionFactory#openSession}
196         * method and sets the {@link Session}'s flush mode to "MANUAL".
197         * @param sessionFactory the SessionFactory that this filter uses
198         * @return the Session to use
199         * @throws DataAccessResourceFailureException if the Session could not be created
200         * @see FlushMode#MANUAL
201         */
202        @SuppressWarnings("deprecation")
203        protected Session openSession(SessionFactory sessionFactory) throws DataAccessResourceFailureException {
204                try {
205                        Session session = sessionFactory.openSession();
206                        session.setFlushMode(FlushMode.MANUAL);
207                        return session;
208                }
209                catch (HibernateException ex) {
210                        throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex);
211                }
212        }
213
214        private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) {
215                if (asyncManager.getCallableInterceptor(key) == null) {
216                        return false;
217                }
218                ((AsyncRequestInterceptor) asyncManager.getCallableInterceptor(key)).bindSession();
219                return true;
220        }
221
222}