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