温馨提示:学习本文之前,需要对图片处理工具有一定的了解,可以看下我之前写的图片处理专栏
本文要介绍的内容很厉害,系好安全带,发车了~
不知道读者有没有使用过类似阿里云文件URL处理图片的功能
通过在原图链接后拼接图片处理参数,可以实现图片实时处理!!!听起来很厉害吧,那我们自己能否实现这一功能呢?
其实要实现起来也很简单 ^-^
在线图片处理的整体流程
在展开今天内容之前,先大概讲下整个接口的处理数据流是怎样的,让大家有个宏观的印象。
1、整体流程
2、图片服务(image service)接口处理流程
这里有个下载原图的步骤,由于图片服务并没有源文件,所以必须先将源文件拉取到本地,才能进行后续处理
由于请求会先经过存储网关,所以会解析原图的内网地址,通过内网带宽拉取图片,速度是很快的
分析阶段
图片服务收到请求,处理逻辑为:解析参数 -> 校验参数 -> 拼接处理命令 -> GM执行命令 -> 生成结果文件 -> 返回文件
所以关键步骤就是拼接处理命令
目前实现了:图片缩放、自适应方向、质量变换、格式转换、裁剪、水印等功能
一般最快速的实现方式就是循环将参数转为对应的命令,拼接字符串,但是这不符合“开闭原则”呀,有没有跟优雅的方式,可以实现功能扩展而不需要改动原逻辑呢?今天的主角——
装饰器模式
隆重登场~
什么是装饰器模式?
在不影响其他对象的情况下,以动态地给一个对象添加一些额外的职责。
该模式一般包含如下的角色:
- Component抽象构件角色:真实对象和装饰对象有相同的接口。这样,客户端对象就能够以与真实对象相同的方式同装饰对象交互
- ConcreteComponent具体构件角色:定义一个具体的对象,也可以给这个对象添加一些职责
- Decorator装饰角色:持有一个抽象构件的引用。装饰对象接受所有客户端的请求,并把这些请求转发给真实的对象。这样,就能在真实对象调用前后增加新的功能
- ConcreteDecorator具体装饰角色:负责给构件对象增加新的责任
代码实现
本次实现的URL图片处理参考阿里云的链接拼接方式
1、参数解析
假设现在有一个请求,请求链接为: xxx.test.com/test.jpg?s-…
该链接的图片处理参数部分为:s-oss-process=image/resize,m_mfit,w_100,h_100/quality,q_80/format,jpg
s-oss-process=image 主要是用于让网关识别这是图片处理的流量,将流量转发到具体的服务
所以实际获取到需要解析的参数为:resize,m_mfit,w_100,h_100/quality,q_80/format,jpg
观察参数可以发现,每个具体的功能用“/”分隔,功能中第一个逗号前面的是操作名,后面是操作的具体参数,核心代码如下:
// 这里假设imageProcess已经是上面需要解析的部分(resize,m_mfit,w_100,h_100/quality,q_80/format,jpg)
String[] params = imageProcess.split("/");
LinkedHashMap<String, String> paramsMap = new LinkedHashMap<>();
for (String paramsStr : params) {
String[] kv = paramsStr.split(",", 2);
if (kv.length != 2) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "处理参数拼接有误");
}
// 判断功能参数以及格式转换是否合法
if (!ImageProcessConstants.Action.containsAction(kv[0])) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, kv[0]+"为不支持的图片处理功能");
}
if (StringUtils.equals(kv[0], ImageProcessConstants.Action.FORMAT)) {
if (!ImageProcessConstants.Format.containsFormat(kv[1])) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, kv[1]+"为不支持的图片转换格式");
}
}
paramsMap.put(kv[0], kv[1]);
}
我们将URL解析成一个有序的Map:
- resize:m_mfit,w_100,h_100
- quality:q_80
- format:jpg
2、拼接处理命令
这里就用到了装饰器模式,先看看结构图:
1)定义图片处理顶层接口(Component)
/**
* 图片处理顶层接口,所有被包装类,包装类都继承这个接口
*/
public interface ImageProcess {
/**
* 获取图片处理命令
*/
ImageCommand getImageCommands();
}
2)定义具体的构建角色(ConcreteComponent)
/**
* 图片处理类的实现类
*/
public class ConvertImageProcess implements ImageProcess {
@Override
public ImageCommand getImageCommands() {
return new ConvertCommand();
}
}
3)定义图片处理的装饰角色(Decorator)
该对象持有ImageProcess
具体构建角色对象的引用,同时自己也继承ImageProcess
接口,通过构造方法接收具体的处理参数,重写接口的方法,实现对具体构件对象的功能增强。
/**
* 装饰器都抽象类,Decorator角色
*/
public class ImageProcessDecorator implements ImageProcess {
/**
* 被装饰对象
*/
private ImageProcess imageProcess;
/**
* 各具体装饰器对应的参数
*/
protected String params;
public ImageProcessDecorator(ImageProcess imageProcess, String params) {
this.imageProcess = imageProcess;
this.params = params;
}
@Override
public ImageCommand getImageCommands() {
return this.imageProcess.getImageCommands();
}
}
4)实现具体的装饰器角色(ConcreteDecorator)
这里只列举一个例子,其他实现也很类似
/**
* 质量转换装饰器
*/
public class QualityProcessDecorator extends ImageProcessDecorator {
public QualityProcessDecorator(ImageProcess imageProcess, String params) {
super(imageProcess, params);
}
@Override
public ImageCommand getImageCommands() {
int quality = this.parseQualityParams(params);
ImageCommand imageCommand = super.getImageCommands();
imageCommand.quality(quality);
return imageCommand;
}
/**
* 质量转换参数解析
*
* @param params 如:q_100
*/
private int parseQualityParams(String params) {
if (StringUtils.isBlank(params)) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "quality params is empty");
}
if (params.length() <= 2 || !params.startsWith("q_")) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "quality参数错误");
}
String value = params.substring(2);
int quality = NumberUtils.toInt(value, 0);
if (quality < 1 || quality > 100) {
throw new ParamValidException(ErrorCode.COMMON_ILLEGAL_PARAMS, "quality的取值范围是 [1,100]");
}
return quality;
}
}
外层通过第1步解析参数得到的Map,调用装饰器,生成具体的执行命令:
/**
* 获取图片组合命令
*
* @param params 参数
* @param srcPath 源文件路径
* @param distPath 目标文件路径
*/
public ImageCommand getImageComposeCommand(Map<String, String> params, String srcPath, String distPath) {
ImageProcess imageProcess = new ConvertImageProcess();
imageProcess = new AddImageProcessDecorator(imageProcess, srcPath);
for (Map.Entry<String, String> entry : params.entrySet()) {
switch (entry.getKey()) {
case ImageProcessConstants.Action.RESIZE:
imageProcess = new ResizeProcessDecorator(imageProcess, entry.getValue()); break;
case ImageProcessConstants.Action.AUTO_ORIENT:
imageProcess = new AutoOrientProcessDecorator(imageProcess, entry.getValue()); break;
case ImageProcessConstants.Action.CROP:
imageProcess = new CropProcessDecorator(imageProcess, entry.getValue()); break;
case ImageProcessConstants.Action.QUALITY:
imageProcess = new QualityProcessDecorator(imageProcess, entry.getValue()); break;
}
}
imageProcess = new AddImageProcessDecorator(imageProcess, distPath);
return imageProcess.getImageCommands();
}
至此,我们将URL传递的参数,解析为GraphicsMagick
的处理命令:convert ${srcPath} -resize 100x100^ -quality 80 ${disPath}.jpg
如果对上面的命令感觉很懵的话,可以看下:GraphicsMagick实现云服务商基础图片处理
3、生成结果文件
将第2步生成的图片处理命令丢给GM进程进行处理,就能得到结果文件啦~
这里引入了gm4java这个工具提供的进程池控制GM进程数(代码中注入即可:PooledGMService)
<dependency>
<groupId>com.sharneng</groupId>
<artifactId>gm4java</artifactId>
<version>1.1.1</version>
</dependency>
可以参考看下:Springboot集成GraphicsMagick
End
以上就是在线图片处理的整个处理过程,是不是发现逻辑其实很简单,使用装饰器模式很方便的让我们后续增加更多的功能。这就是具体设计模式在实际项目中的应用!
后续对服务压测结束后,可能会输出关于结果文件采用 普通方式 和 “零拷贝”方式 响应给调用方的文章,大家有兴趣可以点个关注留意下~