前面我们完成了java项目开发的基本环境搭建,有了jdk的支撑,并辅以集成开发神器,接下来我们将正式开启Java工程化实践之路。本篇我们先手动完成项目的依赖引入、项目构建和打包部署这些操作,下一篇再换成gradle看有多方便。
多线程处理能力
首先,我们要在原来的基础上优化和完善代码功能,当我们一个目录中有太多图片时,原先的主线程处理加水印的操作将会变慢,为此需要引入多线程,让多个线程每人均摊下处理任务。
首先将main
方法中读取图片并调用加水印的操作封装到一个addWatermark
方法中:
java
/**
* description: 对一个图片文件添加水印
* @since: 1.0.1
* @author: Java小卷
* @date: 2024/3/8 20:57
* @Param srcImgFile: 图片文件
* @return: void
*/
public static void addWatermark(File srcImgFile) {
try ( OutputStream outImgStream = new FileOutputStream(DIST_DIR + File.separator + srcImgFile.getName())) {
Image srcImg = ImageIO.read(srcImgFile);//文件转化为图片
// 加水印
BufferedImage bufImg = doWaterMark(srcImg);
ImageIO.write(bufImg, getFileExt(srcImgFile.getName()), outImgStream);
outImgStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
现在main
方法要做的事情:
java
public static void main(String[] args) throws Exception {
// 输出目录
File distDir = new File(DIST_DIR);
if (!distDir.exists()) {
if (!distDir.mkdirs()) throw new RuntimeException("创建目标目录失败");
}
// 获取一个目录下所有文件
File dir = new File(SRC_DIR);
File[] files = dir.listFiles();
// 每个线程处理的文件数
int fileCount = files.length / threadCount;
Thread[] threads = new Thread[threadCount];
// 对每个线程要处理的开始结束位置进行分段
for (int i = 0; i < threadCount; i++) {
int startIndex = i * fileCount;
int tempEndIndex = startIndex + fileCount - 1;
if (i == threadCount - 1 && tempEndIndex < files.length - 1) {
tempEndIndex = files.length - 1;
}
final int endIndex = tempEndIndex;
threads[i] = new Thread(() -> {
for (int j = startIndex; j <= endIndex; j++) {
File file = files[j];
// 实际调用添加水印的方法
addWatermark(file);
}
});
}
long start = System.currentTimeMillis();
// 启动线程
for (int i = 0; i < threadCount; i++) {
threads[i].start();
}
for (int i = 0; i < threadCount; i++) {
threads[i].join();
}
double consumeSeconds = (double) (System.currentTimeMillis() - start) / 1000;
System.out.println("耗时:" + consumeSeconds);
}
把输入、输出目录以及线程处理数提取为常量以方便修改:
java
private static final String SRC_DIR = "E:/java_juan/gradle/note/imgs";
private static final String DIST_DIR = "E:/java_juan/gradle/note/imgs-watermark";
private static final int threadCount = 3;
执行完成:
这样我们就可以实现对一个目录下的所有图片进行多线程的批量加水印操作,以保护我们markdown笔记的著作权。执行发现存在两个问题:
- 重复执行,并不会覆盖先前处理过的图片(图片名称保持不变)
- 无法对gif动图进行每帧加水印,处理后它只是一个静态图片
引入外部类库
因此,我们可以借助第三方类库封装的功能帮我们修复上述两个问题。只是引入两个jar包,我们手动添加即可。在工程下创建一个lib
目录,添加两个类库,分别用于gif图片处理和文件操作:
对lib目录右键添加为类库,这样就把这两个类库引入到咱们的类路径了
修复和扩展功能
接下来,局部调整下代码。首先在main
方法一开始,判断如果存在输出目录,则先进行清空操作:
java
File distDir = new File(DIST_DIR);
if (distDir.exists()) {
// 调用common-io包中的工具类进行清空
FileUtils.cleanDirectory(distDir);
} else {
if (!distDir.mkdirs()) throw new RuntimeException("创建目标目录失败");
}
调整addWatermark
方法中的处理逻辑,实现对gif动图加水印的兼容:
java
/**
* description: 对一个图片文件添加水印(兼容gif动图)
* @since: 1.0.2
* @author: Java小卷
* @date: 2024/3/8 21:44
* @Param srcImgFile: 图片文件
* @return: void
*/
public static void addWatermark(File srcImgFile) {
try ( OutputStream outImgStream = new FileOutputStream(DIST_DIR + File.separator + srcImgFile.getName())) {
if (srcImgFile.getName().endsWith(".gif")) {
// gif处理
GifDecoder decoder = new GifDecoder();
decoder.read(srcImgFile.getAbsolutePath());
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
encoder.setRepeat(0);
encoder.start(outImgStream);
for (int i = 0; i < decoder.getFrameCount(); i++) {
encoder.addFrame(doWaterMark(decoder.getFrame(i)));
encoder.setDelay(decoder.getDelay(i));
}
encoder.finish();
} else {
// 非gif
Image srcImg = ImageIO.read(srcImgFile);//文件转化为图片
// 加水印
BufferedImage bufImg = doWaterMark(srcImg);
ImageIO.write(bufImg, getFileExt(srcImgFile.getName()), outImgStream);
outImgStream.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
测试发现,gif动图的加水印确实更加耗时了,因为需要对每一帧都执行加水印操作。这个程序还有需要改进的地方,虽然大家分的图片数量差不多均匀的,但是gif图片的比重决定了任务的繁重程度,应该在此基础上再把gif图片进行均摊,否则就出现了下面的情况:
优化思路
将gif图片单独过滤出来进行多线程的均摊,这会比之前任务比重分配不均的执行效率要高,即便有些gif图片的帧率差别较大。后面的版本会按照这个思路进行优化实现。如果希望对一个较大的gif图片也采用多线程的方式来处理加水印,这也是一个很好的议题,我们也可以实现几种方案来看看效率能否显著提升。
打成可运行jar包
为了从命令行接收参数来覆盖默认的设置,做如下调整。先去掉常量的final
修饰,同时调整下命名规则为驼峰写法。
java
private static String srcDir = "E:/java_juan/gradle/note/imgs";
private static String distDir = "E:/java_juan/gradle/note/imgs-watermark";
private static int threadCount = 3;
在main
方法开头,加上从命令行获取参数的判断,这里简单判断下长度为2则传入输入输出目录路径,长度为3则额外加上线程数。
java
// 判断命令行参数
if (args.length == 2) {
srcDir = args[0];
distDir = args[1];
} else if (args.length == 3) {
srcDir = args[0];
distDir = args[1];
threadCount = Integer.parseInt(args[2]);
}
System.out.println("src dir: " + srcDir);
System.out.println("dist dir: " + distDir);
System.out.println("thread count: " + threadCount);
接下来是打成jar包的操作:
先打开项目结构视图
找到主类,点ok
回到项目主页:
看到生成的jar包了
创建jar启动项
最后运行jar启动项,得到执行结果。
本篇我们演示了,不借助于项目构建工具的前提下,如何手动完成项目的构建和打包发布,发现还是挺繁琐的。这还好,我们项目依赖的jar包才两个,要是几十上百个呢,再加上依赖的版本繁多,非常难于管理维护,后续小卷将为大家介绍spring官方推荐的项目管理工具gradle,对我们项目的依赖、构建和发布做更好的管理。大家加油!