FINERACT-2406: Set COMMIT flush mode during transaction processing

This commit is contained in:
Adam Saghy
2025-11-06 21:12:58 +01:00
parent 5079203d57
commit ccf760c79d
11 changed files with 737 additions and 47 deletions
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.fineract.infrastructure.core.annotation;
import jakarta.persistence.FlushModeType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to specify the flush mode for a method or class. When applied to a class, all public methods will use the
* specified flush mode. When applied to a method, it overrides any class-level annotation.
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface WithFlushMode {
/**
* The flush mode to be used for the annotated method or class methods.
*
* @return the flush mode
*/
FlushModeType value() default FlushModeType.AUTO;
}
@@ -0,0 +1,102 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.fineract.infrastructure.core.aop;
import jakarta.persistence.FlushModeType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.annotation.WithFlushMode;
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ClassUtils;
/**
* Aspect that handles the @WithFlushMode annotation to manage JPA flush mode around method execution.
* <p>
* This aspect is ordered to run after the @Transactional aspect (Ordered.LOWEST_PRECEDENCE - 1) to ensure proper
* transaction management. It will only modify the flush mode if there is an active transaction.
*/
@Aspect
@Component
@Order
@RequiredArgsConstructor
public class FlushModeAspect {
private static final Logger logger = LoggerFactory.getLogger(FlushModeAspect.class);
private final FlushModeHandler flushModeHandler;
@Around("@within(withFlushMode) || @annotation(withFlushMode)")
public Object manageFlushMode(ProceedingJoinPoint joinPoint, WithFlushMode withFlushMode) {
// Get the effective annotation (method level takes precedence over class level)
WithFlushMode effectiveAnnotation = getEffectiveAnnotation(joinPoint, withFlushMode);
if (effectiveAnnotation == null) {
return jointPointProceed(joinPoint);
}
FlushModeType flushMode = effectiveAnnotation.value();
// Check if we're in an active transaction
boolean hasActiveTransaction = TransactionSynchronizationManager.isActualTransactionActive();
if (!hasActiveTransaction) {
if (logger.isDebugEnabled()) {
logger.warn("No active transaction found for @WithFlushMode on {}.{}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName());
}
return jointPointProceed(joinPoint);
}
if (logger.isDebugEnabled()) {
logger.debug("Setting flush mode to {} for {}.{}", flushMode, joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName());
}
// Use FlushModeHandler to manage the flush mode around method execution
return flushModeHandler.withFlushMode(flushMode, () -> jointPointProceed(joinPoint));
}
private static Object jointPointProceed(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (RuntimeException e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException("Error in method with @WithFlushMode", e);
}
}
private WithFlushMode getEffectiveAnnotation(ProceedingJoinPoint joinPoint, WithFlushMode annotation) {
// If the annotation is already present on the method, use it
if (annotation != null && joinPoint.getSignature() instanceof MethodSignature) {
return annotation;
}
// Otherwise, try to get the class-level annotation
Class<?> targetClass = ClassUtils.getUserClass(joinPoint.getTarget().getClass());
return AnnotationUtils.findAnnotation(targetClass, WithFlushMode.class);
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.core.persistence;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.PersistenceContext;
import java.util.function.Supplier;
import org.springframework.stereotype.Component;
@Component
@@ -30,10 +31,28 @@ public class FlushModeHandler {
private EntityManager entityManager;
public void withFlushMode(FlushModeType flushMode, Runnable runnable) {
withFlushMode(flushMode, () -> {
runnable.run();
return null;
});
}
/**
* Executes the provided supplier with the specified flush mode, then restores the original flush mode.
*
* @param flushMode
* the flush mode to set
* @param supplier
* the code to execute
* @param <T>
* the type of the result
* @return the result of the supplier
*/
public <T> T withFlushMode(FlushModeType flushMode, Supplier<T> supplier) {
FlushModeType original = entityManager.getFlushMode();
try {
entityManager.setFlushMode(flushMode);
runnable.run();
return supplier.get();
} finally {
entityManager.setFlushMode(original);
}