Decoupling Image Thumbnail Generation with the Bridge Pattern
To generate and store image thumbnails, the system must support multiple input sources—such as local files or remote URLs—and various output destinations, including local sttorage, MinIO, Alibaba Cloud OSS, or Qiniu Cloud. Given that both input and output strategies may evolve independently, a flexible design is essential.
The solution leverages the Bridge pattern to decouple abstraction from implementation. This allows new loading or saving mechanisms to be added without modifying existing application logic. The core idea is to define two parallel hierarchies: one for loading images (ThumbLoad) and another for saving them (ThumbSave), with composition linking the two at runtime.
Abstraction: Image Loading
The base ThumbLoad class defines the contract for loading an image and delegates saving to a ThumbSave instance:
public abstract class ThumbLoad {
protected ThumbSave saver;
public ThumbLoad(ThumbSave saver) {
this.saver = saver;
}
protected boolean success = false;
protected String errorMessage = null;
public abstract ImageActionVo process(String source);
protected boolean isUrlAccessible(String url, boolean requireImageType) {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setConnectTimeout(2000);
int status = conn.getResponseCode();
if (status != 200) return false;
if (requireImageType) {
String contentType = conn.getContentType();
if (!contentType.startsWith("image/")) return false;
}
return true;
} catch (Exception e) {
return false;
} finally {
// disconnect logic omitted for brevity
}
}
}
Implementation: Image Saving
The ThumbSave hierarchy handles where and how the processed thumbnail is stored:
public abstract class ThumbSave {
protected final String outputPath;
protected final int targetWidth;
protected final int targetHeight;
public ThumbSave(String outputPath, int width, int height) {
this.outputPath = outputPath;
this.targetWidth = width;
this.targetHeight = height;
}
public abstract SaveResult persist(InputStream originalStream);
public static class SaveResult {
public final boolean success;
public final String uri;
public final String message;
public SaveResult(boolean success, String uri, String message) {
this.success = success;
this.uri = uri;
this.message = message;
}
}
}
Concrete Loaders
Local file loader:
public class LocalImageLoader extends ThumbLoad {
public LocalImageLoader(ThumbSave saver) {
super(saver);
}
@Override
public ImageActionVo process(String filePath) {
File file = new File(filePath);
if (!file.exists()) {
return new ImageActionVo(filePath, false, null, "File not found");
}
try (InputStream in = new FileInputStream(file)) {
SaveResult result = saver.persist(in);
return new ImageActionVo(filePath, result.success, result.uri, result.message);
} catch (IOException e) {
return new ImageActionVo(filePath, false, null, e.getMessage());
}
}
}
Remote URL loader:
public class RemoteImageLoader extends ThumbLoad {
public RemoteImageLoader(ThumbSave saver) {
super(saver);
}
@Override
public ImageActionVo process(String imageUrl) {
if (!isUrlAccessible(imageUrl, true)) {
return new ImageActionVo(imageUrl, false, null, "URL inaccessible or not an image");
}
try {
URL url = new URL(imageUrl);
try (InputStream in = url.openStream()) {
SaveResult result = saver.persist(in);
return new ImageActionVo(imageUrl, result.success, result.uri, result.message);
}
} catch (IOException e) {
return new ImageActionVo(imageUrl, false, null, e.getMessage());
}
}
}
Concrete Savers
Local disk saver:
public class LocalThumbnailSaver extends ThumbSave {
public LocalThumbnailSaver(String path, int w, int h) {
super(path, w, h);
}
@Override
public SaveResult persist(InputStream in) {
try {
Thumbnails.of(in).size(targetWidth, targetHeight).toFile(outputPath);
return new SaveResult(true, outputPath, null);
} catch (IOException e) {
return new SaveResult(false, null, e.getMessage());
}
}
}
Cloud storage saver (via internal file service):
public class CloudThumbnailSaver extends ThumbSave {
public CloudThumbnailSaver(String hintPath, int w, int h) {
super(hintPath, w, h);
}
@Override
public SaveResult persist(InputStream in) {
try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
Thumbnails.of(in).size(targetWidth, targetHeight).toOutputStream(buffer);
byte[] data = buffer.toByteArray();
try (InputStream uploadStream = new ByteArrayInputStream(data)) {
MockMultipartFile file = new MockMultipartFile("thumb.jpg", "thumb.jpg",
MediaType.IMAGE_JPEG_VALUE, uploadStream);
R response = fileFeignClient.upload("thumb_images", file);
if (response.isSuccess()) {
String cloudUrl = (String) response.get("data");
return new SaveResult(true, cloudUrl, null);
} else {
return new SaveResult(false, null, response.getMessage());
}
}
} catch (IOException e) {
return new SaveResult(false, null, e.getMessage());
}
}
}
This architecture cleanly separates concerns: loaders handle source acquiistion, savers manage destination logic, and the bridge between them enables any loader to work with any saver. New storage backends or input types can be introduced without cascadnig changes across the system.