com.peterphi.std.guice.hibernate.module.TransactionMethodInterceptor.java Source code

Java tutorial

Introduction

Here is the source code for com.peterphi.std.guice.hibernate.module.TransactionMethodInterceptor.java

Source

package com.peterphi.std.guice.hibernate.module;

/*
 * Based on warp-persist HibernateLocalTxnInterceptor which is originally
 * 
 * Copyright (C) 2008 Wideplay Interactive.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.inject.Provider;
import com.peterphi.std.guice.common.metrics.GuiceMetricNames;
import com.peterphi.std.guice.database.annotation.Transactional;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.log4j.Logger;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.Transaction;

import java.lang.reflect.Method;

/**
 * A Guice AOP interceptor for methods annotated with the {@link Transactional} annotation to transparently start and
 * commit/rollback a Hibernate transaction
 */
class TransactionMethodInterceptor implements MethodInterceptor {
    private static final Logger log = Logger.getLogger(TransactionMethodInterceptor.class);

    private final Provider<Session> sessionProvider;

    private final Timer calls;
    private final Timer transactionStartedCalls;
    private final Meter errorRollbacks;
    private final Meter commitFailures;

    public TransactionMethodInterceptor(Provider<Session> sessionProvider, MetricRegistry metrics) {
        this.sessionProvider = sessionProvider;

        this.calls = metrics.timer(GuiceMetricNames.TRANSACTION_CALLS_TIMER);
        this.transactionStartedCalls = metrics.timer(GuiceMetricNames.TRANSACTION_OWNER_CALLS_TIMER);
        this.errorRollbacks = metrics.meter(GuiceMetricNames.TRANSACTION_ERROR_ROLLBACK_METER);
        this.commitFailures = metrics.meter(GuiceMetricNames.TRANSACTION_COMMIT_FAILURE_METER);
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        final Session session = sessionProvider.get();

        Timer.Context callTimer = calls.time();

        try {
            if (session.getTransaction().isActive()) {
                // allow silent joining of enclosing transactional methods (NOTE: this ignores the current method's txn-al settings)
                if (log.isTraceEnabled())
                    log.trace("Joining existing transaction to call " + invocation.getMethod().toGenericString());

                return invocation.proceed();
            } else {
                if (log.isTraceEnabled())
                    log.trace("Creating new transaction to call " + invocation.getMethod().toGenericString());

                final Transactional annotation = readAnnotation(invocation);
                final boolean readOnly = annotation.readOnly();

                // We are responsible for creating+closing the connection
                Timer.Context ownerTimer = transactionStartedCalls.time();
                try {
                    // no transaction already started, so start one and enforce its semantics
                    final Transaction tx = session.beginTransaction();

                    if (readOnly)
                        makeReadOnly(session);
                    else
                        makeReadWrite(session);

                    // Execute the method
                    final Object result;
                    try {
                        result = invocation.proceed();
                    } catch (Exception e) {
                        if (shouldRollback(annotation, e)) {
                            errorRollbacks.mark();

                            rollback(tx, e);
                        } else {
                            complete(tx, readOnly);
                        }

                        // propagate the exception
                        throw e;
                    } catch (Error e) {
                        errorRollbacks.mark();

                        rollback(tx);

                        // propagate the error
                        throw e;
                    }

                    // The method completed successfully, we can complete the the transaction
                    // we can't move into the above try block because it'll interfere with the do not move into try block as it interferes with the advised method's throwing semantics
                    RuntimeException commitException = null;
                    try {
                        complete(tx, readOnly);
                    } catch (RuntimeException e) {
                        commitFailures.mark();

                        rollback(tx);

                        commitException = e;
                    }

                    // propagate anyway
                    if (commitException != null)
                        throw commitException;

                    // or return result
                    return result;
                } finally {
                    ownerTimer.stop();

                    if (session.isOpen()) {
                        // Close the session
                        session.close();
                    }
                }
            }
        } finally {
            callTimer.stop();
        }
    }

    /**
     * Make the {@link java.sql.Connection} underlying this {@link Session} read/write
     *
     * @param session
     */
    private void makeReadWrite(final Session session) {
        session.doWork(SetJDBCConnectionReadOnlyWork.READ_WRITE);
    }

    /**
     * Make the session (and the underlying {@link java.sql.Connection} read only
     *
     * @param session
     */
    private void makeReadOnly(final Session session) {
        session.setDefaultReadOnly(true);
        session.setFlushMode(FlushMode.MANUAL);

        // Make the Connection read only
        session.doWork(SetJDBCConnectionReadOnlyWork.READ_ONLY);
    }

    /**
     * Read the Transactional annotation for a given method invocation
     *
     * @param invocation
     *
     * @return
     */
    private Transactional readAnnotation(MethodInvocation invocation) {
        final Method method = invocation.getMethod();

        if (method.isAnnotationPresent(Transactional.class)) {
            return method.getAnnotation(Transactional.class);
        } else {
            throw new RuntimeException("Could not find Transactional annotation");
        }
    }

    /**
     * @param annotation
     *       The metadata annotation of the method
     * @param e
     *       The exception to test for rollback
     *
     * @return returns true if the transaction should be rolled back, otherwise false
     */
    private boolean shouldRollback(Transactional annotation, Exception e) {
        return isInstanceOf(e, annotation.rollbackOn()) && !isInstanceOf(e, annotation.exceptOn());
    }

    private static boolean isInstanceOf(Exception e, Class<? extends Exception>[] classes) {
        for (Class<? extends Exception> type : classes) {
            if (type.isInstance(e))
                return true;
        }

        return false;
    }

    /**
     * Complete the transaction
     *
     * @param tx
     * @param readOnly
     *       the read-only flag on the transaction (if true, the transaction will be rolled back, otherwise the transaction will be
     *       committed)
     */
    private final void complete(Transaction tx, boolean readOnly) {
        if (log.isTraceEnabled())
            log.trace("Complete " + tx);

        if (!readOnly)
            tx.commit();
        else
            tx.rollback();
    }

    private final void rollback(Transaction tx) {
        if (log.isTraceEnabled())
            log.trace("Rollback " + tx);

        tx.rollback();
    }

    private final void rollback(Transaction tx, Exception e) {
        if (log.isDebugEnabled())
            log.debug(e.getClass().getSimpleName() + " causes rollback");

        tx.rollback();
    }
}