View Javadoc
1   /*
2    *    Copyright 2013-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.cdi;
17  
18  import jakarta.inject.Inject;
19  import jakarta.interceptor.AroundInvoke;
20  import jakarta.interceptor.Interceptor;
21  import jakarta.interceptor.InvocationContext;
22  import jakarta.transaction.HeuristicMixedException;
23  import jakarta.transaction.HeuristicRollbackException;
24  import jakarta.transaction.NotSupportedException;
25  import jakarta.transaction.RollbackException;
26  import jakarta.transaction.SystemException;
27  
28  import java.io.Serializable;
29  import java.lang.reflect.InvocationTargetException;
30  import java.lang.reflect.UndeclaredThrowableException;
31  
32  import org.apache.ibatis.session.SqlSessionManager;
33  
34  /**
35   * Best-effort interceptor for local transactions. It locates all the instances of {@code SqlSssionManager} and starts
36   * transactions on all them. It cannot guarantee atomiticy if there is more than one {@code SqlSssionManager}. Use XA
37   * drivers, a JTA container and the {@link JtaTransactionInterceptor} in that case.
38   *
39   * @see JtaTransactionInterceptor
40   *
41   * @author Frank David Martínez
42   */
43  @Transactional
44  @Interceptor
45  public class LocalTransactionInterceptor implements Serializable {
46  
47    private static final long serialVersionUID = 1L;
48  
49    @Inject
50    private transient SqlSessionManagerRegistry registry;
51  
52    /**
53     * Invoke.
54     *
55     * @param ctx
56     *          the ctx
57     *
58     * @return the object
59     *
60     * @throws Exception
61     *           the exception
62     */
63    @AroundInvoke
64    public Object invoke(InvocationContext ctx) throws Exception {
65      Transactional transactional = getTransactionalAnnotation(ctx);
66      boolean isInitiator = start(transactional);
67      boolean isExternalJta = isTransactionActive();
68      if (isInitiator && !isExternalJta) {
69        beginJta();
70      }
71      boolean needsRollback = transactional.rollbackOnly();
72      Object result;
73      try {
74        result = ctx.proceed();
75      } catch (Exception ex) {
76        Exception unwrapped = unwrapException(ex);
77        needsRollback = needsRollback || needsRollback(transactional, unwrapped);
78        throw unwrapped;
79      } finally {
80        if (isInitiator) {
81          try {
82            if (needsRollback) {
83              rollback(transactional);
84            } else {
85              commit(transactional);
86            }
87          } finally {
88            close();
89            endJta(isExternalJta, needsRollback);
90          }
91        }
92      }
93      return result;
94    }
95  
96    /**
97     * Checks if is transaction active.
98     *
99     * @return true, if is transaction active
100    *
101    * @throws SystemException
102    *           used by jtaTransactionInterceptor
103    */
104   protected boolean isTransactionActive() throws SystemException {
105     return false;
106   }
107 
108   /**
109    * Begin jta.
110    *
111    * @throws NotSupportedException
112    *           used by jtaTransactionInterceptor
113    * @throws SystemException
114    *           used by jtaTransactionInterceptor
115    */
116   protected void beginJta() throws NotSupportedException, SystemException {
117     // nothing to do
118   }
119 
120   /**
121    * End jta.
122    *
123    * @param isExternaTransaction
124    *          the is externa transaction
125    * @param commit
126    *          the commit
127    *
128    * @throws SystemException
129    *           used by jtaTransactionInterceptor
130    * @throws RollbackException
131    *           used by jtaTransactionInterceptor
132    * @throws HeuristicMixedException
133    *           used by jtaTransactionInterceptor
134    * @throws HeuristicRollbackException
135    *           used by jtaTransactionInterceptor
136    */
137   protected void endJta(boolean isExternaTransaction, boolean commit)
138       throws SystemException, RollbackException, HeuristicMixedException, HeuristicRollbackException {
139     // nothing to do
140   }
141 
142   private boolean needsRollback(Transactional transactional, Throwable throwable) {
143     if (RuntimeException.class.isAssignableFrom(throwable.getClass())) {
144       return true;
145     }
146     for (Class<? extends Throwable> exceptionClass : transactional.rollbackFor()) {
147       if (exceptionClass.isAssignableFrom(throwable.getClass())) {
148         return true;
149       }
150     }
151     return false;
152   }
153 
154   protected Transactional getTransactionalAnnotation(InvocationContext ctx) {
155     Transactional t = ctx.getMethod().getAnnotation(Transactional.class);
156     if (t == null) {
157       t = ctx.getMethod().getDeclaringClass().getAnnotation(Transactional.class);
158     }
159     return t;
160   }
161 
162   private boolean start(Transactional transactional) {
163     boolean started = false;
164     for (SqlSessionManager manager : this.registry.getManagers()) {
165       if (!manager.isManagedSessionStarted()) {
166         manager.startManagedSession(transactional.executorType(),
167             transactional.isolation().getTransactionIsolationLevel());
168         started = true;
169       }
170     }
171     return started;
172   }
173 
174   private void commit(Transactional transactional) {
175     for (SqlSessionManager manager : this.registry.getManagers()) {
176       manager.commit(transactional.force());
177     }
178   }
179 
180   private void rollback(Transactional transactional) {
181     for (SqlSessionManager manager : this.registry.getManagers()) {
182       manager.rollback(transactional.force());
183     }
184   }
185 
186   private void close() {
187     for (SqlSessionManager manager : this.registry.getManagers()) {
188       manager.close();
189     }
190   }
191 
192   private Exception unwrapException(Exception wrapped) {
193     Throwable unwrapped = wrapped;
194     while (true) {
195       if (unwrapped instanceof InvocationTargetException) {
196         unwrapped = ((InvocationTargetException) unwrapped).getTargetException();
197       } else if (unwrapped instanceof UndeclaredThrowableException) {
198         unwrapped = ((UndeclaredThrowableException) unwrapped).getUndeclaredThrowable();
199       } else if (!(unwrapped instanceof Exception)) {
200         return new RuntimeException(unwrapped);
201       } else {
202         return (Exception) unwrapped;
203       }
204     }
205   }
206 
207 }