基于责任链模式实现图片下载

代码已开源到codeberg

https://codeberg.org/meowrain/ImageDownloader/

效果:

过滤出大小为 1mb ~ 5mb的图片

image-20250519191200764

image-20250519191243144

什么是责任链模式?

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

责任链模式结构

image-20250519192120876

责任链使用场景

  • 当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。
  • 当必须按顺序执行多个处理者时, 可以使用该模式。
  • 如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。

构建图片下载器

目录结构

image-20250519193004190

handler

我们先创建Handler接口,包含setNext,handle,process方法

package cn.meowrain.handler;

import cn.meowrain.req.ImageDownloadRequest;

/**
 * 处理器接口,定义了责任链模式中的处理节点行为
 *
 * <p>该接口用于构建一个处理图片下载请求的责任链,
 * 每个处理器可以选择处理请求或将其传递给链中的下一个处理器</p>
 */
public interface Handler {

    /**
     * 设置责任链中的下一个处理器
     *
     * @param handler 下一个处理器实例
     * @return 当前处理器实例(便于链式调用)
     */
    void setNextHandler(Handler handler);

    /**
     * 获取责任链中的下一个处理器
     *
     * @return 下一个处理器实例,如果没有则返回null
     */
    Handler getNextHandler();

    /**
     * 处理请求的入口方法
     *
     * <p>实现类应在此方法中决定是自己处理请求,
     * 还是将其传递给下一个处理器</p>
     *
     * @param request 图片下载请求对象
     */
    void handle(ImageDownloadRequest request);

    /**
     * 实际处理请求的核心方法
     *
     * <p>实现类应在此方法中完成具体的请求处理逻辑</p>
     *
     * @param request 图片下载请求对象
     * @return 处理结果:true表示处理成功,false表示处理失败
     */
    boolean process(ImageDownloadRequest request);
}

接下来要写一个抽象类,来实现基本方法,比如getNextHandlerhandlesetNextHandler方法

package cn.meowrain.handler;

import cn.meowrain.req.ImageDownloadRequest;

public abstract class BaseHandler implements Handler {
    private Handler nextHandler;

    @Override
    public void setNextHandler(Handler nextAbstractHandler) {
        this.nextHandler = nextAbstractHandler;
    }

    @Override
    public Handler getNextHandler() {
        return nextHandler;
    }

    @Override
    public void handle(ImageDownloadRequest request) {
        if (process(request)) {
            // 如果当前责任类处理成功,且还有nextHandler,就传给下一个handler处理
            if (getNextHandler() != null) {
                nextHandler.handle(request);
            } else {
                // 链条处理完毕,请求成功通过所有环节(如果 SaveHandler 是最后一个,这里表示已保存)
                System.out.println("--- Request " + request.getId().substring(0, 6) + "... successfully processed by the chain. ---");
            }
        } else {
            System.out.println("--- Request " + request.getId().substring(0, 6) + "... failed to process by the chain. ---");
            return;
        }
    }

}

接下来实现具体处理的handler类

SizeCheckHandler

package cn.meowrain.handler;

import cn.meowrain.req.ImageDownloadRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SizeCheckHandler extends BaseHandler {
    private static final long MIN_SIZE_BYTES = 1 * 1024 * 1024;
    private static final long MAX_SIZE_BYTES = 5 * 1024 * 1024; // 500kB
    private static final Logger logger = LoggerFactory.getLogger(SizeCheckHandler.class);

    @Override
    public boolean process(ImageDownloadRequest request) {
        System.out.println(getClass().getSimpleName() + " checking size for " + request.getId().substring(0, 6) + "... (Size: " + request.getSizeInBytes() + " bytes)");
        if (request.getSizeInBytes() < MAX_SIZE_BYTES && request.getSizeInBytes() >= MIN_SIZE_BYTES) {
            logger.info("  Size Check PASSED: " + request.getSizeInBytes() + " bytes is < 5MB.");
            return true; // 小于 5MB,继续传递给下一个处理者
        } else {
            logger.error("  Size Check FAILED: " + request.getSizeInBytes() + " bytes is >= 5MB.");
            return false; // 大于等于 5MB,中断链条
        }
    }
}

ImageTypeCheckHandler

package cn.meowrain.handler;

import cn.meowrain.req.ImageDownloadRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ImageTypeCheckHandler extends BaseHandler {
    private static final Logger logger = LoggerFactory.getLogger(ImageTypeCheckHandler.class);

    @Override
    public boolean process(ImageDownloadRequest request) {
        System.out.println(getClass().getSimpleName() + " checking type for " + request.getId().substring(0, 6) + "... (Type: " + request.getContentType() + ")");
        if ("image/webp".equalsIgnoreCase(request.getContentType())
                || "image/png".equalsIgnoreCase(request.getContentType())
                || "image/jpeg".equalsIgnoreCase(request.getContentType())) {
            logger.info("  Type Check PASSED: " + request.getContentType() + " is image.");
            return true; // 是 webp,jpeg或者Png,继续传递给下一个处理者
        } else {
            logger.error("  Type Check FAILED: " + request.getContentType() + " is NOT image.");
            return false; // 不是 webp,jpeg或者Png,中断链条
        }
    }
}

ImageSaveHandler

package cn.meowrain.handler;

import cn.meowrain.Main;
import cn.meowrain.req.ImageDownloadRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class ImageSaveHandler extends BaseHandler {
    private static final String UPLOAD_DIR = "uploads";
    private static final Logger logger = LoggerFactory.getLogger(ImageSaveHandler.class);

    public ImageSaveHandler() {
        File uploadDir = new File(UPLOAD_DIR);
        if (!uploadDir.exists()) {
            boolean created = uploadDir.mkdirs();
            if (!created) {
                System.err.println("Error creating upload directory: " + UPLOAD_DIR);
            }
        }
    }

    @Override
    public boolean process(ImageDownloadRequest request) {
        // 如果请求到达这里,说明它已经通过了前面的所有检查(类型和大小)
        System.out.println(getClass().getSimpleName() + " saving image for " + request.getId().substring(0, 6) + "...");

        String filename = request.getId() + "." + request.getContentType().split("/")[1];
        File file = new File(UPLOAD_DIR, filename);

        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(request.getImageData());
            logger.info("  Image SAVED successfully: " + file.getAbsolutePath());
            return true; // 保存成功,链条处理完成
        } catch (IOException e) {
            // 保存失败
            logger.error("  Error SAVING image " + request.getId().substring(0, 6) + "...: " + e.getMessage());
            // 保存失败,中断链条(虽然已经是最后一个了,但体现处理失败)
            return false;
        }
    }
}


现在有个问题:如果单纯这样的话,我们需要new好几个handler然后再分别给他们设置nextHandler,这就很繁杂了

        Handler handler1 = new ImageTypeCheckHandler();
        Handler handler2 = new SizeCheckHandler();
        Handler handler3 = new ImageSaveHandler();
        handler1.setNextHandler(handler2);
        handler2.setNextHandler(handler3);

那我们就可以利用链表来构建一个链了

ChainBuilder

package cn.meowrain.builder;

import cn.meowrain.handler.Handler;

public class ChainBuilder {
    private Handler head = null; // 链条的头部
    private Handler tail = null; // 链条的尾部

    public static ChainBuilder newBuilder() {
        return new ChainBuilder();
    }

    public ChainBuilder addHandler(Handler handler) {
        if (head == null) {
            head = handler;
            tail = handler;
        } else {
            tail.setNextHandler(handler);
            tail = handler;
        }
        return this;
    }

    public Handler build() {
        return head;
    }
}

现在我们有这个类了,直接用

 Handler handlers = ChainBuilder.newBuilder().addHandler(new ImageTypeCheckHandler()).addHandler(new SizeCheckHandler()).addHandler(new ImageSaveHandler()).build();

就能构建完整的handler链了

ImageDownloadRequest

用这个对象保存图片的基本信息,传给责任链进行处理

package cn.meowrain.req;

import java.util.UUID;

public class ImageDownloadRequest {
    private final byte[] imageData;
    private final String contentType;
    private final long sizeInBytes;
    private final String originalUrl;
    private final String id = UUID.randomUUID().toString();

    public ImageDownloadRequest(byte[] imageData, String contentType, long sizeInBytes, String originalUrl) {
        this.imageData = imageData;
        this.contentType = contentType;
        this.sizeInBytes = sizeInBytes;
        this.originalUrl = originalUrl;
    }

    public byte[] getImageData() {
        return imageData;
    }

    public String getContentType() {
        return contentType;
    }

    public long getSizeInBytes() {
        return sizeInBytes;
    }

    public String getOriginalUrl() {
        return originalUrl;
    }

    public String getId() {
        return id;
    }
    // 方便日志输出的摘要信息
    public String toStringSummary() {
        return String.format("ID: %s, Type: %s, Size: %d bytes (%.2f MB), Source: %s",
                id.substring(0, 6) + "...", // 缩短ID
                contentType,
                sizeInBytes,
                sizeInBytes / (1024.0 * 1024.0),
                originalUrl);
    }
}

Downloader

package cn.meowrain.utils;

import cn.meowrain.req.ImageDownloadRequest;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Optional;

// 需要 ImageDownloadRequest 类,确保它在同一个包或者被正确导入
// import ImageDownloadRequest;

public class ImageDownloader {

    // 推荐使用一个 HttpClient 实例,因为它管理连接池等资源
    // 使用 static final 确保它只创建一次
    private static final HttpClient client = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2) // 可选:优先使用 HTTP/2
            .followRedirects(HttpClient.Redirect.NORMAL) // 自动处理重定向
            .connectTimeout(Duration.ofSeconds(10)) // 设置连接超时时间
            .build();

    // 私有构造函数,阻止外部直接创建实例(因为我们使用静态方法下载)
    private ImageDownloader() {
    }

    /**
     * 从指定的 URL 下载图片,并封装成 ImageDownloadRequest 对象。
     *
     * @param imageUrl 要下载图片的 URL
     * @return 包含图片数据、类型、大小等的 ImageDownloadRequest 对象,如果下载失败则返回 null。
     */
    public static ImageDownloadRequest downloadImage(String imageUrl) {
        // 将字符串 URL 转换为 URI 对象
        URI uri = URI.create(imageUrl);

        // 构建 HTTP GET 请求
        HttpRequest request = HttpRequest.newBuilder()
                .uri(uri)
                .timeout(Duration.ofSeconds(20)) // 设置读取超时时间
                .header("User-Agent", "Java HttpClient Image Downloader Example") // 一个友好的 User-Agent
                .build();

        System.out.println("Attempting to download from: " + imageUrl);

        try {
            // 发送请求并获取响应,响应体直接作为 byte[]
            HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());

            int statusCode = response.statusCode();
            // 检查 HTTP 状态码,200 表示成功
            if (statusCode != 200) {
                System.err.println("Download failed for " + imageUrl + ". HTTP Status code: " + statusCode);
                return null; // 下载失败,返回 null
            }

            // 获取 Content-Type 头部,用于判断图片类型
            Optional<String> contentTypeOptional = response.headers().firstValue("Content-Type");
            // 如果 Content-Type 头部不存在,使用默认值 "unknown"
            String contentType = contentTypeOptional.orElse("unknown");

            // 获取响应体数据 (图片数据)
            byte[] imageData = response.body();
            // 获取图片大小
            long sizeInBytes = imageData.length;

            // 重要:获取经过重定向后的最终 URL,作为请求的原始 URL 来源记录
            String finalUrl = response.uri().toString();

            // 创建 ImageDownloadRequest 对象
            ImageDownloadRequest downloadRequest = new ImageDownloadRequest(
                    imageData,
                    contentType,
                    sizeInBytes,
                    finalUrl // 使用最终的 URL
            );

            System.out.println("Successfully downloaded image summary: " + downloadRequest.toStringSummary());
            return downloadRequest;

        } catch (IOException | InterruptedException e) {
            // 处理 IO 异常或线程中断异常
            System.err.println("Error during image download from " + imageUrl + ": " + e.getMessage());
            // 打印堆栈跟踪,以便调试
            // e.printStackTrace();
            return null; // 下载失败,返回 null
        }
    }
}

启动类

package cn.meowrain;

import cn.meowrain.builder.ChainBuilder;
import cn.meowrain.handler.Handler;
import cn.meowrain.handler.ImageSaveHandler;
import cn.meowrain.handler.ImageTypeCheckHandler;
import cn.meowrain.handler.SizeCheckHandler;
import cn.meowrain.req.ImageDownloadRequest;
import cn.meowrain.utils.ImageDownloader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
    // 使用 Runtime.getRuntime().availableProcessors() 获取 CPU 可用核心数
    private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();

    private static final int NUM_IMAGES_TO_FETCH = 20; // 模拟拉取的图片数量

    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        Handler handlers = ChainBuilder.newBuilder().addHandler(new ImageTypeCheckHandler()).addHandler(new SizeCheckHandler()).addHandler(new ImageSaveHandler()).build();
        ExecutorService executorService = Executors.newFixedThreadPool(NUM_THREADS);
        System.out.println("Starting image fetching and processing with " + NUM_THREADS + " threads (CPU Cores)...");

        // 3. 模拟提交图片下载和处理任务 (这部分不变)
        for (int i = 0; i < NUM_IMAGES_TO_FETCH; i++) {
            final int taskIndex = i + 1;
            executorService.submit(() -> {
                ImageDownloadRequest request = ImageDownloader.downloadImage("https://www.dmoe.cc/random.php");
                System.out.println("Thread " + Thread.currentThread().getName() + " submitting request " + request.toStringSummary() + " to chain.");
                handlers.handle(request);
            });
        }
        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(1, TimeUnit.HOURS)) {
                System.err.println("Executor did not terminate in the specified time.");
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("Waiting for executor termination interrupted: " + e.getMessage());
            executorService.shutdownNow();
        }

        System.out.println("\nAll image processing tasks finished.");
        System.out.println("--- CPU Cores Thread Pool Example End ---");
    }
}

image-20250519193913257