View Javadoc
1   /*
2    *    Copyright 2009-2023 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       https://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.guice.transactional;
17  
18  import static java.lang.String.format;
19  import static java.lang.Thread.currentThread;
20  
21  import jakarta.inject.Inject;
22  
23  import java.lang.reflect.Constructor;
24  import java.lang.reflect.Method;
25  import java.util.Arrays;
26  
27  import org.aopalliance.intercept.MethodInterceptor;
28  import org.aopalliance.intercept.MethodInvocation;
29  import org.apache.ibatis.logging.Log;
30  import org.apache.ibatis.logging.LogFactory;
31  import org.apache.ibatis.session.SqlSessionManager;
32  
33  /**
34   * Method interceptor for {@link Transactional} annotation.
35   */
36  public final class TransactionalMethodInterceptor implements MethodInterceptor {
37  
38    private static final Class<?>[] CAUSE_TYPES = new Class[] { Throwable.class };
39  
40    private static final Class<?>[] MESSAGE_CAUSE_TYPES = new Class[] { String.class, Throwable.class };
41  
42    /**
43     * This class logger.
44     */
45    private final Log log = LogFactory.getLog(getClass());
46  
47    /**
48     * The {@code SqlSessionManager} reference.
49     */
50    @Inject
51    private SqlSessionManager sqlSessionManager;
52  
53    /**
54     * Sets the SqlSessionManager instance.
55     *
56     * @param sqlSessionManager
57     *          the SqlSessionManager instance.
58     */
59    public void setSqlSessionManager(SqlSessionManager sqlSessionManager) {
60      this.sqlSessionManager = sqlSessionManager;
61    }
62  
63    /**
64     * {@inheritDoc}
65     */
66    @Override
67    public Object invoke(MethodInvocation invocation) throws Throwable {
68      Method interceptedMethod = invocation.getMethod();
69      Transactional transactional = interceptedMethod.getAnnotation(Transactional.class);
70  
71      // The annotation may be present at the class level instead
72      if (transactional == null) {
73        transactional = interceptedMethod.getDeclaringClass().getAnnotation(Transactional.class);
74      }
75  
76      String debugPrefix = null;
77      if (this.log.isDebugEnabled()) {
78        debugPrefix = format("[Intercepted method: %s]", interceptedMethod.toGenericString());
79      }
80  
81      boolean isSessionInherited = this.sqlSessionManager.isManagedSessionStarted();
82  
83      if (isSessionInherited) {
84        if (log.isDebugEnabled()) {
85          log.debug(format("%s - SqlSession already set for thread: %s", debugPrefix, currentThread().getId()));
86        }
87      } else {
88        if (log.isDebugEnabled()) {
89          log.debug(
90              format("%s - SqlSession not set for thread: %s, creating a new one", debugPrefix, currentThread().getId()));
91        }
92  
93        sqlSessionManager.startManagedSession(transactional.executorType(),
94            transactional.isolation().getTransactionIsolationLevel());
95      }
96  
97      Object object = null;
98      boolean needsRollback = transactional.rollbackOnly();
99      try {
100       object = invocation.proceed();
101     } catch (Throwable t) {
102       needsRollback = true;
103       throw convertThrowableIfNeeded(invocation, transactional, t);
104     } finally {
105       if (!isSessionInherited) {
106         try {
107           if (needsRollback) {
108             if (log.isDebugEnabled()) {
109               log.debug(debugPrefix + " - SqlSession of thread: " + currentThread().getId() + " rolling back");
110             }
111 
112             sqlSessionManager.rollback(true);
113           } else {
114             if (log.isDebugEnabled()) {
115               log.debug(debugPrefix + " - SqlSession of thread: " + currentThread().getId() + " committing");
116             }
117 
118             sqlSessionManager.commit(transactional.force());
119           }
120         } finally {
121           if (log.isDebugEnabled()) {
122             log.debug(format("%s - SqlSession of thread: %s terminated its life-cycle, closing it", debugPrefix,
123                 currentThread().getId()));
124           }
125 
126           sqlSessionManager.close();
127         }
128       } else if (log.isDebugEnabled()) {
129         log.debug(format("%s - SqlSession of thread: %s is inherited, skipped close operation", debugPrefix,
130             currentThread().getId()));
131       }
132     }
133 
134     return object;
135   }
136 
137   private Throwable convertThrowableIfNeeded(MethodInvocation invocation, Transactional transactional, Throwable t) {
138     Method interceptedMethod = invocation.getMethod();
139 
140     // check the caught exception is declared in the invoked method
141     for (Class<?> exceptionClass : interceptedMethod.getExceptionTypes()) {
142       if (exceptionClass.isAssignableFrom(t.getClass())) {
143         return t;
144       }
145     }
146 
147     // check the caught exception is of same rethrow type
148     if (transactional.rethrowExceptionsAs().isAssignableFrom(t.getClass())) {
149       return t;
150     }
151 
152     // rethrow the exception as new exception
153     String errorMessage;
154     Object[] initargs;
155     Class<?>[] initargsType;
156 
157     if (transactional.exceptionMessage().length() != 0) {
158       errorMessage = format(transactional.exceptionMessage(), invocation.getArguments());
159       initargs = new Object[] { errorMessage, t };
160       initargsType = MESSAGE_CAUSE_TYPES;
161     } else {
162       initargs = new Object[] { t };
163       initargsType = CAUSE_TYPES;
164     }
165 
166     Constructor<? extends Throwable> exceptionConstructor = getMatchingConstructor(transactional.rethrowExceptionsAs(),
167         initargsType);
168     Throwable rethrowEx = null;
169     if (exceptionConstructor != null) {
170       try {
171         rethrowEx = exceptionConstructor.newInstance(initargs);
172       } catch (Exception e) {
173         errorMessage = format("Impossible to re-throw '%s', it needs the constructor with %s argument(s).",
174             transactional.rethrowExceptionsAs().getName(), Arrays.toString(initargsType));
175         log.error(errorMessage, e);
176         rethrowEx = new RuntimeException(errorMessage, e);
177       }
178     } else {
179       errorMessage = format("Impossible to re-throw '%s', it needs the constructor with %s or %s argument(s).",
180           transactional.rethrowExceptionsAs().getName(), Arrays.toString(CAUSE_TYPES),
181           Arrays.toString(MESSAGE_CAUSE_TYPES));
182       log.error(errorMessage);
183       rethrowEx = new RuntimeException(errorMessage);
184     }
185 
186     return rethrowEx;
187   }
188 
189   @SuppressWarnings("unchecked")
190   private static <E extends Throwable> Constructor<E> getMatchingConstructor(Class<E> type, Class<?>[] argumentsType) {
191     Class<? super E> currentType = type;
192     while (Object.class != currentType) {
193       for (Constructor<?> constructor : currentType.getConstructors()) {
194         if (Arrays.equals(argumentsType, constructor.getParameterTypes())) {
195           return (Constructor<E>) constructor;
196         }
197       }
198       currentType = currentType.getSuperclass();
199     }
200     return null;
201   }
202 
203 }