How to Build an Asynchronous Excel Upload Service with AOP and Thread Pools
This article explains how to handle time‑consuming Excel uploads by using a template‑method design, wrapping the process with an AOP aspect, and executing the actual upload asynchronously in a thread pool, while recording logs and error details for later inspection.
Background
Many systems allow users to upload Excel files, which are parsed on the backend, validated, and stored in the database.
If the Excel contains many rows or complex parsing logic, the parsing and validation can become time‑consuming, causing synchronous APIs to time out and preventing error messages from being returned to the front end.
A common solution is to start a child thread for parsing, return immediately from the main thread, and let the child thread record the upload status and validation results in the database, while a separate page queries the status.
Writing such thread‑pool and logging code for every upload task is repetitive and pollutes business logic, reducing readability.
From a generic perspective, this scenario fits the Template Method design pattern: define an abstract class with an abstract upload method and provide common logging functionality.
However, the template approach still has two drawbacks: the upload method’s parameter structure is fixed, making changes difficult; and each upload service still needs to extend the abstract class, which is not very convenient.
To address these issues, the author proposes using an AOP‑based wrapper that treats the upload process similarly to a transaction: perform initialization before the actual operation, and handle success or failure afterwards.
Implementation
First, define a log entity:
public class FileUploadLog {
private Integer id;
private String batchNo; // unique code
private String key; // file key on file server
private String fileName; // error log file name
private Integer status; // upload status
private String createName; // uploader
private String uploadType;
private Date endTime;
private Date startTime;
}Then define an enum to record the upload type:
public enum UploadType {
UNKNOWN(1, "未知"),
TYPE2(2, "类型2"),
TYPE1(3, "类型1");
private int code;
private String desc;
private static Map<Integer, UploadType> map = new HashMap<>();
static {
for (UploadType v : UploadType.values()) {
map.put(v.code, v);
}
}
UploadType(int code, String desc) { this.code = code; this.desc = desc; }
public int getCode() { return code; }
public String getDesc() { return desc; }
public static UploadType getByCode(Integer code) { return map.get(code); }
}Define an annotation to mark upload methods:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Upload {
UploadType type() default UploadType.UNKNOWN;
}Implement an aspect that creates a thread pool, records a log entry, executes the annotated method asynchronously, and writes success or failure information back to the database:
@Component
@Aspect
@Slf4j
public class UploadAspect {
public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("upload-pool-%d")
.setPriority(Thread.NORM_PRIORITY)
.build();
public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(
10, 20, 300L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1024), commonThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
@Pointcut("@annotation(com.aaa.bbb.Upload)")
public void uploadPoint() {}
@Around("uploadPoint()")
public Object uploadControl(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Upload annotation = signature.getMethod().getAnnotation(Upload.class);
UploadType type = annotation == null ? UploadType.UNKNOWN : annotation.type();
String batchNo = UUID.randomUUID().toString().replace("-", "");
writeLogToDB(batchNo, type, new Date());
uploadExecuteService.submit(() -> {
try {
String errorMessage = (String) pjp.proceed();
if (StringUtils.isEmpty(errorMessage)) {
writeSuccessToDB(batchNo);
} else {
fail(errorMessage, batchNo);
}
} catch (Throwable e) {
LOGGER.error("Import failed:", e);
fail(e.toString(), batchNo);
}
});
return new Object();
}
private void fail(String message, String batchNo) {
String s3Key = UUID.randomUUID().toString().replace("-", "");
String fileName = "错误日志_" + DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH时mm分ss秒") + ExportConstant.txtSuffix;
String filePath = "/home/xxx/xxx/" + fileName;
File file = new File(filePath);
OutputStream os = null;
try {
os = new FileOutputStream(file);
os.write(message.getBytes());
} catch (Exception e) {
LOGGER.error("Write file error", e);
} finally {
try { if (os != null) os.close(); } catch (Exception e) { LOGGER.error("Close error", e); }
}
upFileToS3(file, s3Key);
writeFailToDB(batchNo, s3Key, fileName);
deleteFile(file);
}
}Finally, use the annotation on a service method; the method only needs to return an error string when validation fails:
@Upload(type = UploadType.TYPE1)
public String upload(List<ClassOne> items) {
if (items == null || items.isEmpty()) {
return;
}
// validation
String error = uploadCheck(items);
if (StringUtils.isNotEmpty(error)) {
return error;
}
deleteAll();
batchInsert(items);
}Conclusion
The AOP‑based “upload wheel” simplifies asynchronous Excel uploads, improves team productivity, and keeps business code clean.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
