Asynchronous Excel Import with Thread Pool and AOP in Java

This article explains how to handle large Excel uploads in Java by offloading parsing and validation to a thread pool, using the Template Method pattern to reduce boilerplate, and finally applying a custom AOP annotation to manage logging, error handling, and asynchronous execution in a clean, reusable way.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Asynchronous Excel Import with Thread Pool and AOP in Java

Many systems face the problem that uploading a large Excel file and parsing it synchronously can cause time‑outs and prevent error messages from being returned to the front‑end.

To avoid this, the author first suggests launching a separate thread for the parsing work while the main thread returns immediately, and proposes using the Template Method design pattern to encapsulate common logging and thread‑pool logic.

@Slf4j
public abstract class AbstractUploadService<T> {
    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());

    protected abstract String upload(List<T> data);

    protected void execute(String userName, List<T> data) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        uploadExecuteService.submit(() -> {
            // log start
            writeLogToDb(uuid, userName, updateTime, "导入中");
            String errorLog = "";
            try {
                errorLog = upload(data);
                writeSuccess(uuid, "导入中", updateTime);
            } catch (Exception e) {
                LOGGER.error("导入错误", e);
                writeFailToDb(uuid, "导入失败", e.getMessage(), updateTime);
            }
            if (StringUtils.isNotEmpty(errorLog)) {
                writeFailToDb(uuid, "导入失败", errorLog, updateTime);
            }
        });
    }
}

Although this reduces duplicated code, two issues remain: the abstract upload method fixes the parameter type, and every service still needs to extend the abstract class.

To solve these, the author creates a custom annotation and an AOP aspect that mimics transaction handling, allowing any method annotated with @Upload to be wrapped with the same logging and asynchronous execution logic.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Upload {
    UploadType type() default UploadType.未知;
}
@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(value = "uploadPoint()")
    public Object uploadControl(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Upload annotation = signature.getMethod().getAnnotation(Upload.class);
        UploadType type = annotation == null ? UploadType.未知 : 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("导入失败:", 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 outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            outputStream.write(message.getBytes());
        } catch (Exception e) {
            LOGGER.error("写入文件错误", e);
        } finally {
            try { if (outputStream != null) outputStream.close(); } catch (Exception e) { LOGGER.error("关闭错误", e); }
        }
        upFileToS3(file, s3Key);
        writeFailToDB(batchNo, s3Key, fileName);
        deleteFile(file);
    }
}

Supporting classes such as the log entity and the UploadType enum are also defined to store upload metadata and categorize the source of the upload.

public class FileUploadLog {
    private Integer id;
    private String batchNo; // unique code
    private String key; // file key on storage
    private String fileName;
    private Integer status;
    private String createName;
    private String uploadType;
    private Date endTime;
    private Date startTime;
}

public enum UploadType {
    未知(1, "未知"),
    类型2(2, "类型2"),
    类型1(3, "类型1");
    private int code;
    private String desc;
    private static final Map<Integer, UploadType> map = new HashMap<>();
    static { for (UploadType v : 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); }
}

To use the solution, a service method simply adds the @Upload(type = UploadType.类型1) annotation and returns an error string when validation fails; the aspect handles the rest.

@Upload(type = UploadType.类型1)
public String upload(List<ClassOne> items) {
    if (items == null || items.isEmpty()) return;
    String error = uploadCheck(items);
    if (StringUtils.isNotEmpty(error)) return error;
    deleteAll();
    batchInsert(items);
}

The author concludes that this lightweight “wheel” greatly improves team productivity by abstracting repetitive asynchronous upload logic, allowing developers to focus on business code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaaopfile uploaddesign patternthread poolasynchronous processing
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.