← Back to Documentation Portal|📊 Coverage Quality Gates|Coverage Dashboard
Payflow API — Automated JaCoCo Code Coverage (90% Line / 73% Branch)

PerUserRateLimiterAspect.java

package com.payflow.resilience;

import java.lang.reflect.Method;

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.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.payflow.config.RateLimiterKeyResolver;

import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;

/**
 * Spring AOP Aspect intercepting methods annotated with
 * {@link PerUserRateLimiter}. Enforces per-user rate limiting using dynamic
 * partition keys and throws {@link RequestNotPermitted} when capacity is
 * exceeded.
 */
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 50)
public class PerUserRateLimiterAspect {

	private static final Logger LOG = LoggerFactory.getLogger(PerUserRateLimiterAspect.class);

	private final UserRateLimiterService userRateLimiterService;
	private final RateLimiterKeyResolver rateLimiterKeyResolver;

	public PerUserRateLimiterAspect(UserRateLimiterService userRateLimiterService,
			RateLimiterKeyResolver rateLimiterKeyResolver) {
		this.userRateLimiterService = userRateLimiterService;
		this.rateLimiterKeyResolver = rateLimiterKeyResolver;
	}

	@Around("@annotation(perUserRateLimiter)")
	public Object enforceRateLimit(ProceedingJoinPoint joinPoint, PerUserRateLimiter perUserRateLimiter)
			throws Throwable {
		String userKey = rateLimiterKeyResolver.resolveKey();
		String baseConfigName = perUserRateLimiter.name();

		RateLimiter rateLimiter = userRateLimiterService.getRateLimiterForUser(baseConfigName, userKey);
		boolean permissionAcquired = rateLimiter.acquirePermission();

		if (!permissionAcquired) {
			String targetClass = joinPoint.getTarget().getClass().getSimpleName();
			String targetMethod = joinPoint.getSignature().getName();
			LOG.warn("Per-user rate limit exceeded for user '{}' on method '{}.{}'", userKey, targetClass,
					targetMethod);

			String fallbackMethodName = perUserRateLimiter.fallbackMethod();
			if (fallbackMethodName != null && !fallbackMethodName.isBlank()) {
				return invokeFallback(joinPoint, fallbackMethodName,
						RequestNotPermitted.createRequestNotPermitted(rateLimiter));
			}

			throw RequestNotPermitted.createRequestNotPermitted(rateLimiter);
		}

		return joinPoint.proceed();
	}

	private Object invokeFallback(ProceedingJoinPoint joinPoint, String fallbackMethodName, Throwable cause)
			throws Throwable {
		Object target = joinPoint.getTarget();
		Object[] args = joinPoint.getArgs();
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Class<?>[] paramTypes = signature.getParameterTypes();

		// Try finding method with (originalArgs..., Throwable)
		Class<?>[] fallbackParamTypes = new Class<?>[paramTypes.length + 1];
		System.arraycopy(paramTypes, 0, fallbackParamTypes, 0, paramTypes.length);
		fallbackParamTypes[paramTypes.length] = Throwable.class;

		Object[] fallbackArgs = new Object[args.length + 1];
		System.arraycopy(args, 0, fallbackArgs, 0, args.length);
		fallbackArgs[args.length] = cause;

		try {
			Method fallbackMethod = target.getClass().getMethod(fallbackMethodName, fallbackParamTypes);
			return fallbackMethod.invoke(target, fallbackArgs);
		} catch (NoSuchMethodException e) {
			// Try finding method with exact cause class
			fallbackParamTypes[paramTypes.length] = cause.getClass();
			try {
				Class<?> targetClass = target.getClass();
				Method fallbackMethod = targetClass.getMethod(fallbackMethodName, fallbackParamTypes);
				return fallbackMethod.invoke(target, fallbackArgs);
			} catch (NoSuchMethodException ex) {
				LOG.error("Fallback method '{}(...)' not found on {}", fallbackMethodName,
						target.getClass().getSimpleName());
				throw cause;
			}
		}
	}
}