Backend Development 12 min read

SpringBoot Image Processing with Thumbnailator, FastDFS, and Asynchronous Thread Pool

This article demonstrates how to integrate Thumbnailator for image watermarking and thumbnail generation, configure a SpringBoot thread pool, and use asynchronous @Async methods together with FastDFS storage to efficiently handle bulk image uploads in a backend service.

Top Architect
Top Architect
Top Architect
SpringBoot Image Processing with Thumbnailator, FastDFS, and Asynchronous Thread Pool

Environment

SpringBoot + FastDFS + Thumbnailator are used. The FastDFS environment must be set up separately.

Thumbnailator Dependency

<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.8</version>
</dependency>

Utility Class for Image Processing

import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.geometry.Positions;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;

@Component
public class PictureUtil {
    private static File markIco = null;
    static {
        try {
            markIco = new File(new File("").getCanonicalPath() + "/icon.png");
            LogUtil.info(PictureUtil.class, "Watermark image load " + (markIco.exists() ? "success" : "fail"));
        } catch (Exception e) {}
    }
    /** Add watermark */
    public void photoMark(File sourceFile, File toFile) throws IOException {
        Thumbnails.of(sourceFile)
            .size(600, 450)
            .watermark(Positions.BOTTOM_CENTER, ImageIO.read(markIco), 0.7f)
            .toFile(toFile);
    }
    /** Generate thumbnail */
    public void photoSmaller(File sourceFile, File toFile) throws IOException {
        Thumbnails.of(sourceFile)
            .size(200, 150)
            .outputQuality(0.4f)
            .toFile(toFile);
    }
    /** Generate video thumbnail (not used yet) */
    public void photoSmallerForVedio(File sourceFile, File toFile) throws IOException {
        Thumbnails.of(sourceFile)
            .size(440, 340)
            .watermark(Positions.BOTTOM_CENTER, ImageIO.read(markIco), 0.1f)
            .outputQuality(0.8f)
            .toFile(toFile);
    }
}

SpringBoot Thread Pool Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class PoolConfig {
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.initialize();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(32);
        executor.setQueueCapacity(512);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("ThreadPool-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }
}

Key Async Notes

The controller class must be annotated with @EnableAsync .

The service method that should run asynchronously must be annotated with @Async .

Calls to an @Async method from within the same class will not be asynchronous; the call must come from another bean (e.g., controller → service).

Controller Endpoint for Image Upload

@ApiOperation("上传业务图片")
@PostMapping("/push/photo/{id}/{name}")
public R pushHousingPhotoMethod(@ApiParam("SourceId") @PathVariable Integer id,
                                @ApiParam("图片名称不约束,可不填则使用原名,可使用随机码或原名称,但必须带扩展名")
                                @PathVariable(required = false) String name,
                                @RequestParam MultipartFile file) throws InterruptedException, ExecutionException, IOException {
    String fileName = file.getOriginalFilename();
    String ext = StringUtils.substring(fileName, fileName.lastIndexOf('.'), fileName.length());
    File tempPhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
    file.transferTo(tempPhoto);
    service.pushPhoto(id, name, tempPhoto);
    return new R();
}

Utility for Generating 32‑Bit UUID

/** Generate a 32‑character UUID without hyphens */
public synchronized static String make32BitUUID() {
    return UUID.randomUUID().toString().replace("-", "");
}

Service Layer Implementation (Async)

@Async
public void pushHousingPhoto(Integer id, String name, File file) throws InterruptedException, ExecutionException, IOException {
    Long startTime = System.currentTimeMillis();
    Integer[] numb = fastDfsService.upLoadPhoto(StringUtils.isBlank(name) ? file.getName() : name, file).get();
    SourcePhotosContext context = new SourcePhotosContext();
    context.setSourceId(id);
    context.setNumber(numb[0]);
    context.setNumber2(numb[1]);
    sourcePhotosContextService.insertNew(context);
    Long endTime = System.currentTimeMillis();
    LogUtil.info(this.getClass(), "source [ " + id + " ] 绑定图片 [ " + name + " ] 成功,内部处理耗时 [" + (endTime - startTime) + "ms]");
}

FastDFS Service (Future‑based)

@Override
public Future
upLoadPhoto(String fileName, MultipartFile file) throws IOException {
    String ext = StringUtils.substring(fileName, fileName.lastIndexOf('.'));
    File sourcePhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
    file.transferTo(sourcePhoto);
    return upLoadPhoto(fileName, sourcePhoto);
}

@Override
public Future
upLoadPhoto(String fileName, File sourcePhoto) throws IOException {
    String ext = StringUtils.substring(fileName, fileName.lastIndexOf('.'));
    File markedPhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
    File smallerPhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
    pictureUtil.photoMark(sourcePhoto, markedPhoto);
    pictureUtil.photoSmaller(markedPhoto, smallerPhoto);
    Integer markedPhotoNumber = upLoadPhotoCtrl(fileName, markedPhoto);
    Integer smallerPhotoNumber = upLoadPhotoCtrl("mini_" + fileName, smallerPhoto);
    sourcePhoto.delete();
    markedPhoto.delete();
    smallerPhoto.delete();
    Integer[] res = new Integer[]{markedPhotoNumber, smallerPhotoNumber};
    return new AsyncResult(res);
}

The article also includes diagrams illustrating the upload flow and notes that the asynchronous approach avoids blocking the client while the server processes images.

Overall, the guide provides a practical, end‑to‑end solution for asynchronous image handling in a SpringBoot backend, covering dependencies, utility code, thread‑pool setup, controller/service design, and common pitfalls.

BackendThreadPoolSpringBootAsyncImageProcessingfastdfsThumbnailator
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login 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.