AOP Fixes

This commit is contained in:
Anthony Stirling 2025-07-11 22:53:16 +01:00
parent bbf5d5f6d4
commit ae3ed72283
5 changed files with 54 additions and 108 deletions

View File

@ -10,6 +10,8 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
@ -33,7 +35,6 @@ public class AutoJobAspect {
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
private final JobExecutorService jobExecutorService;
private final HttpServletRequest request;
private final FileOrUploadService fileOrUploadService;
private final FileStorage fileStorage;
@ -42,7 +43,17 @@ public class AutoJobAspect {
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
// This aspect will run before any audit aspects due to @Order(0)
// Extract parameters from the request and annotation
boolean async = Boolean.parseBoolean(request.getParameter("async"));
boolean async = false;
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest request = attrs.getRequest();
async = Boolean.parseBoolean(request.getParameter("async"));
log.debug("AutoJobAspect: Processing {} {} with async={}", request.getMethod(), request.getRequestURI(), async);
}
} catch (Exception e) {
log.debug("Could not retrieve async parameter from request: {}", e.getMessage());
}
long timeout = autoJobPostMapping.timeout();
int retryCount = autoJobPostMapping.retryCount();
boolean trackProgress = autoJobPostMapping.trackProgress();
@ -54,19 +65,8 @@ public class AutoJobAspect {
retryCount,
trackProgress);
// Copy and process arguments
// In a test environment, we might need to update the original objects for verification
boolean isTestEnvironment = false;
try {
isTestEnvironment = Class.forName("org.junit.jupiter.api.Test") != null;
} catch (ClassNotFoundException e) {
// Not in a test environment
}
Object[] args =
isTestEnvironment
? processArgsInPlace(joinPoint.getArgs(), async)
: copyAndProcessArgs(joinPoint.getArgs(), async);
// Process arguments in-place to avoid type mismatch issues
Object[] args = processArgsInPlace(joinPoint.getArgs(), async);
// Extract queueable and resourceWeight parameters and validate
boolean queueable = autoJobPostMapping.queueable();
@ -229,79 +229,10 @@ public class AutoJobAspect {
resourceWeight);
}
/**
* Creates deep copies of arguments when needed to avoid mutating the original objects
* Particularly important for PDFFile objects that might be reused by Spring
*
* @param originalArgs The original arguments
* @param async Whether this is an async operation
* @return A new array with safely processed arguments
*/
private Object[] copyAndProcessArgs(Object[] originalArgs, boolean async) {
if (originalArgs == null || originalArgs.length == 0) {
return originalArgs;
}
Object[] processedArgs = new Object[originalArgs.length];
// Copy all arguments
for (int i = 0; i < originalArgs.length; i++) {
Object arg = originalArgs[i];
if (arg instanceof PDFFile pdfFile) {
// Create a copy of PDFFile to avoid mutating the original
// Using direct property access instead of reflection for better performance
PDFFile pdfFileCopy = new PDFFile();
pdfFileCopy.setFileId(pdfFile.getFileId());
pdfFileCopy.setFileInput(pdfFile.getFileInput());
// Case 1: fileId is provided but no fileInput
if (pdfFileCopy.getFileInput() == null && pdfFileCopy.getFileId() != null) {
try {
log.debug("Using fileId {} to get file content", pdfFileCopy.getFileId());
MultipartFile file = fileStorage.retrieveFile(pdfFileCopy.getFileId());
pdfFileCopy.setFileInput(file);
} catch (Exception e) {
throw new RuntimeException(
"Failed to resolve file by ID: " + pdfFileCopy.getFileId(), e);
}
}
// Case 2: For async requests, we need to make a copy of the MultipartFile
else if (async && pdfFileCopy.getFileInput() != null) {
try {
log.debug("Making persistent copy of uploaded file for async processing");
MultipartFile originalFile = pdfFileCopy.getFileInput();
String fileId = fileStorage.storeFile(originalFile);
// Store the fileId for later reference
pdfFileCopy.setFileId(fileId);
// Replace the original MultipartFile with our persistent copy
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
pdfFileCopy.setFileInput(persistentFile);
log.debug("Created persistent file copy with fileId: {}", fileId);
} catch (IOException e) {
throw new RuntimeException(
"Failed to create persistent copy of uploaded file", e);
}
}
processedArgs[i] = pdfFileCopy;
} else {
// For non-PDFFile objects, just pass the original reference
// If other classes need copy-on-write, add them here
processedArgs[i] = arg;
}
}
return processedArgs;
}
/**
* Processes arguments in-place for testing purposes This is similar to our original
* implementation before introducing copy-on-write It's only used in test environments to
* maintain test compatibility
* Processes arguments in-place to handle file resolution and async file persistence.
* This approach avoids type mismatch issues by modifying the original objects directly.
*
* @param originalArgs The original arguments
* @param async Whether this is an async operation
@ -356,10 +287,14 @@ public class AutoJobAspect {
private String getJobIdFromContext() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest request = attrs.getRequest();
return (String) request.getAttribute("jobId");
}
} catch (Exception e) {
log.debug("Could not retrieve job ID from context: {}", e.getMessage());
}
return null;
}
}
}

View File

@ -6,6 +6,8 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -28,6 +30,7 @@ public class JobResult {
private String error;
/** List of result files for jobs that produce files */
@JsonIgnore
private List<ResultFile> resultFiles;
/** Time when the job was created */

View File

@ -14,6 +14,8 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
@ -30,7 +32,6 @@ public class JobExecutorService {
private final TaskManager taskManager;
private final FileStorage fileStorage;
private final HttpServletRequest request;
private final ResourceMonitor resourceMonitor;
private final JobQueue jobQueue;
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
@ -39,14 +40,12 @@ public class JobExecutorService {
public JobExecutorService(
TaskManager taskManager,
FileStorage fileStorage,
HttpServletRequest request,
ResourceMonitor resourceMonitor,
JobQueue jobQueue,
@Value("${spring.mvc.async.request-timeout:1200000}") long asyncRequestTimeoutMs,
@Value("${server.servlet.session.timeout:30m}") String sessionTimeout) {
this.taskManager = taskManager;
this.fileStorage = fileStorage;
this.request = request;
this.resourceMonitor = resourceMonitor;
this.jobQueue = jobQueue;
@ -100,7 +99,10 @@ public class JobExecutorService {
String jobId = UUID.randomUUID().toString();
// Store the job ID in the request for potential use by other components
if (request != null) {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletRequest request = attrs.getRequest();
request.setAttribute("jobId", jobId);
// Also track this job ID in the user's session for authorization purposes
@ -119,6 +121,9 @@ public class JobExecutorService {
log.debug("Added job ID {} to user session", jobId);
}
}
} catch (Exception e) {
log.debug("Could not store job ID in request context: {}", e.getMessage());
}
// Determine which timeout to use
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;

View File

@ -1,6 +1,5 @@
package stirling.software.common.service;
import io.github.pixee.security.ZipSecurity;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -21,6 +20,8 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.ZipSecurity;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
@ -361,7 +362,8 @@ public class TaskManager {
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(zipFile.getBytes()))) {
ZipSecurity.createHardenedInputStream(
new ByteArrayInputStream(zipFile.getBytes()))) {
ZipEntry entry;
while ((entry = zipIn.getNextEntry()) != null) {
if (!entry.isDirectory()) {

View File

@ -29,7 +29,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
"type",
"principal",
"startDate",
"endDate");
"endDate",
"async");
@Override
public boolean preHandle(