View Javadoc
1   /*
2    *    Copyright 2013-2026 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  @Transactional
42  @Interceptor
43  public class LocalTransactionInterceptor implements Serializable {
44  
45    private static final long serialVersionUID = 1L;
46  
47    @Inject
48    private transient SqlSessionManagerRegistry registry;
49  
50    /**
51     * Invoke.
52     *
53     * @param ctx
54     *          the ctx
55     *
56     * @return the object
57     *
58     * @throws Exception
59     *           the exception
60     */
61    @AroundInvoke
62    public Object invoke(InvocationContext ctx) throws Exception {
63      Transactional transactional = getTransactionalAnnotation(ctx);
64      boolean isInitiator = start(transactional);
65      boolean isExternalJta = isTransactionActive();
66      if (isInitiator && !isExternalJta) {
67        beginJta();
68      }
69      boolean needsRollback = transactional.rollbackOnly();
70      Object result;
71      try {
72        result = ctx.proceed();
73      } catch (Exception ex) {
74        Exception unwrapped = unwrapException(ex);
75        needsRollback = needsRollback || needsRollback(transactional, unwrapped);
76        throw unwrapped;
77      } finally {
78        if (isInitiator) {
79          try {
80            if (needsRollback) {
81              rollback(transactional);
82            } else {
83              commit(transactional);
84            }
85          } finally {
86            close();
87            endJta(isExternalJta, needsRollback);
88          }
89        }
90      }
91      return result;
92    }
93  
94    /**
95     * Checks if is transaction active.
96     *
97     * @return true, if is transaction active
98     *
99     * @throws SystemException
100    *           used by jtaTransactionInterceptor
101    */
102   protected boolean isTransactionActive() throws SystemException {
103     return false;
104   }
105 
106   /**
107    * Begin jta.
108    *
109    * @throws NotSupportedException
110    *           used by jtaTransactionInterceptor
111    * @throws SystemException
112    *           used by jtaTransactionInterceptor
113    */
114   protected void beginJta() throws NotSupportedException, SystemException {
115     // nothing to do
116   }
117 
118   /**
119    * End jta.
120    *
121    * @param isExternaTransaction
122    *          the is externa transaction
123    * @param commit
124    *          the commit
125    *
126    * @throws SystemException
127    *           used by jtaTransactionInterceptor
128    * @throws RollbackException
129    *           used by jtaTransactionInterceptor
130    * @throws HeuristicMixedException
131    *           used by jtaTransactionInterceptor
132    * @throws HeuristicRollbackException
133    *           used by jtaTransactionInterceptor
134    */
135   protected void endJta(boolean isExternaTransaction, boolean commit)
136       throws SystemException, RollbackException, HeuristicMixedException, HeuristicRollbackException {
137     // nothing to do
138   }
139 
140   private boolean needsRollback(Transactional transactional, Throwable throwable) {
141     if (RuntimeException.class.isAssignableFrom(throwable.getClass())) {
142       return true;
143     }
144     for (Class<? extends Throwable> exceptionClass : transactional.rollbackFor()) {
145       if (exceptionClass.isAssignableFrom(throwable.getClass())) {
146         return true;
147       }
148     }
149     return false;
150   }
151 
152   protected Transactional getTransactionalAnnotation(InvocationContext ctx) {
153     Transactional t = ctx.getMethod().getAnnotation(Transactional.class);
154     if (t == null) {
155       t = ctx.getMethod().getDeclaringClass().getAnnotation(Transactional.class);
156     }
157     return t;
158   }
159 
160   private boolean start(Transactional transactional) {
161     boolean started = false;
162     for (SqlSessionManager manager : this.registry.getManagers()) {
163       if (!manager.isManagedSessionStarted()) {
164         manager.startManagedSession(transactional.executorType(),
165             transactional.isolation().getTransactionIsolationLevel());
166         started = true;
167       }
168     }
169     return started;
170   }
171 
172   private void commit(Transactional transactional) {
173     for (SqlSessionManager manager : this.registry.getManagers()) {
174       manager.commit(transactional.force());
175     }
176   }
177 
178   private void rollback(Transactional transactional) {
179     for (SqlSessionManager manager : this.registry.getManagers()) {
180       manager.rollback(transactional.force());
181     }
182   }
183 
184   private void close() {
185     for (SqlSessionManager manager : this.registry.getManagers()) {
186       manager.close();
187     }
188   }
189 
190   private Exception unwrapException(Exception wrapped) {
191     Throwable unwrapped = wrapped;
192     while (true) {
193       if (unwrapped instanceof InvocationTargetException invocationTargetException) {
194         unwrapped = invocationTargetException.getTargetException();
195       } else if (unwrapped instanceof UndeclaredThrowableException undeclaredThrowableException) {
196         unwrapped = undeclaredThrowableException.getUndeclaredThrowable();
197       } else if (unwrapped instanceof Exception exception) {
198         return exception;
199       } else {
200         return new RuntimeException(unwrapped);
201       }
202     }
203   }
204 
205 }