001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.transaction.reactive; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.LinkedHashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025 026import org.apache.commons.logging.Log; 027import org.apache.commons.logging.LogFactory; 028import reactor.core.publisher.Mono; 029 030import org.springframework.core.annotation.AnnotationAwareOrderComparator; 031import org.springframework.lang.Nullable; 032import org.springframework.transaction.NoTransactionException; 033import org.springframework.util.Assert; 034 035/** 036 * Central delegate that manages resources and transaction synchronizations per 037 * subscriber context. 038 * To be used by resource management code but not by typical application code. 039 * 040 * <p>Supports one resource per key without overwriting, that is, a resource needs 041 * to be removed before a new one can be set for the same key. 042 * Supports a list of transaction synchronizations if synchronization is active. 043 * 044 * <p>Resource management code should check for context-bound resources, e.g. 045 * database connections, via {@code getResource}. Such code is normally not 046 * supposed to bind resources to units of work, as this is the responsibility 047 * of transaction managers. A further option is to lazily bind on first use if 048 * transaction synchronization is active, for performing transactions that span 049 * an arbitrary number of resources. 050 * 051 * <p>Transaction synchronization must be activated and deactivated by a transaction 052 * manager via {@link #initSynchronization()} and {@link #clearSynchronization()}. 053 * This is automatically supported by {@link AbstractReactiveTransactionManager}, 054 * and thus by all standard Spring transaction managers. 055 * 056 * <p>Resource management code should only register synchronizations when this 057 * manager is active, which can be checked via {@link #isSynchronizationActive}; 058 * it should perform immediate resource cleanup else. If transaction synchronization 059 * isn't active, there is either no current transaction, or the transaction manager 060 * doesn't support transaction synchronization. 061 * 062 * <p>Synchronization is for example used to always return the same resources within 063 * a transaction, e.g. a database connection for any given connection factory. 064 * 065 * @author Mark Paluch 066 * @author Juergen Hoeller 067 * @since 5.2 068 * @see #isSynchronizationActive 069 * @see #registerSynchronization 070 * @see TransactionSynchronization 071 */ 072public class TransactionSynchronizationManager { 073 074 private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class); 075 076 private final TransactionContext transactionContext; 077 078 079 public TransactionSynchronizationManager(TransactionContext transactionContext) { 080 this.transactionContext = transactionContext; 081 } 082 083 084 /** 085 * Get the {@link TransactionSynchronizationManager} that is associated with 086 * the current transaction context. 087 * <p>Mainly intended for code that wants to bind resources or synchronizations. 088 * @throws NoTransactionException if the transaction info cannot be found — 089 * for example, because the method was invoked outside a managed transaction 090 */ 091 public static Mono<TransactionSynchronizationManager> forCurrentTransaction() { 092 return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new); 093 } 094 095 /** 096 * Check if there is a resource for the given key bound to the current thread. 097 * @param key the key to check (usually the resource factory) 098 * @return if there is a value bound to the current thread 099 */ 100 public boolean hasResource(Object key) { 101 Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); 102 Object value = doGetResource(actualKey); 103 return (value != null); 104 } 105 106 /** 107 * Retrieve a resource for the given key that is bound to the current thread. 108 * @param key the key to check (usually the resource factory) 109 * @return a value bound to the current thread (usually the active 110 * resource object), or {@code null} if none 111 */ 112 @Nullable 113 public Object getResource(Object key) { 114 Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); 115 Object value = doGetResource(actualKey); 116 if (value != null && logger.isTraceEnabled()) { 117 logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to context [" + 118 this.transactionContext.getName() + "]"); 119 } 120 return value; 121 } 122 123 /** 124 * Actually check the value of the resource that is bound for the given key. 125 */ 126 @Nullable 127 private Object doGetResource(Object actualKey) { 128 return this.transactionContext.getResources().get(actualKey); 129 } 130 131 /** 132 * Bind the given resource for the given key to the current context. 133 * @param key the key to bind the value to (usually the resource factory) 134 * @param value the value to bind (usually the active resource object) 135 * @throws IllegalStateException if there is already a value bound to the context 136 */ 137 public void bindResource(Object key, Object value) throws IllegalStateException { 138 Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); 139 Assert.notNull(value, "Value must not be null"); 140 Map<Object, Object> map = this.transactionContext.getResources(); 141 Object oldValue = map.put(actualKey, value); 142 if (oldValue != null) { 143 throw new IllegalStateException("Already value [" + oldValue + "] for key [" + 144 actualKey + "] bound to context [" + this.transactionContext.getName() + "]"); 145 } 146 if (logger.isTraceEnabled()) { 147 logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to context [" + 148 this.transactionContext.getName() + "]"); 149 } 150 } 151 152 /** 153 * Unbind a resource for the given key from the current context. 154 * @param key the key to unbind (usually the resource factory) 155 * @return the previously bound value (usually the active resource object) 156 * @throws IllegalStateException if there is no value bound to the context 157 */ 158 public Object unbindResource(Object key) throws IllegalStateException { 159 Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); 160 Object value = doUnbindResource(actualKey); 161 if (value == null) { 162 throw new IllegalStateException( 163 "No value for key [" + actualKey + "] bound to context [" + this.transactionContext.getName() + "]"); 164 } 165 return value; 166 } 167 168 /** 169 * Unbind a resource for the given key from the current context. 170 * @param key the key to unbind (usually the resource factory) 171 * @return the previously bound value, or {@code null} if none bound 172 */ 173 @Nullable 174 public Object unbindResourceIfPossible(Object key) { 175 Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); 176 return doUnbindResource(actualKey); 177 } 178 179 /** 180 * Actually remove the value of the resource that is bound for the given key. 181 */ 182 @Nullable 183 private Object doUnbindResource(Object actualKey) { 184 Map<Object, Object> map = this.transactionContext.getResources(); 185 Object value = map.remove(actualKey); 186 if (value != null && logger.isTraceEnabled()) { 187 logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from context [" + 188 this.transactionContext.getName() + "]"); 189 } 190 return value; 191 } 192 193 194 //------------------------------------------------------------------------- 195 // Management of transaction synchronizations 196 //------------------------------------------------------------------------- 197 198 /** 199 * Return if transaction synchronization is active for the current context. 200 * Can be called before register to avoid unnecessary instance creation. 201 * @see #registerSynchronization 202 */ 203 public boolean isSynchronizationActive() { 204 return (this.transactionContext.getSynchronizations() != null); 205 } 206 207 /** 208 * Activate transaction synchronization for the current context. 209 * Called by a transaction manager on transaction begin. 210 * @throws IllegalStateException if synchronization is already active 211 */ 212 public void initSynchronization() throws IllegalStateException { 213 if (isSynchronizationActive()) { 214 throw new IllegalStateException("Cannot activate transaction synchronization - already active"); 215 } 216 logger.trace("Initializing transaction synchronization"); 217 this.transactionContext.setSynchronizations(new LinkedHashSet<>()); 218 } 219 220 /** 221 * Register a new transaction synchronization for the current context. 222 * Typically called by resource management code. 223 * <p>Note that synchronizations can implement the 224 * {@link org.springframework.core.Ordered} interface. 225 * They will be executed in an order according to their order value (if any). 226 * @param synchronization the synchronization object to register 227 * @throws IllegalStateException if transaction synchronization is not active 228 * @see org.springframework.core.Ordered 229 */ 230 public void registerSynchronization(TransactionSynchronization synchronization) 231 throws IllegalStateException { 232 233 Assert.notNull(synchronization, "TransactionSynchronization must not be null"); 234 Set<TransactionSynchronization> synchs = this.transactionContext.getSynchronizations(); 235 if (synchs == null) { 236 throw new IllegalStateException("Transaction synchronization is not active"); 237 } 238 synchs.add(synchronization); 239 } 240 241 /** 242 * Return an unmodifiable snapshot list of all registered synchronizations 243 * for the current context. 244 * @return unmodifiable List of TransactionSynchronization instances 245 * @throws IllegalStateException if synchronization is not active 246 * @see TransactionSynchronization 247 */ 248 public List<TransactionSynchronization> getSynchronizations() throws IllegalStateException { 249 Set<TransactionSynchronization> synchs = this.transactionContext.getSynchronizations(); 250 if (synchs == null) { 251 throw new IllegalStateException("Transaction synchronization is not active"); 252 } 253 // Return unmodifiable snapshot, to avoid ConcurrentModificationExceptions 254 // while iterating and invoking synchronization callbacks that in turn 255 // might register further synchronizations. 256 if (synchs.isEmpty()) { 257 return Collections.emptyList(); 258 } 259 else { 260 // Sort lazily here, not in registerSynchronization. 261 List<TransactionSynchronization> sortedSynchs = new ArrayList<>(synchs); 262 AnnotationAwareOrderComparator.sort(sortedSynchs); 263 return Collections.unmodifiableList(sortedSynchs); 264 } 265 } 266 267 /** 268 * Deactivate transaction synchronization for the current context. 269 * Called by the transaction manager on transaction cleanup. 270 * @throws IllegalStateException if synchronization is not active 271 */ 272 public void clearSynchronization() throws IllegalStateException { 273 if (!isSynchronizationActive()) { 274 throw new IllegalStateException("Cannot deactivate transaction synchronization - not active"); 275 } 276 logger.trace("Clearing transaction synchronization"); 277 this.transactionContext.setSynchronizations(null); 278 } 279 280 281 //------------------------------------------------------------------------- 282 // Exposure of transaction characteristics 283 //------------------------------------------------------------------------- 284 285 /** 286 * Expose the name of the current transaction, if any. 287 * Called by the transaction manager on transaction begin and on cleanup. 288 * @param name the name of the transaction, or {@code null} to reset it 289 * @see org.springframework.transaction.TransactionDefinition#getName() 290 */ 291 public void setCurrentTransactionName(@Nullable String name) { 292 this.transactionContext.setCurrentTransactionName(name); 293 } 294 295 /** 296 * Return the name of the current transaction, or {@code null} if none set. 297 * To be called by resource management code for optimizations per use case, 298 * for example to optimize fetch strategies for specific named transactions. 299 * @see org.springframework.transaction.TransactionDefinition#getName() 300 */ 301 @Nullable 302 public String getCurrentTransactionName() { 303 return this.transactionContext.getCurrentTransactionName(); 304 } 305 306 /** 307 * Expose a read-only flag for the current transaction. 308 * Called by the transaction manager on transaction begin and on cleanup. 309 * @param readOnly {@code true} to mark the current transaction 310 * as read-only; {@code false} to reset such a read-only marker 311 * @see org.springframework.transaction.TransactionDefinition#isReadOnly() 312 */ 313 public void setCurrentTransactionReadOnly(boolean readOnly) { 314 this.transactionContext.setCurrentTransactionReadOnly(readOnly); 315 } 316 317 /** 318 * Return whether the current transaction is marked as read-only. 319 * To be called by resource management code when preparing a newly 320 * created resource. 321 * <p>Note that transaction synchronizations receive the read-only flag 322 * as argument for the {@code beforeCommit} callback, to be able 323 * to suppress change detection on commit. The present method is meant 324 * to be used for earlier read-only checks. 325 * @see org.springframework.transaction.TransactionDefinition#isReadOnly() 326 * @see TransactionSynchronization#beforeCommit(boolean) 327 */ 328 public boolean isCurrentTransactionReadOnly() { 329 return this.transactionContext.isCurrentTransactionReadOnly(); 330 } 331 332 /** 333 * Expose an isolation level for the current transaction. 334 * Called by the transaction manager on transaction begin and on cleanup. 335 * @param isolationLevel the isolation level to expose, according to the 336 * R2DBC Connection constants (equivalent to the corresponding Spring 337 * TransactionDefinition constants), or {@code null} to reset it 338 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED 339 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED 340 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ 341 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE 342 * @see org.springframework.transaction.TransactionDefinition#getIsolationLevel() 343 */ 344 public void setCurrentTransactionIsolationLevel(@Nullable Integer isolationLevel) { 345 this.transactionContext.setCurrentTransactionIsolationLevel(isolationLevel); 346 } 347 348 /** 349 * Return the isolation level for the current transaction, if any. 350 * To be called by resource management code when preparing a newly 351 * created resource (for example, a R2DBC Connection). 352 * @return the currently exposed isolation level, according to the 353 * R2DBC Connection constants (equivalent to the corresponding Spring 354 * TransactionDefinition constants), or {@code null} if none 355 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED 356 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED 357 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ 358 * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE 359 * @see org.springframework.transaction.TransactionDefinition#getIsolationLevel() 360 */ 361 @Nullable 362 public Integer getCurrentTransactionIsolationLevel() { 363 return this.transactionContext.getCurrentTransactionIsolationLevel(); 364 } 365 366 /** 367 * Expose whether there currently is an actual transaction active. 368 * Called by the transaction manager on transaction begin and on cleanup. 369 * @param active {@code true} to mark the current context as being associated 370 * with an actual transaction; {@code false} to reset that marker 371 */ 372 public void setActualTransactionActive(boolean active) { 373 this.transactionContext.setActualTransactionActive(active); 374 } 375 376 /** 377 * Return whether there currently is an actual transaction active. 378 * This indicates whether the current context is associated with an actual 379 * transaction rather than just with active transaction synchronization. 380 * <p>To be called by resource management code that wants to discriminate 381 * between active transaction synchronization (with or without backing 382 * resource transaction; also on PROPAGATION_SUPPORTS) and an actual 383 * transaction being active (with backing resource transaction; 384 * on PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, etc). 385 * @see #isSynchronizationActive() 386 */ 387 public boolean isActualTransactionActive() { 388 return this.transactionContext.isActualTransactionActive(); 389 } 390 391 /** 392 * Clear the entire transaction synchronization state: 393 * registered synchronizations as well as the various transaction characteristics. 394 * @see #clearSynchronization() 395 * @see #setCurrentTransactionName 396 * @see #setCurrentTransactionReadOnly 397 * @see #setCurrentTransactionIsolationLevel 398 * @see #setActualTransactionActive 399 */ 400 public void clear() { 401 this.transactionContext.clear(); 402 } 403 404}