基于责任链模式实现图片下载
代码已开源到codeberg
效果:
过滤出大小为 1mb ~ 5mb的图片
什么是责任链模式?
责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。
责任链模式结构
责任链使用场景
- 当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时, 可以使用责任链模式。
- 当必须按顺序执行多个处理者时, 可以使用该模式。
- 如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。
构建图片下载器
目录结构
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);
}
接下来要写一个抽象类,来实现基本方法,比如getNextHandler
和handle
,setNextHandler
方法
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 ---");
}
}