Java tutorial
/* * Hibernate, Relational Persistence for Idiomatic Java * * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.context.internal; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.transaction.Synchronization; import org.hibernate.ConnectionReleaseMode; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.context.spi.AbstractCurrentSessionContext; import org.hibernate.engine.jdbc.LobCreationContext; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.resource.transaction.spi.TransactionStatus; import org.jboss.logging.Logger; /** * A {@link org.hibernate.context.spi.CurrentSessionContext} impl which scopes the notion of current * session by the current thread of execution. Unlike the JTA counterpart, threads do not give us a nice * hook to perform any type of cleanup making it questionable for this impl to actually generate Session * instances. In the interest of usability, it was decided to have this default impl actually generate * a session upon first request and then clean it up after the {@link org.hibernate.Transaction} * associated with that session is committed/rolled-back. In order for ensuring that happens, the * sessions generated here are unusable until after {@link Session#beginTransaction()} has been * called. If <tt>close()</tt> is called on a session managed by this class, it will be automatically * unbound. * * Additionally, the static {@link #bind} and {@link #unbind} methods are provided to allow application * code to explicitly control opening and closing of these sessions. This, with some from of interception, * is the preferred approach. It also allows easy framework integration and one possible approach for * implementing long-sessions. * * The {@link #buildOrObtainSession}, {@link #isAutoCloseEnabled}, {@link #isAutoFlushEnabled}, * {@link #getConnectionReleaseMode}, and {@link #buildCleanupSynch} methods are all provided to allow easy * subclassing (for long-running session scenarios, for example). * * @author Steve Ebersole * @author Sanne Grinovero */ public class ThreadLocalSessionContext extends AbstractCurrentSessionContext { private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class, ThreadLocalSessionContext.class.getName()); private static final Class[] SESSION_PROXY_INTERFACES = new Class[] { Session.class, SessionImplementor.class, EventSource.class, LobCreationContext.class }; /** * A ThreadLocal maintaining current sessions for the given execution thread. * The actual ThreadLocal variable is a java.util.Map to account for * the possibility for multiple SessionFactory instances being used during execution * of the given thread. */ private static final ThreadLocal<Map<SessionFactory, Session>> CONTEXT_TL = ThreadLocal .withInitial(HashMap::new); /** * Constructs a ThreadLocal * * @param factory The factory this context will service */ public ThreadLocalSessionContext(SessionFactoryImplementor factory) { super(factory); } @Override public final Session currentSession() throws HibernateException { Session current = existingSession(factory()); if (current == null) { current = buildOrObtainSession(); // register a cleanup sync current.getTransaction().registerSynchronization(buildCleanupSynch()); // wrap the session in the transaction-protection proxy if (needsWrapping(current)) { current = wrap(current); } // then bind it doBind(current, factory()); } else { validateExistingSession(current); } return current; } private boolean needsWrapping(Session session) { // try to make sure we don't wrap and already wrapped session if (Proxy.isProxyClass(session.getClass())) { final InvocationHandler invocationHandler = Proxy.getInvocationHandler(session); if (invocationHandler != null && TransactionProtectionWrapper.class.isInstance(invocationHandler)) { return false; } } return true; } /** * Getter for property 'factory'. * * @return Value for property 'factory'. */ protected SessionFactoryImplementor getFactory() { return factory(); } /** * Strictly provided for sub-classing purposes; specifically to allow long-session * support. * <p/> * This implementation always just opens a new session. * * @return the built or (re)obtained session. */ @SuppressWarnings("deprecation") protected Session buildOrObtainSession() { return baseSessionBuilder().autoClose(isAutoCloseEnabled()) .connectionReleaseMode(getConnectionReleaseMode()).flushBeforeCompletion(isAutoFlushEnabled()) .openSession(); } protected CleanupSync buildCleanupSynch() { return new CleanupSync(factory()); } /** * Mainly for subclass usage. This impl always returns true. * * @return Whether or not the the session should be closed by transaction completion. */ protected boolean isAutoCloseEnabled() { return true; } /** * Mainly for subclass usage. This impl always returns true. * * @return Whether or not the the session should be flushed prior transaction completion. */ protected boolean isAutoFlushEnabled() { return true; } /** * Mainly for subclass usage. This impl always returns after_transaction. * * @return The connection release mode for any built sessions. */ protected ConnectionReleaseMode getConnectionReleaseMode() { return factory().getSettings().getConnectionReleaseMode(); } protected Session wrap(Session session) { final TransactionProtectionWrapper wrapper = new TransactionProtectionWrapper(session); final Session wrapped = (Session) Proxy.newProxyInstance(Session.class.getClassLoader(), SESSION_PROXY_INTERFACES, wrapper); // yick! need this for proper serialization/deserialization handling... wrapper.setWrapped(wrapped); return wrapped; } /** * Associates the given session with the current thread of execution. * * @param session The session to bind. */ public static void bind(org.hibernate.Session session) { final SessionFactory factory = session.getSessionFactory(); doBind(session, factory); } private static void terminateOrphanedSession(Session orphan) { if (orphan != null) { LOG.alreadySessionBound(); try { final Transaction orphanTransaction = orphan.getTransaction(); if (orphanTransaction != null && orphanTransaction.getStatus() == TransactionStatus.ACTIVE) { try { orphanTransaction.rollback(); } catch (Throwable t) { LOG.debug("Unable to rollback transaction for orphaned session", t); } } } finally { try { orphan.close(); } catch (Throwable t) { LOG.debug("Unable to close orphaned session", t); } } } } /** * Disassociates a previously bound session from the current thread of execution. * * @param factory The factory for which the session should be unbound. * @return The session which was unbound. */ public static Session unbind(SessionFactory factory) { return doUnbind(factory, true); } private static Session existingSession(SessionFactory factory) { return sessionMap().get(factory); } protected static Map<SessionFactory, Session> sessionMap() { return CONTEXT_TL.get(); } @SuppressWarnings({ "unchecked" }) private static void doBind(org.hibernate.Session session, SessionFactory factory) { Session orphanedPreviousSession = sessionMap().put(factory, session); terminateOrphanedSession(orphanedPreviousSession); } private static Session doUnbind(SessionFactory factory, boolean releaseMapIfEmpty) { final Map<SessionFactory, Session> sessionMap = sessionMap(); final Session session = sessionMap.remove(factory); if (releaseMapIfEmpty && sessionMap.isEmpty()) { //Do not use set(null) as it would prevent the initialValue to be invoked again in case of need. CONTEXT_TL.remove(); } return session; } /** * Transaction sync used for cleanup of the internal session map. */ protected static class CleanupSync implements Synchronization, Serializable { protected final SessionFactory factory; public CleanupSync(SessionFactory factory) { this.factory = factory; } @Override public void beforeCompletion() { } @Override public void afterCompletion(int i) { unbind(factory); } } private class TransactionProtectionWrapper implements InvocationHandler, Serializable { private final Session realSession; private Session wrappedSession; public TransactionProtectionWrapper(Session realSession) { this.realSession = realSession; } @Override @SuppressWarnings("SimplifiableIfStatement") public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { final String methodName = method.getName(); // first check methods calls that we handle completely locally: if ("equals".equals(methodName) && method.getParameterCount() == 1) { if (args[0] == null || !Proxy.isProxyClass(args[0].getClass())) { return false; } return this.equals(Proxy.getInvocationHandler(args[0])); } else if ("hashCode".equals(methodName) && method.getParameterCount() == 0) { return this.hashCode(); } else if ("toString".equals(methodName) && method.getParameterCount() == 0) { return String.format(Locale.ROOT, "ThreadLocalSessionContext.TransactionProtectionWrapper[%s]", realSession); } // then check method calls that we need to delegate to the real Session try { // If close() is called, guarantee unbind() if ("close".equals(methodName)) { unbind(realSession.getSessionFactory()); } else if ("getStatistics".equals(methodName) || "isOpen".equals(methodName) || "getListeners".equals(methodName)) { // allow these to go through the the real session no matter what LOG.tracef("Allowing invocation [%s] to proceed to real session", methodName); } else if (!realSession.isOpen()) { // essentially, if the real session is closed allow any // method call to pass through since the real session // will complain by throwing an appropriate exception; // NOTE that allowing close() above has the same basic effect, // but we capture that there simply to doAfterTransactionCompletion the unbind... LOG.tracef("Allowing invocation [%s] to proceed to real (closed) session", methodName); } else if (realSession.getTransaction().getStatus() != TransactionStatus.ACTIVE) { // limit the methods available if no transaction is active if ("beginTransaction".equals(methodName) || "getTransaction".equals(methodName) || "isTransactionInProgress".equals(methodName) || "setFlushMode".equals(methodName) || "setHibernateFlushMode".equals(methodName) || "getFactory".equals(methodName) || "getSessionFactory".equals(methodName) || "getTenantIdentifier".equals(methodName)) { LOG.tracef("Allowing invocation [%s] to proceed to real (non-transacted) session", methodName); } else if ("reconnect".equals(methodName) || "disconnect".equals(methodName)) { // allow these (deprecated) methods to pass through LOG.tracef( "Allowing invocation [%s] to proceed to real (non-transacted) session - deprecated methods", methodName); } else { throw new HibernateException("Calling method '" + methodName + "' is not valid without an active transaction (Current status: " + realSession.getTransaction().getStatus() + ")"); } } LOG.tracef("Allowing proxy invocation [%s] to proceed to real session", methodName); return method.invoke(realSession, args); } catch (InvocationTargetException e) { if (e.getTargetException() instanceof RuntimeException) { throw (RuntimeException) e.getTargetException(); } throw e; } } /** * Setter for property 'wrapped'. * * @param wrapped Value to set for property 'wrapped'. */ public void setWrapped(Session wrapped) { this.wrappedSession = wrapped; } // serialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private void writeObject(ObjectOutputStream oos) throws IOException { // if a ThreadLocalSessionContext-bound session happens to get // serialized, to be completely correct, we need to make sure // that unbinding of that session occurs. oos.defaultWriteObject(); if (existingSession(factory()) == wrappedSession) { unbind(factory()); } } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // on the inverse, it makes sense that if a ThreadLocalSessionContext- // bound session then gets deserialized to go ahead and re-bind it to // the ThreadLocalSessionContext session map. ois.defaultReadObject(); realSession.getTransaction().registerSynchronization(buildCleanupSynch()); doBind(wrappedSession, factory()); } } }