提供可重用的、开源的Java代码类库Apache Commons

Apache Commons包含了很多开源的工具,用于解决平时编程经常会遇到的问题,减少重复劳动。我选了一些比较常用的项目做简单介绍。

Commons BeanUtils

针对Bean的一个工具集。由于Bean往往是由一堆get和set组成,所以BeanUtils也是在此基础上进行一些包装。 

一个比较常用的功能是Bean Copy,也就是copy bean的属性。如果做分层架构开发的话就会用到,比如从PO(Persistent Object)拷贝数据到VO(Value Object)。

pom依赖:

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

常用类:

  • BeanUtils

Commons Codec

Apache Commons Codec提供了许多编解码相关的工具类。Codec目前最新版本是1.15,最低要求Java7以上。

pom.xml依赖

<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>

常用 API

常用的类有

  • Base64编码和解码:Base64
  • Hex编码和解码:Hex
  • 加密工具类:DigestUtils
  • URL编码和解码:URLCodec 

二进制相关

二进制包主要提供16进制、Base64、Base32等的编解码工具类。

16进制(Hex类)

十六进制常用于将二进制以更简短的方式展示,比如MD5是128位,展现起来太长,而转换为16进制后只需要32个字符即可表示出来。

// byte数组转为16进制字符串
String hex = Hex.encodeHexString("123".getBytes());
System.out.println(hex);
// 16进制字符串解码
byte[] src = Hex.decodeHex(hex);
System.out.println(new String(src));

Base64,Base32,Base16

Base64是网络上最常见的用于传输二进制数据的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。Base32就是使用32个可打印字符,Base16就是使用16个(实际上相当于16进制)。
// base64编码
String base64 = Base64.encodeBase64String("测试".getBytes());
System.out.println(base64);
// base64解码
byte[] src = Base64.decodeBase64(base64);
System.out.println(new String(src));
// 字符串是否是base64
Base64.isBase64(base64);

// Base32 Base16 同理
Codec还提供了Base系列的流处理,以流的方式去处理Base编解码
// 以流方式提供Base64编码和解码
// 附:"123"的base64编码为"MTIz"

// 对输入流做base64编码
InputStream is = new ByteArrayInputStream("123".getBytes());
Base64InputStream ebis = new Base64InputStream(is, true);
String enc = IOUtils.toString(ebis, "UTF-8"); // MTIz

// 对base64数据流做解码
is = new ByteArrayInputStream(enc.getBytes());
Base64InputStream dbis = new Base64InputStream(is, false);
String dec = IOUtils.toString(dbis, "UTF-8"); // 123

// -----------------------

// 将数据做base64编码写入输出流
final String data = "123";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Base64OutputStream ebos = new Base64OutputStream(baos, true);
IOUtils.write(data, ebos, "UTF-8");
String enc2 = baos.toString(); // MTIz

// 将base64数据做解码写入输出流
baos = new ByteArrayOutputStream();
Base64OutputStream dbos = new Base64OutputStream(baos, false);
IOUtils.write(data, dbos, "UTF-8");
String dec2 = dbos.toString(); // 123

URL相关

URL之所以要进行编码,是因为URL中有些字符会引起歧义。

例如URL参数字符串中使用key=value键值对这样的形式来传参,键值对之间以&符号分隔,如/s?q=abc&ie=utf-8。如果你的value字符串中包含了=或者&,那么势必会造成接收URL的服务器解析错误,因此必须将引起歧义的&和=符号进行转义,也就是对其进行编码。

又如URL的编码格式采用的是ASCII码,而不是Unicode,这也就是说你不能在URL中包含任何非ASCII字符,例如中文。否则如果客户端浏览器和服务端浏览器支持的字符集不同的情况下,中文可能会造成问题。

URL编码的原则就是使用安全的字符(没有特殊用途或者特殊意义的可打印字符)去表示那些不安全的字符。

URLCodec urlCodec = new URLCodec();
// url编码
String encUrl = urlCodec.encode("http://x.com?f=哈");
System.out.println(encUrl);
// url解码
String decUrl = urlCodec.decode(encUrl);
System.out.println(decUrl);

摘要算法

摘要算法是一种单向的散列算法,它满足以下几个特点。

  • 输入长度是任意的
  • 输出长度是固定的
  • 对每一个给定的输入,计算输出是很容易的
  • 不可逆,无法通过输出推算出原数据
  • 输出不依赖于输入。就是输入数据变动一个字节结果会相差很多

由于摘要算法以上特点,主要用于数据完整性校验。例如网上的资源一般会提供一个摘要值(一般用MD5算法),用户下载后可以通过工具对资源做MD5后和网上给定的值比较,如果不一致说明文件不完整了,可能是下载过程网络波动内容有丢失,也可能被人篡改过。

也可以做数据的指纹,比如网盘秒传,就是利用摘要值做判断。客户端上传前先对文件做摘要值,传给服务端,服务端发现有相同摘要的文件说明两个文件内容是一致的,这样就无需上传直接将文件存储路径指向这个文件就可以了,既实现了秒传,还节约了服务器磁盘空间(不同用户相同内容的文件实际上指向的是同一份文件)。

很多系统也将密码做md5后存储,其中这种方式并不安全。md5已经很很多公开结果了,并且使用彩虹表碰撞也很容易破解了。所以并不建议使用md5存储密码。密码推荐使用BCrypt算法。

摘要算法主要有以下几个

  • MD(Message Digest):消息摘要
  • SHA(Secure Hash Algorithm):安全散列
  • MAC(Message Authentication Code):消息认证码

MD系列

主要有MD2、MD4、MD5,目前一般常用MD5。
// 如果使用Java自带的api需要十多行才能实现md5算法

// 对数据做md5,参数支持字符串,字节数据,输入流
String md5 = DigestUtils.md5Hex("测试");

SHA系列

SHA系列有SHA-1、SHA-224、SHA-256、SHA-384、SHA-512,SHA3-224、SHA3-256、SHA3-384、SHA3-512等。目前安全起见一般选择256以上,推荐384以上。当然摘要越长则计算耗时也越长,需要根据需求权衡。

// 参数支持字符串,字节数据,输入流
String sha1 = DigestUtils.sha1Hex("测试");
String sha256 = DigestUtils.sha256Hex("测试");
String sha384 = DigestUtils.sha384Hex("测试");
String sha512 = DigestUtils.sha512Hex("测试");
String sha3_256 = DigestUtils.sha3_256Hex("测试");
String sha3_384 = DigestUtils.sha3_384Hex("测试");
String sha3_512 = DigestUtils.sha3_512Hex("测试");

HMAC系列

HMAC(keyed-Hash Message Authentication Code)系列是包含密钥的散列算法,包含了MD和SHA两个系列的消息摘要算法。融合了MD,SHA。

MD系列:HMacMD2,HMacMD4,HMacMD5

SHA系列:HMacSHA1,HMacSHA224,HMacSHA256,HMacSHA38,HMacSHA512

String key = "asdf3234asdf3234asdf3234asdf3234";
String valueToDigest = "测试数据"; // valueToDigest参数支持字节数据,流,文件等
// 做HMAC-MD5摘要
String hmacMd5 = new HmacUtils(HmacAlgorithms.HMAC_MD5, key).hmacHex(valueToDigest);
// 做HMAC-sha摘要
String hmacSha256 = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, key).hmacHex(valueToDigest);
String hmacSha384 = new HmacUtils(HmacAlgorithms.HMAC_SHA_384, key).hmacHex(valueToDigest);
String hmacSha512 = new HmacUtils(HmacAlgorithms.HMAC_SHA_512, key).hmacHex(valueToDigest);

命令行 

codec包还提供了一个命令行做摘要算法的入口。

java -cp ./commons-codec-1.15.jar org.apache.commons.codec.cli.Digest MD5 123

Commons Collections

可以把这个工具看成是java.util的扩展,主要是集合的处理、条件判断等。

pom.xml依赖

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

常用 API

  • CollectionUtils

常用方法说明:

/**
 * 1、除非元素为null,否则向集合添加元素
 */
CollectionUtils.addIgnoreNull(personList,null);
/**
 * 2、将两个已排序的集合a和b合并为一个已排序的列表,以便保留元素的自然顺序
 */
CollectionUtils.collate(Iterable<? extends O> a, Iterable<? extends O> b)
/**
 * 3、将两个已排序的集合a和b合并到一个已排序的列表中,以便保留根据Comparator c的元素顺序。
 */
CollectionUtils.collate(Iterable<? extends O> a, Iterable<? extends O> b, Comparator<? super O> c)
/**
 * 4、返回该个集合中是否含有至少有一个元素
 */
CollectionUtils.containsAny(Collection<?> coll1, T... coll2)
/**
 * 5、如果参数是null,则返回不可变的空集合,否则返回参数本身。(很实用 ,最终返回List EMPTY_LIST = new EmptyList<>())
 */
CollectionUtils.emptyIfNull(Collection<T> collection)
/**
 * 6、空安全检查指定的集合是否为空
 */
CollectionUtils.isEmpty(Collection<?> coll)
/**
 * 7、 空安全检查指定的集合是否为空。
 */
CollectionUtils.isNotEmpty(Collection<?> coll)
/**
 * 8、反转给定数组的顺序。
 */
CollectionUtils.reverseArray(Object[] array);
/**
 * 9、差集
 */
CollectionUtils.subtract(Iterable<? extends O> a, Iterable<? extends O> b)
/**
 * 10、并集
 */
CollectionUtils.union(Iterable<? extends O> a, Iterable<? extends O> b)
/**
 * 11、交集
 */
CollectionUtils.intersection(Collection a, Collection b)
/**
 *12、 交集的补集(析取)
 */
CollectionUtils.disjunction(Collection a, Collection b)

Commons Configuration

这个工具是用来帮助处理配置文件的,支持很多种存储方式。

pom.xml依赖

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-configuration2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-configuration2</artifactId>
    <version>2.7</version>
</dependency>

常用 API

  • PropertiesConfiguration
  • Configuration 

Commons FileUpload

文件上传功能。

pom.xml依赖

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

常用 API

  • ServletFileUpload
  • DiskFileItemFactory
  • FileItem

Commons IO

Apache Commons IO是对java.io的扩展,主要是对Java中的bio封装了一些好用的工具类。

Commons IO目前最新版本是2.10.0,最低要求Java8以上。

pom.xml依赖

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

整体结构:

org.apache.commons.ioorg.apache.commons.io.comparator //比较器
org.apache.commons.io.file //文件
org.apache.commons.io.filefilter //文件过滤器
org.apache.commons.io.function //函数
org.apache.commons.io.inputorg.apache.commons.io.monitor //监视器
org.apache.commons.io.outputorg.apache.commons.io.serialization //序列化

常用 API

常用的类有:

  • IOUtils 包含一些工具类,用于处理读,写和拷贝,这些方法基于 InputStream, OutputStream, Reader 和 Writer工作;
  • FileUtils 包含一些工具类,它们基于File对象工作,包括读,写,拷贝和比较文件;
  • FilenameUtils包含一些工具类,它们基于文件名工作而不是File对象。这个类旨在 在Unix和Windows环境下保持一致,帮助在两个环境下过渡(如从开发环境到生成环境);
  • FileSystemUtils包含一些工具类,基于文件系统访问功能不被JDK支持。目前,只有一个方法就是得到驱动器空余空间。注意这使用命令行而不是 native code;
  • EndianUtils 包含静态方法来交换Java基本类型和流的字节序;
  • SwappedDataInputStream实现了DataInput接口。可以从文件中读取非本地字节序;

IOUtils

IOUtils可以说是Commons IO中最常用的。

关闭流

InputStream inputStream = new FileInputStream("test.txt");
OutputStream outputStream = new FileOutputStream("test.txt");
// 原生写法
if (inputStream != null) {
    try {
        inputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
if (outputStream != null) {
    try {
        outputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
// commons写法(可以传任意数量的流)
IOUtils.closeQuietly(inputStream, outputStream);

读取流

// ==== 输入流转换为byte数组 ====
// 原生写法
InputStream is = new FileInputStream("foo.txt");
byte[] buf = new byte[1024];
int len;
ByteArrayOutputStream out = new ByteArrayOutputStream();
while ((len = is.read(buf)) != -1) {
    out.write(buf, 0, len);
}
byte[] result = out.toByteArray();
// commons写法
byte[] result2 = IOUtils.toByteArray(is);

// ---------------------------------------

// ==== 输入流转换为字符串 ====
// 原生写法
InputStream is = new FileInputStream("foo.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
    sb.append(line);
}
String result = sb.toString();
// commons写法
String result2 = IOUtils.toString(is, "UTF-8");

// IOUtils.toString 还有很多重载方法,保证有你想要的
// 将reader转换为字符串
String toString(Reader reader, String charset) throws IOException;
// 将url转换为字符串,也就是可以直接将网络上的内容下载为字符串
String toString(URL url, String charset) throws IOException;

其他

// 按照行读取结果
InputStream is = new FileInputStream("test.txt");
List<String> lines = IOUtils.readLines(is, "UTF-8");

// 将行集合写入输出流
OutputStream os = new FileOutputStream("newTest.txt");
IOUtils.writeLines(lines, System.lineSeparator(), os, "UTF-8");

// 拷贝输入流到输出流
InputStream inputStream = new FileInputStream("src.txt");
OutputStream outputStream = new FileOutputStream("dest.txt");
IOUtils.copy(inputStream, outputStream);

文件相关

文件相关主要有:

  • FileUtils:文件工具类
  • FilenameUtils:文件名工具类
  • PathUtils:路径工具类(主要是操作JDK7新增的java.nio.file.Path类)

文件读写

File readFile = new File("test.txt");
// 读取文件
String str = FileUtils.readFileToString(readFile, "UTF-8");
// 读取文件为字节数组
byte[] bytes = FileUtils.readFileToByteArray(readFile);
// 按行读取文件
List<String> lines =  FileUtils.readLines(readFile, "UTF-8");

File writeFile = new File("out.txt");
// 将字符串写入文件
FileUtils.writeStringToFile(writeFile, "测试文本", "UTF-8");
// 将字节数组写入文件
FileUtils.writeByteArrayToFile(writeFile, bytes);
// 将字符串列表一行一行写入文件
FileUtils.writeLines(writeFile, lines, "UTF-8");

移动和复制

File srcFile = new File("src.txt");
File destFile = new File("dest.txt");
File srcDir = new File("/srcDir");
File destDir = new File("/destDir");
// 移动/拷贝文件
FileUtils.moveFile(srcFile, destFile);
FileUtils.copyFile(srcFile, destFile);
// 移动/拷贝文件到目录
FileUtils.moveFileToDirectory(srcFile, destDir, true);
FileUtils.copyFileToDirectory(srcFile, destDir);
// 移动/拷贝目录
FileUtils.moveDirectory(srcDir, destDir);
FileUtils.copyDirectory(srcDir, destDir);
// 拷贝网络资源到文件
FileUtils.copyURLToFile(new URL("http://xx"), destFile);
// 拷贝流到文件
FileUtils.copyInputStreamToFile(new FileInputStream("test.txt"), destFile);
// ... ...

其他文件操作

File file = new File("test.txt");
File dir = new File("/test");
// 删除文件
FileUtils.delete(file);
// 删除目录
FileUtils.deleteDirectory(dir);
// 文件大小,如果是目录则递归计算总大小
long s = FileUtils.sizeOf(file);
// 则递归计算目录总大小,参数不是目录会抛出异常
long sd = FileUtils.sizeOfDirectory(dir);
// 递归获取目录下的所有文件
Collection<File> files = FileUtils.listFiles(dir, null, true);
// 获取jvm中的io临时目录
FileUtils.getTempDirectory();
// ... ...

文件名称相称

// 获取名称,后缀等
String name = "/home/xxx/test.txt";
FilenameUtils.getName(name); // "test.txt"
FilenameUtils.getBaseName(name); // "test"
FilenameUtils.getExtension(name); // "txt"
FilenameUtils.getPath(name); // "/home/xxx/"

// 将相对路径转换为绝对路径
FilenameUtils.normalize("/foo/bar/.."); // "/foo"

JDK7的Path操作

// path既可以表示目录也可以表示文件

// 获取当前路径
Path path = PathUtils.current();
// 删除path
PathUtils.delete(path);
// 路径或文件是否为空
PathUtils.isEmpty(path);
// 设置只读
PathUtils.setReadOnly(path, true);
// 复制
PathUtils.copyFileToDirectory(Paths.get("test.txt"), path);
PathUtils.copyDirectory(Paths.get("/srcPath"), Paths.get("/destPath"));
// 统计目录内文件数量
Counters.PathCounters counters = PathUtils.countDirectory(path);
counters.getByteCounter(); // 字节大小
counters.getDirectoryCounter(); // 目录个数
counters.getFileCounter(); // 文件个数
// ... ...

流相关 

org.apache.commons.io.input和org.apache.commons.io.output包下有许多好用的过滤流,下面列举几个做下说明

自动关闭的输入流 AutoCloseInputStream

/**
 * AutoCloseInputStream是一个过滤流,用来包装其他流,读取完后流会自动关掉
 * 实现原理很简单,当读取完后将底层的流关闭,然后创建一个ClosedInputStream赋值给它包装的输入流。
 * 注:如果输入流没有全部读取是不会关掉底层流的
 */
public void autoCloseDemo() throws Exception {
    InputStream is = new FileInputStream("test.txt");
    AutoCloseInputStream acis = new AutoCloseInputStream(is);
    IOUtils.toByteArray(acis); // 将流全部读完
    // 可以省略关闭流的逻辑了
}

倒序文件读取 ReversedLinesFileReader

// 从后向前按行读取
try (ReversedLinesFileReader reader = new ReversedLinesFileReader(new File("test.txt"), Charset.forName("UTF-8"))) {
    String lastLine = reader.readLine(); // 读取最后一行
    List<String> line5 = reader.readLines(5); // 从后再读5行
}

带计数功能的流 CountingInputStream,CountingOutputStream

/**
 * 大家都知道只给一个输入流咱们是没办法准确的知道它的大小的,虽说流提供了available()方法
 * 但是这个方法只有在ByteArrayInputStream的情况下拿到的是准确的大小,其他如文件流网络流等都是不准确的
 * (当然用野路子也可以实现,比如写入临时文件通过File.length()方法获取,然后在将文件转换为文件流)
 * 下面这个流可以实现计数功能,当把文件读完大小也就计算出来了
 */
public void countingDemo() {
    InputStream is = new FileInputStream("test.txt");
    try (CountingInputStream cis = new CountingInputStream(is)) {
        String txt = IOUtils.toString(cis, "UTF-8"); // 文件内容
        long size = cis.getByteCount(); // 读取的字节数
    } catch (IOException e) {
        // 异常处理
    }
}

可观察的输入流 ObservableInputStream

可观察的输入流(典型的观察者模式),可实现边读取边处理

比如将某些字节替换为另一个字节,计算md5摘要等

当然你也可以完全写到文件后在做处理,这样相当于做了两次遍历,性能较差。

这是一个基类,使用时需要继承它来扩展自己的流,示例如下:

private class MyObservableInputStream extends ObservableInputStream {
    class MyObserver extends Observer {
        @Override
        public void data(final int input) throws IOException {
            // 做自定义处理
        }
        @Override
        public void data(final byte[] input, final int offset, final int length) throws IOException {
            // 做自定义处理
        }
    }
    public MyObservableInputStream(InputStream inputStream) {
        super(inputStream);
    }
}

其他

  • BOMInputStream: 同时读取文本文件的bom头
  • BoundedInputStream:有界的流,控制只允许读取前x个字节
  • BrokenInputStream: 一个错误流,永远抛出IOException
  • CharSequenceInputStream: 支持StringBuilder,StringBuffer等读取
  • LockableFileWriter: 带锁的Writer,同一个文件同时只允许一个流写入,多余的写入操作会跑出IOException
  • StringBuilderWriter: StringBuilder的Writer
  • ... ...

文件比较器

org.apache.commons.io.compare包有很多现成的文件比较器,可以对文件排序的时候直接拿来用。
  • DefaultFileComparator:默认文件比较器,直接使用File的compare方法。(文件集合排序( Collections.sort() )时传此比较器和不传效果一样)
  • DirectoryFileComparator:目录排在文件之前
  • ExtensionFileComparator:扩展名比较器,按照文件的扩展名的ascii顺序排序,无扩展名的始终排在前面
  • LastModifiedFileComparator:按照文件的最后修改时间排序
  • NameFileComparator:按照文件名称排序
  • PathFileComparator:按照路径排序,父目录优先排在前面
  • SizeFileComparator:按照文件大小排序,小文件排在前面(目录会计算其总大小)
  • CompositeFileComparator:组合排序,将以上排序规则组合在一起
List<File> files = Arrays.asList(new File[]{
    new File("/foo/def"),
    new File("/foo/test.txt"),
    new File("/foo/abc"),
    new File("/foo/hh.txt")});
// 排序目录在前
Collections.sort(files, DirectoryFileComparator.DIRECTORY_COMPARATOR); // ["/foo/def", "/foo/abc", "/foo/test.txt", "/foo/hh.txt"]
// 排序目录在后
Collections.sort(files, DirectoryFileComparator.DIRECTORY_REVERSE); // ["/foo/test.txt", "/foo/hh.txt", "/foo/def", "/foo/abc"]
// 组合排序,首先按目录在前排序,其次再按照名称排序
Comparator dirAndNameComp = new CompositeFileComparator(
    DirectoryFileComparator.DIRECTORY_COMPARATOR,
    NameFileComparator.NAME_COMPARATOR);
Collections.sort(files, dirAndNameComp); // ["/foo/abc", "/foo/def", "/foo/hh.txt", "/foo/test.txt"]

文件监视器

org.apache.commons.io.monitor包主要提供对文件的创建、修改、删除的监听操作。
public static void main(String[] args) throws Exception {
    // 监听目录下文件变化。可通过参数控制监听某些文件,默认监听目录所有文件
    FileAlterationObserver observer = new FileAlterationObserver("/foo");
    observer.addListener(new myListener());
    FileAlterationMonitor monitor = new FileAlterationMonitor();
    monitor.addObserver(observer);
    monitor.start(); // 启动监视器
    Thread.currentThread().join(); // 避免主线程退出造成监视器退出
}

private class myListener extends FileAlterationListenerAdaptor {
    @Override
    public void onFileCreate(File file) {
        System.out.println("fileCreated:" + file.getAbsolutePath());
    }
    @Override
    public void onFileChange(File file) {
        System.out.println("fileChanged:" + file.getAbsolutePath());
    }
    @Override
    public void onFileDelete(File file) {
        System.out.println("fileDeleted:" + file.getAbsolutePath());
    }
}

Commons Lang

Apache Commons Lang是对java.lang的扩展,基本上是commons中最常用的工具包。

目前Lang包有两个commons-lang3和commons-lang。

lang最新版本是2.6,最低要求Java1.2以上,目前官方已不在维护。lang3目前最新版本是3.12.0,最低要求Java8以上。相对于lang来说完全支持Java8的特性,废除了一些旧的API。该版本无法兼容旧有版本,于是为了避免冲突改名为lang3。

Java8以上的用户推荐使用lang3代替lang。

pom.xml依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

整体包结构:

org.apache.commons.lang3
org.apache.commons.lang3.builder //建造者
org.apache.commons.lang3.concurrent //并发
org.apache.commons.lang3.event //事件
org.apache.commons.lang3.exception //异常
org.apache.commons.lang3.math //数据公式
org.apache.commons.lang3.mutable //可变值包装
org.apache.commons.lang3.reflect //反射
org.apache.commons.lang3.text
org.apache.commons.lang3.text.translate
org.apache.commons.lang3.time //时间
org.apache.commons.lang3.tuple //元组

常用 API

常用的类有:

  • StringUtils 字符串的处理类
  • RandomStringUtils 随机数生成类
  • NumberUtils 数字类
  • ArrayUtils 数组类
  • DateUtils 日期类

日期相关 

在Java8之前,日期只提供了java.util.Date类和java.util.Calendar类,说实话这些API并不是很好用,而且也存在线程安全的问题,所以Java8推出了新的日期API。如果你还在用旧的日期API,可以使用DateUtils和DateFormatUtils工具类。

字符串转日期

final String strDate = "2021-07-04 11:11:11";
final String pattern = "yyyy-MM-dd HH:mm:ss";
// 原生写法
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
Date date1 = sdf.parse(strDate);
// commons写法
Date date2 = DateUtils.parseDate(strDate, pattern);

日期转字符串

final Date date = new Date();
final String pattern = "yyyy年MM月dd日";
// 原生写法
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
String strDate = sdf.format(date);
// 使用commons写法
String strDate = DateFormatUtils.format(date, pattern);

日期计算

final Date date = new Date();
// 原生写法
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.DATE, 5); // 加5天
cal.add(Calendar.HOUR_OF_DAY, -5); // 减5小时
// 使用commons写法
Date newDate1 = DateUtils.addDays(date, 5); // 加5天
Date newDate2 = DateUtils.addHours(date, -5); // 减5小时
Date newDate3 = DateUtils.truncate(date, Calendar.DATE); // 过滤时分秒
boolean isSameDay = DateUtils.isSameDay(newDate1, newDate2); // 判断是否是同一天

字符串相关

字符串是Java中最常用的类型,相关的工具类也可以说是最常用的

字符串判空

String str = "";
// 原生写法
if (str == null || str.length() == 0) {
    // Do something
}
// commons写法
if (StringUtils.isEmpty(str)) {
    // Do something
}  
/* StringUtils.isEmpty(null)      = true
 * StringUtils.isEmpty("")        = true
 * StringUtils.isEmpty(" ")       = false
 * StringUtils.isEmpty("bob")     = false
 * StringUtils.isEmpty("  bob  ") = false
 */

相关方法:

// isEmpty取反
StringUtils.isNotEmpty(str);
/* 
 * null, 空串,空格为true
 * StringUtils.isBlank(null)      = true
 * StringUtils.isBlank("")        = true
 * StringUtils.isBlank(" ")       = true
 * StringUtils.isBlank("bob")     = false
 * StringUtils.isBlank("  bob  ") = false
 */
StringUtils.isBlank(str);
// isBlank取反
StringUtils.isNotBlank(str);
// 任意一个参数为空则结果为true
StringUtils.isAnyEmpty(str1, str2, str3);
// 所有参数为空则结果为true
StringUtils.isAllEmpty(str1, str2, str3);

字符串去空格

// 去除两端空格,不需要判断null
String newStr = StringUtils.trim(str);
/*
 * 去除两端空格,如果是null则转换为空字符串
 * StringUtils.trimToEmpty(null)          = ""
 * StringUtils.trimToEmpty("")            = ""
 * StringUtils.trimToEmpty("     ")       = ""
 * StringUtils.trimToEmpty("abc")         = "abc"
 * StringUtils.trimToEmpty("    abc    ") = "abc"
 */
newStr = StringUtils.trimToEmpty(str);
/*
 * 去除两端空格,如果结果是空串则转换为null
 * StringUtils.trimToNull(null)          = null
 * StringUtils.trimToNull("")            = null
 * StringUtils.trimToNull("     ")       = null
 * StringUtils.trimToNull("abc")         = "abc"
 * StringUtils.trimToNull("    abc    ") = "abc"
 */
newStr = StringUtils.trimToNull(str);
/*
 * 去两端 给定字符串中任意字符
 * StringUtils.strip(null, *)          = null
 * StringUtils.strip("", *)            = ""
 * StringUtils.strip("abc", null)      = "abc"
 * StringUtils.strip("  abc", null)    = "abc"
 * StringUtils.strip("abc  ", null)    = "abc"
 * StringUtils.strip(" abc ", null)    = "abc"
 * StringUtils.strip("  abcyx", "xyz") = "  abc"
 */
newStr = StringUtils.strip(str, "stripChars");
// 去左端 给定字符串中任意字符
newStr = StringUtils.stripStart(str, "stripChars");
// 去右端 给定字符串中任意字符
newStr = StringUtils.stripEnd(str, "stripChars");

字符串分割

/*
 * 按照空格分割字符串 结果为数组
 * StringUtils.split(null)       = null
 * StringUtils.split("")         = []
 * StringUtils.split("abc def")  = ["abc", "def"]
 * StringUtils.split("abc  def") = ["abc", "def"]
 * tringUtils.split(" abc ")    = ["abc"]
 */
StringUtils.split(str);
// 按照某些字符分割 结果为数组,自动去除了截取后的空字符串
StringUtils.split(str, ",");

取子字符串

// 获得"ab.cc.txt"中最后一个.之前的字符串
StringUtils.substringBeforeLast("ab.cc.txt", "."); // ab.cc
// 相似方法
// 获得"ab.cc.txt"中最后一个.之后的字符串(常用于获取文件后缀名)
StringUtils.substringAfterLast("ab.cc.txt", "."); // txt
// 获得"ab.cc.txt"中第一个.之前的字符串
StringUtils.substringBefore("ab.cc.txt", "."); // ab
// 获得"ab.cc.txt"中第一个.之后的字符串
StringUtils.substringAfter("ab.cc.txt", "."); // cc.txt
// 获取"ab.cc.txt"中.之间的字符串
StringUtils.substringBetween("ab.cc.txt", "."); // cc
// 看名字和参数应该就知道干什么的了
StringUtils.substringBetween("a(bb)c", "(", ")"); // bb

随机字符串

// 随机生成长度为5的字符串
RandomStringUtils.random(5);
// 随机生成长度为5的"只含大小写字母"字符串
RandomStringUtils.randomAlphabetic(5);
// 随机生成长度为5的"只含大小写字母和数字"字符串
RandomStringUtils.randomAlphanumeric(5);
// 随机生成长度为5的"只含数字"字符串
RandomStringUtils.randomNumeric(5);

其他

// 首字母大写
StringUtils.capitalize("test"); // Test
// 字符串合并
StringUtils.join(new int[]{1,2,3}, ",");// 1,2,3
// 缩写
StringUtils.abbreviate("abcdefg", 6);// "abc..."
// 判断字符串是否是数字
StringUtils.isNumeric("abc123");// false
// 删除指定字符
StringUtils.remove("abbc", "b"); // ac
// ... ... 还有很多,感兴趣可以自己研究

反射相关

反射是Java中非要重要的特性,原生的反射API代码冗长,Lang包中反射相关的工具类可以很方便的实现反向相关功能

属性操作

public class ReflectDemo {
    private static String sAbc = "111";
    private String abc = "123";
    public void fieldDemo() throws Exception {
        ReflectDemo reflectDemo = new ReflectDemo();
        // 反射获取对象实例属性的值
        // 原生写法
        Field abcField = reflectDemo.getClass().getDeclaredField("abc");
        abcField.setAccessible(true);// 设置访问级别,如果private属性不设置则访问会报错
        String value = (String) abcField.get(reflectDemo);// 123
        // commons写法
        String value2 = (String) FieldUtils.readDeclaredField(reflectDemo, "abc", true);//123
        // 方法名如果不含Declared会向父类上一直查找
    }
}

注:方法名含Declared的只会在当前类实例上寻找,不包含Declared的在当前类上找不到则会递归向父类上一直查找。

相关方法:

public class ReflectDemo {
    private static String sAbc = "111";
    private String abc = "123";
    public void fieldRelated() throws Exception {
        ReflectDemo reflectDemo = new ReflectDemo();
        // 反射获取对象属性的值
        String value2 = (String) FieldUtils.readField(reflectDemo, "abc", true);//123
        // 反射获取类静态属性的值
        String value3 = (String) FieldUtils.readStaticField(ReflectDemo.class, "sAbc", true);//111
        // 反射设置对象属性值
        FieldUtils.writeField(reflectDemo, "abc", "newValue", true);
        // 反射设置类静态属性的值
        FieldUtils.writeStaticField(ReflectDemo.class, "sAbc", "newStaticValue", true);
    }
}

获取注解方法 

// 获取被Test注解标识的方法
        
// 原生写法
List<Method> annotatedMethods = new ArrayList<Method>();
for (Method method : ReflectDemo.class.getMethods()) {
    if (method.getAnnotation(Test.class) != null) {
        annotatedMethods.add(method);
    }
}
// commons写法
Method[] methods = MethodUtils.getMethodsWithAnnotation(ReflectDemo.class, Test.class);

方法调用

private static void testStaticMethod(String param1) {}
private void testMethod(String param1) {}

public void invokeDemo() throws Exception {
    // 调用函数"testMethod"
    ReflectDemo reflectDemo = new ReflectDemo();
    // 原生写法
    Method testMethod = reflectDemo.getClass().getDeclaredMethod("testMethod");
    testMethod.setAccessible(true); // 设置访问级别,如果private函数不设置则调用会报错
    testMethod.invoke(reflectDemo, "testParam");
    // commons写法
    MethodUtils.invokeExactMethod(reflectDemo, "testMethod", "testParam");

    // ---------- 类似方法 ----------
    // 调用static方法
    MethodUtils.invokeExactStaticMethod(ReflectDemo.class, "testStaticMethod", "testParam");
    // 调用方法(含继承过来的方法)
    MethodUtils.invokeMethod(reflectDemo, "testMethod", "testParam");
    // 调用static方法(当前不存在则向父类寻找匹配的静态方法)
    MethodUtils.invokeStaticMethod(ReflectDemo.class, "testStaticMethod", "testParam");
}

其他还有ClassUtils,ConstructorUtils,TypeUtils等不是很常用。

系统相关

主要是获取操作系统和JVM一些信息

// 判断操作系统类型
boolean isWin = SystemUtils.IS_OS_WINDOWS;
boolean isWin10 = SystemUtils.IS_OS_WINDOWS_10;
boolean isWin2012 = SystemUtils.IS_OS_WINDOWS_2012;
boolean isMac = SystemUtils.IS_OS_MAC;
boolean isLinux = SystemUtils.IS_OS_LINUX;
boolean isUnix = SystemUtils.IS_OS_UNIX;
boolean isSolaris = SystemUtils.IS_OS_SOLARIS;
// ... ...

// 判断java版本
boolean isJava6 = SystemUtils.IS_JAVA_1_6;
boolean isJava8 = SystemUtils.IS_JAVA_1_8;
boolean isJava11 = SystemUtils.IS_JAVA_11;
boolean isJava14 = SystemUtils.IS_JAVA_14;
// ... ...

// 获取java相关目录
File javaHome = SystemUtils.getJavaHome();
File userHome = SystemUtils.getUserHome();// 操作系统用户目录
File userDir = SystemUtils.getUserDir();// 项目所在路径
File tmpDir = SystemUtils.getJavaIoTmpDir();

Commons Net

该库实现了许多基本Internet协议的客户端。库的目的是提供基本的协议访问,而不是更高级别的抽象。

pom.xml依赖

<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.8.0</version>
</dependency>

常用 API

Commons-Net 提供的网络客户端非常多:

  • SocketClient:所有TCP实现的基类
  • DatagramSocketClient :所有UDP实现的基类
  • FTPClient/FTPSClient:文件传输协议
  • FTPHTTPClient
  • NNTPClient:网络新闻组传输协议
  • SMTPClient****/****SMTPSClient:电子邮件传输的协议
  • POP3Client/POP3SClient:POP3协议
  • IMAPClient/IMAPSClient:交互邮件访问协议
  • TelnetClient:Telnet协议
  • TFTPClient:简单文件传输协议
  • FingerClient
  • WhoisClient
  • RExecClient/RCommandClient/RLoginClient****
  • TimeTCPClient/TimeUDPClient,DaytimeTCPClient/DaytimeUDPClient
  • EchoTCPClient/EchoUDPClient
  • ****DiscardTCPClient/DiscardUDPClient
  • ****NTPUDPClient

此处只列举几个简单的做下示例:

WhoisClient

// 基于TCP协议
WhoisClient whoisClient = new WhoisClient();
try {
    // 基于TCP使用connect
    whoisClient.connect(WhoisClient.DEFAULT_HOST);
    String host = whoisClient.query("baidu.com");
    System.out.println(host);
} finally {
    // 基于TCP需要主动断开连接
    whoisClient.disconnect();
}

TimeUDPClient

// 基于UDP协议
TimeUDPClient timeClient = new TimeUDPClient();
timeClient.setDefaultTimeout(3000);
InetAddress addr = InetAddress.getByName("time.nist.gov");
// UDP使用open
timeClient.open();
Date d = timeClient.getDate(addr);
System.out.println(DateFormatUtils.format(d, "yyyy-MM-dd HH:mm:ss.SSS"));
// UDP使用close关闭连接
timeClient.close();

工具方法

Commons-Net 还提供了一些网络相关的工具类,简化我们的一些网络处理,主要有以下几个:

  • SubnetUtils:在给定网络地址和子网掩码的情况下执行一些子网计算
  • TrustManagerUtils:TrustManager 工具类
  • KeyManagerUtils:KeyManager 工具类
  • SSLContextUtils:SSLContext 工具类

SSL相关

Java API写法:

// 使用jdk原生获取SSLContext
String key = "c:/client.jks";
String password = "123456"; // key密码
KeyStore keyStore = KeyStore.getInstance("JKS");  //创建一个keystore来管理密钥库
keyStore.load(new FileInputStream(key), password.toCharArray());
//创建jkd密钥访问库
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(keyStore);
KeyManager[] kms = kmf.getKeyManagers();
TrustManager[] tms = tmf.getTrustManagers();
SSLContext ctx = SSLContext.getInstance("TLSv1"); // 支持SSLv2, SSLv3, TLSv1, TLSv1.1, SSLv2Hello
ctx.init(kms, tms, null);

Commons Net 写法:

// 使用commons-net相关工具类写法
String key = "c:/client.jks";
String password = "123456"; // key密码
File keyFile = new File(key);
KeyManager km = KeyManagerUtils.createClientKeyManager(keyFile, password);
TrustManager tm = TrustManagerUtils.getDefaultTrustManager(keyStore);
SSLContext ctx2 = SSLContextUtils.createSSLContext("TLSv1", km, tm);

子网工具类

// 以下为两种初始化subnet工具类的方式
// 1. 使用IP和子网掩码
SubnetUtils subnet = new SubnetUtils("192.168.1.113", "255.255.255.0");
// 2. 使用CIDR格式地址
// SubnetUtils subnet = new SubnetUtils("192.168.1.113/24");

// 获取网络各种参数
SubnetUtils.SubnetInfo si = subnet.getInfo();
// 获取IP地址
si.getAddress(); // 192.168.1.113
// 获取网络地址(IP地址和子网掩码进行与运算,结果是网络地址,即主机号全0是网络地址)
si.getNetworkAddress(); // 192.168.1.0
// 获取广播地址(专门用于同时向网络中所有工作站进行发送的一个地址)
si.getBroadcastAddress(); // 192.168.1.255
// 获取子网下的起始地址
si.getLowAddress(); // 192.168.1.1
// 获取子网下的终止地址
si.getHighAddress(); // 192.168.1.254
// 获取上一个地址
si.getPreviousAddress(); // 192.168.1.112
// 获取下一个地址
si.getNextAddress(); // 192.168.1.114
// 获取子网掩码
si.getNetmask(); // 255.255.255.0
// 获取CIDR格式的网络地址
si.getCidrSignature(); // 192.168.1.113/24
// 获取子网最大IP个数
si.getAddressCountLong(); // 254
// 获取所有IP地址数组
si.getAllAddresses(); // [192.168.1.1, ..., 192.168.1.254]

Commons Validator

用来帮助进行验证的工具。比如验证Email字符串,日期字符串等是否合法。

pom.xml依赖:

<!-- https://mvnrepository.com/artifact/commons-validator/commons-validator -->
<dependency>
    <groupId>commons-validator</groupId>
    <artifactId>commons-validator</artifactId>
    <version>1.7</version>
</dependency>

Commons Virtual File System

提供对各种资源的访问接口。支持的资源类型包括。

这个包的功能很强大,极大的简化了程序对资源的访问。

pom.xml依赖:

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-vfs2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-vfs2</artifactId>
    <version>2.9.0</version>
</dependency>

Commons Compress

Apache Commons Compress提供了许多编解码相关的工具类。Compress目前最新版本是1.21,最低要求Java8以上。

pom.xml依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.21</version>
</dependency>

包的完整结构:

org.apache.commons.compress
org.apache.commons.compress.archivers //归档
org.apache.commons.compress.changes //变化
org.apache.commons.compress.compressors //压缩
org.apache.commons.compress.parallel //并行
org.apache.commons.compress.utils //工具
org.apache.commons.compress.harmony //pack算法抽离

常用 API

压缩

压缩:按某种算法减小文件所占用空间的大小

解压:按对应的逆向算法恢复文件

Compress自带了很多压缩相关的类,主要以下几个:

  • GzipCompressorOutputStream:压缩"*.gz"文件
  • GzipCompressorInputStream:解压"*.gz"文件
  • BZip2CompressorOutputStream:压缩"*.bz2"文件
  • BZip2CompressorInputStream:解压"*.bz2"文件
  • XZCompressorOutputStream:压缩"*.xz"文件
  • XZCompressorInputStream:解压"*.xz"文件
  • FramedLZ4CompressorOutputStream:压缩"*.lz4"文件
  • FramedLZ4CompressorInputStream:解压"*.lz4"文件
  • BlockLZ4CompressorOutputStream:压缩"*.block_lz4"文件
  • BlockLZ4CompressorInputStream:解压"*.block_lz4"文件
  • Pack200CompressorOutputStream:压缩"*.pack"文件
  • Pack200CompressorInputStream:解压"*.pack"文件
  • DeflateCompressorOutputStream:压缩"*.deflate"文件
  • DeflateCompressorInputStream:解压"*.deflate"文件
  • LZMACompressorOutputStream:压缩"*.lzma"文件
  • LZMACompressorInputStream:解压"*.lzma"文件
  • FramedSnappyCompressorOutputStream:压缩"*.sz"文件
  • FramedSnappyCompressorInputStream:解压"*.sz"文件
  • ZCompressorInputStream:解压"*.Z"文件

gzip

gzip是Unix,Linux上常用的压缩工具,也是当今的WEB站点上非常流行的压缩技术。其有压缩级别等概念,可以通过GzipParameters去设置。JDK8也自带了GZIPInputStream类,用法类似。
// gzip压缩
String file = "/test.js";
GzipParameters parameters = new GzipParameters();
parameters.setCompressionLevel(Deflater.BEST_COMPRESSION);
parameters.setOperatingSystem(3);
parameters.setFilename(FilenameUtils.getName(file));
parameters.setComment("Test file");
parameters.setModificationTime(System.currentTimeMillis());
FileOutputStream fos = new FileOutputStream(file + ".gz");
try (GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(fos, parameters);
    InputStream is = new FileInputStream(file)) {
    IOUtils.copy(is, gzos);
}
// gzip解压
String gzFile = "/test.js.gz";
FileInputStream is = new FileInputStream(gzFile);
try (GzipCompressorInputStream gis = new GzipCompressorInputStream(is)) {
    GzipParameters p = gis.getMetaData();
    File targetFile = new File("/test.js");
    FileUtils.copyToFile(gis, targetFile);
    targetFile.setLastModified(p.getModificationTime());
}

bz2

bz2是Linux下常见的压缩文件格式,是由具有高压缩率的压缩工具bzip2生成,以后缀为.bz2结尾的压缩文件。

// 压缩bz2
String srcFile = "/test.tar";
String targetFile = "/test.tar.bz2";
FileOutputStream os = new FileOutputStream(targetFile);
try (BZip2CompressorOutputStream bzos = new BZip2CompressorOutputStream(os);
    InputStream is = new FileInputStream(srcFile)) {
    IOUtils.copy(is, bzos);
}
// 解压bz2
String bzFile = "/test.tar.bz2";
FileInputStream is = new FileInputStream(bzFile);
try (BZip2CompressorInputStream bzis = new BZip2CompressorInputStream(is)) {
    File targetFile = new File("test.tar");
    FileUtils.copyToFile(bzis, targetFile);
}

归档

归档:将许多零散的文件整理为一个文件,文件总大小基本不变

解包:从归档文件中释放文件

Compress自带了很多归档相关的类,主要以下几个:

  • TarArchiveOutputStream:归档"*.tar"文件
  • TarArchiveInputStream:解包"*.tar"文件
  • ZipArchiveOutputStream:归档压缩"*.zip"文件
  • ZipArchiveInputStream:解包解压"*.zip"文件
  • JarArchiveOutputStream:归档压缩"*.jar"文件
  • JarArchiveInputStream:解包解压"*.jar"文件
  • DumpArchiveOutputStream:归档"*.dump"文件
  • DumpArchiveInputStream:解包"*.dump"文件
  • CpioArchiveOutputStream:归档压缩"*.cpio"文件
  • CpioArchiveInputStream:解包解压"*.cpio"文件
  • ArArchiveOutputStream:归档压缩"*.ar"文件
  • ArArchiveInputStream:解包解压"*.ar"文件
  • ArjArchiveInputStream:解包解压"*.arj"文件
  • SevenZOutputFile:归档压缩"*.7z"文件
  • SevenZFile:解包解压"*.7z"文件
其中zip,jar,cpio,ar,7z既支持归档也支持压缩,能在归档的过程中做压缩处理。

由于他们会处理一个个零散的文件,所以会有ArchiveEntry的概念,即一个ArchiveEntry代表归档包内的一个目录或文件。

tar

tar是Unix和Linux系统上的常用的压缩归档工具,可以将多个文件合并为一个文件,打包后的文件后缀亦为"tar"。

// tar压缩
public void tar() throws IOException {
    File srcDir = new File("/test");
    String targetFile = "/test.tar";
    try (TarArchiveOutputStream tos = new TarArchiveOutputStream(
            new FileOutputStream(targetFile))) {
        tarRecursive(tos, srcDir, "");
    }
}
// 递归压缩目录下的文件和目录
private void tarRecursive(TarArchiveOutputStream tos, File srcFile, String basePath) throws IOException {
    if (srcFile.isDirectory()) {
        File[] files = srcFile.listFiles();
        String nextBasePath = basePath + srcFile.getName() + "/";
        if (ArrayUtils.isEmpty(files)) {
            // 空目录
            TarArchiveEntry entry = new TarArchiveEntry(srcFile, nextBasePath);
            tos.putArchiveEntry(entry);
            tos.closeArchiveEntry();
        } else {
            for (File file : files) {
                tarRecursive(tos, file, nextBasePath);
            }
        }
    } else {
        TarArchiveEntry entry = new TarArchiveEntry(srcFile, basePath + srcFile.getName());
        tos.putArchiveEntry(entry);
        FileUtils.copyFile(srcFile, tos);
        tos.closeArchiveEntry();
    }
}
// tar解压
public void untar() throws IOException {
    InputStream is = new FileInputStream("/test.tar");
    String outPath = "/test";
    try (TarArchiveInputStream tis = new TarArchiveInputStream(is)) {
        TarArchiveEntry nextEntry;
        while ((nextEntry = tis.getNextTarEntry()) != null) {
            String name = nextEntry.getName();
            File file = new File(outPath, name);
            //如果是目录,创建目录
            if (nextEntry.isDirectory()) {
                file.mkdir();
            } else {
                //文件则写入具体的路径中
                FileUtils.copyToFile(tis, file);
                file.setLastModified(nextEntry.getLastModifiedDate().getTime());
            }
        }
    }
}

7z

7z 是一种全新的压缩格式,它拥有极高的压缩比。 

7z 格式的主要特征:

  • 开放的结构
  • 高压缩比
  • 强大的 AES-256 加密
  • 能够兼容任意压缩、转换、加密算法
  • 最高支持 16000000000 GB 的文件压缩
  • 以 Unicode 为标准的文件名
  • 支持固实压缩
  • 支持文件头压缩
// 7z压缩
public void _7z() throws IOException {
    try (SevenZOutputFile outputFile = new SevenZOutputFile(new File("/test.7z"))) {
        File srcFile = new File("/test");
        _7zRecursive(outputFile, srcFile, "");
    }
}
// 递归压缩目录下的文件和目录
private void _7zRecursive(SevenZOutputFile _7zFile, File srcFile, String basePath) throws IOException {
    if (srcFile.isDirectory()) {
        File[] files = srcFile.listFiles();
        String nextBasePath = basePath + srcFile.getName() + "/";
        // 空目录
        if (ArrayUtils.isEmpty(files)) {
            SevenZArchiveEntry entry = _7zFile.createArchiveEntry(srcFile, nextBasePath);
            _7zFile.putArchiveEntry(entry);
            _7zFile.closeArchiveEntry();
        } else {
            for (File file : files) {
                _7zRecursive(_7zFile, file, nextBasePath);
            }
        }
    } else {
        SevenZArchiveEntry entry = _7zFile.createArchiveEntry(srcFile, basePath + srcFile.getName());
        _7zFile.putArchiveEntry(entry);
        byte[] bs = FileUtils.readFileToByteArray(srcFile);
        _7zFile.write(bs);
        _7zFile.closeArchiveEntry();
    }
}
 // 7z解压
public void un7z() throws IOException {
    String outPath = "/test";
    try (SevenZFile archive = new SevenZFile(new File("test.7z"))) {
        SevenZArchiveEntry entry;
        while ((entry = archive.getNextEntry()) != null) {
            File file = new File(outPath, entry.getName());
            if (entry.isDirectory()) {
                file.mkdirs();
            }
            if (entry.hasStream()) {
                final byte [] buf = new byte [1024];
                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
                for (int len = 0; (len = archive.read(buf)) > 0;) {
                    baos.write(buf, 0, len);
                }
                FileUtils.writeByteArrayToFile(file, baos.toByteArray());
            }
        }
    }
}

修改归档文件

有时候我们会有修改归档内文件的需求,比如添加、删除一个文件,修改其中的文件内容等,当然我们也可以全部解压出来改完后在压缩回去。这样除了代码量多一些外,归档文件大也会导致操作时间过长。那么有没有办法用代码去动态的修改归档文件里的内容呢?

org.apache.commons.compress.changes包下正好就提供了一些类用于动态的修改归档文件里的内容。

String tarFile = "/test.tar";
InputStream is = new FileInputStream(tarFile);
// 替换后会覆盖原test.tar,如果是windows可能会由于文件被访问而覆盖报错
OutputStream os = new FileOutputStream(tarFile);
try (TarArchiveInputStream tais = new TarArchiveInputStream(is);
     TarArchiveOutputStream taos = new TarArchiveOutputStream(os)) {
    ChangeSet changes = new ChangeSet();
    // 删除"test.tar中"的"dir/1.txt"文件
    changes.delete("dir/1.txt");
    // 删除"test.tar"中的"t"目录
    changes.delete("t");
    // 添加文件,如果已存在则替换
    File addFile = new File("/a.txt");
    ArchiveEntry addEntry = taos.createArchiveEntry(addFile, addFile.getName());
    // add可传第三个参数:true: 已存在则替换(默认值), false: 不替换
    changes.add(addEntry, new FileInputStream(addFile));
    // 执行修改
    ChangeSetPerformer performer = new ChangeSetPerformer(changes);
    ChangeSetResults result = performer.perform(tais, taos);
}

其他

commons-compress还提供了一些简单的工厂类用户动态的获取压缩流和归档流。

简单工厂

// 使用factory动态获取归档流
ArchiveStreamFactory factory = new ArchiveStreamFactory();
String archiveName = ArchiveStreamFactory.TAR;
InputStream is = new FileInputStream("/in.tar");
OutputStream os = new FileOutputStream("/out.tar");
// 动态获取实现类,此时ais实际上是TarArchiveOutPutStream
ArchiveInputStream ais = factory.createArchiveInputStream(archiveName, is);
ArchiveOutputStream aos = factory.createArchiveOutputStream(archiveName, os);
// 其他业务操作

// ------------------------

// 使用factory动态获取压缩流
CompressorStreamFactory factory = new CompressorStreamFactory();
String compressName = CompressorStreamFactory.GZIP;
InputStream is = new FileInputStream("/in.gz");
OutputStream os = new FileOutputStream("/out.gz");
// 动态获取实现类,此时ais实际上是TarArchiveOutPutStream
CompressorInputStream cis = factory.createCompressorInputStream(compressName, is);
CompressorOutputStream cos = factory.createCompressorOutputStream(compressName, os);
// 其他业务操作

同时解压解包

面说了很多都是单一的操作,那么如果解压"test.tar.gz"这种归档和压缩于一体的文件呢?

其实很简单,我们不需要先解压在解包,可以一步同时完成解压和解包,只需要将对应的流包装一下即可(不得不感叹Java IO的装饰者模式设计真的很巧妙)。

// 解压 解包test.tar.gz文件
String outPath = "/test";
InputStream is = new FileInputStream("/test.tar.gz");
// 先解压,所以需要先用gzip流包装文件流
CompressorInputStream gis = new GzipCompressorInputStream(is);
// 在解包,用tar流包装gzip流
try (ArchiveInputStream tgis = new TarArchiveInputStream(gis)) {
    ArchiveEntry nextEntry;
    while ((nextEntry = tgis.getNextEntry()) != null) {
        String name = nextEntry.getName();
        File file = new File(outPath, name);
        // 如果是目录,创建目录
        if (nextEntry.isDirectory()) {
            file.mkdir();
        } else {
            // 文件则写入具体的路径中
            FileUtils.copyToFile(tgis, file);
            file.setLastModified(nextEntry.getLastModifiedDate().getTime());
        }
    }
}

Commons Exec

Apache Commons Exec主要用于执行外部进程的命令。Exec目前最新版本是1.3,最低要求Java5以上。

用Java执行外部进程命令也是比较常见的一种需求,这种操作依赖特定操作系统,需要我们了解特定系统的行为,例如在Windows上使用cmd.exe。想要可靠地执行外部进程还需要在执行命令之前或之后处理环境变量。

Apache Commons Exec就是为了处理上面概述的各种问题。而且代码实现起来也比较简单。

commons-exec屏蔽了不同操作系统的命令差异,解决了Runtime缓冲区问题导致的线程卡死,同时支持超时和等,用来代替JDK的Runtime API是非常不错的选择。

pom.xml依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-exec</artifactId>
    <version>1.3</version>
</dependency>
常用 API

同步调用

同步调用系统命令后会阻塞当前线程,直到获取到结果。

使用JDK写法

// 不使用工具类的写法
Process process = Runtime.getRuntime().exec("cmd /c ping 192.168.1.10");
int exitCode = process.waitFor(); // 阻塞等待完成
if (exitCode == 0) { // 状态码0表示执行成功
    String result = IOUtils.toString(process.getInputStream()); // "IOUtils" commons io中的工具类,详情可以参见前续文章介绍
    System.out.println(result);
} else {
    String errMsg = IOUtils.toString(process.getErrorStream());
    System.out.println(errMsg);
}

这么写其实有坑。如果执行一个安装脚本会在控制台输出大量内容,这时可能会导致进程卡死(其实是一直阻塞状态)。

这是由于缓冲区满了,无法写入数据,导致线程阻塞,对外现象就是进程无法停止,也不占资源,什么反应也没有。

这种情况可以单独启动一个线程去读取输入流的内容,避免缓冲区占满,示例如下:

final Process process = Runtime.getRuntime().exec("cmd /c ping 192.168.1.10");
new Thread(() -> {
    try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
        String line;
        while ((line = br.readLine()) != null) {
            try {
                process.exitValue();
                break; // exitValue没有异常表示进程执行完成,退出循环
            } catch (IllegalThreadStateException e) {
                // 异常代表进程没有执行完
            }
            //此处只做输出,对结果有其他需求可以在主线程使用其他容器收集此输出
            System.out.println(line);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}).start();
process.waitFor();

如果是异常信息打印过多则处理process.getErrorStream()。 

使用commons写法

commons-exec的command不需要考虑执行环境了,比如windows下不需要添加"cmd /c "的前缀。可以使用自定义的流来接受结果,比如使用文件流将结果保存到文件,使用网络流保存到远程服务器上等。下面的例子为了简单直接使用字节流去接收(如果结果非常大就不要用字节流了,容易内容溢出)。
String command = "ping 192.168.1.10";
//接收正常结果流
ByteArrayOutputStream susStream = new ByteArrayOutputStream();
//接收异常结果流
ByteArrayOutputStream errStream = new ByteArrayOutputStream();
CommandLine commandLine = CommandLine.parse(command);
DefaultExecutor exec = new DefaultExecutor();
PumpStreamHandler streamHandler = new PumpStreamHandler(susStream, errStream);
exec.setStreamHandler(streamHandler);
int code = exec.execute(commandLine);
System.out.println("result code: " + code);
// 不同操作系统注意编码,否则结果乱码
String suc = susStream.toString("GBK");
String err = errStream.toString("GBK");
System.out.println(suc);
System.out.println(err);

异步调用

使用JDK写法

JDK自带的Runtime的API不支持异步执行,如果要异步拿到执行结果需要自己单独创建线程不断轮询进程状态然后通知主线程,下面看一个例子。例子力求简单,所以很多细节不是很严谨,只看大体思路即可(如果要实现exec方便的API需要更多的代码来实现)。
public class RuntimeAsyncDemo {

    public static void main(String[] args) throws Exception {
        System.out.println("1. 开始执行");
        String cmd = "cmd /c ping 192.168.1.11"; // 假设是一个耗时的操作
        execAsync(cmd, processResult -> {
            System.out.println("3. 异步执行完成,success=" + processResult.success + "; msg=" + processResult.result);
            System.exit(0);
        });
        // 做其他操作 ... ...
        System.out.println("2. 做其他操作");
        // 避免主线程退出导致程序退出
        Thread.currentThread().join();
    }
    private static void execAsync(String command, Consumer<ProcessResult> callback) throws IOException {
        final Process process = Runtime.getRuntime().exec(command);
        new Thread(() -> {
            StringBuilder successMsg = new StringBuilder();
            try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"))) {
                // 存放临时结果
                String line;
                while ((line = br.readLine()) != null) {
                    try {
                        successMsg.append(line).append("\r\n");
                        int exitCode = process.exitValue();
                        ProcessResult pr = new ProcessResult();
                        if (exitCode == 0) {
                            pr.success = true;
                            pr.result = successMsg.toString();
                        } else {
                            pr.success = false;
                            pr.result = IOUtils.toString(process.getErrorStream());
                        }
                        callback.accept(pr); // 回调主线程注册的函数
                        break; // exitValue没有异常表示进程执行完成,退出循环
                    } catch (IllegalThreadStateException e) {
                        // 异常代表进程没有执行完
                    }
                    try {
                        // 等待100毫秒在检查是否完成
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }

    private static class ProcessResult {
        boolean success;
        String result;
    }
}

使用commons写法

commons-exec原生支持异步调用。

@Test
public void execAsync() throws IOException, InterruptedException {
    String command = "ping 192.168.1.10";
    //接收正常结果流
    ByteArrayOutputStream susStream = new ByteArrayOutputStream();
    //接收异常结果流
    ByteArrayOutputStream errStream = new ByteArrayOutputStream();
    CommandLine commandLine = CommandLine.parse(command);
    DefaultExecutor exec = new DefaultExecutor();

    PumpStreamHandler streamHandler = new PumpStreamHandler(susStream, errStream);
    exec.setStreamHandler(streamHandler);
    ExecuteResultHandler erh = new ExecuteResultHandler() {
        @Override
        public void onProcessComplete(int exitValue) {
            try {
                String suc = susStream.toString("GBK");
                System.out.println(suc);
                System.out.println("3. 异步执行完成");
            } catch (UnsupportedEncodingException uee) {
                uee.printStackTrace();
            }
        }
        @Override
        public void onProcessFailed(ExecuteException e) {
            try {
                String err = errStream.toString("GBK");
                System.out.println(err);
                System.out.println("3. 异步执行出错");
            } catch (UnsupportedEncodingException uee) {
                uee.printStackTrace();
            }
        }
    };
    System.out.println("1. 开始执行");
    exec.execute(commandLine, erh);
    System.out.println("2. 做其他操作");
    // 避免主线程退出导致程序退出
    Thread.currentThread().join();
}

监控

commons-exec支持监控外部进程的执行状态并做一些操作,如超时,停止等。

在使用Runtime.getRuntime().exec(cmd)执行某些系统命令,如nfs共享的mount时,会由于nfs服务异常等原因导致进程阻塞,使程序没法往下执行,而且也无法捕获到异常,相当于卡死在了。这时如果有超时放弃的功能就好了,当然超时功能可以自己轮询process.exitValue()去实现,稍微麻烦一些。

commons-exec主要通过ExecuteWatchdog类来处理超时。

String command = "ping 192.168.1.10";
ByteArrayOutputStream susStream = new ByteArrayOutputStream();
ByteArrayOutputStream errStream = new ByteArrayOutputStream();
CommandLine commandLine = CommandLine.parse(command);
DefaultExecutor exec = new DefaultExecutor();
//设置一分钟超时
ExecuteWatchdog watchdog = new ExecuteWatchdog(60*1000);
exec.setWatchdog(watchdog);
PumpStreamHandler streamHandler = new PumpStreamHandler(susStream, errStream);
exec.setStreamHandler(streamHandler);
try {
    int code = exec.execute(commandLine);
    System.out.println("result code: " + code);
    // 不同操作系统注意编码,否则结果乱码
    String suc = susStream.toString("GBK");
    String err = errStream.toString("GBK");
    System.out.println(suc+err);
} catch (ExecuteException e) {
    if (watchdog.killedProcess()) {
        // 被watchdog故意杀死
        System.err.println("超时了");
    }
}

ExecuteWatchdog还支持销毁进程,只需调用destroyProcess(),由于ExecuteWatchdog是异步执行的,所以调用后不会马上停止。 

Commons Email

Apache Commons Email是用Java发送电子邮件的工具类库,提供了几种发送电子邮件的API。它构建在Java Mail API 之上,主要是为了简化它。

commons-email目前最新版本是1.5,最低要求Java6以上。

pom.xml依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-email</artifactId>
    <version>1.5</version>
</dependency>

常用 API

Commons-Email的核心是Email类,它是一个抽象类,提供了发送邮件的核心功能。具体有以下几个实现类:

  • SimpleEmail:发送简单邮件,即纯文本邮件
  • MultiPartEmail:发送带附件的邮件
  • HtmlEmail:发送超文本邮件
  • ImageHtmlEmail:发送图文混排的超文本邮件

邮箱设置

想要通过程序发送邮件,首先需要有一个邮箱做为发件人,并开启POP3/SMTP服务,获取对应邮箱的授权码,不同的邮箱开启方式和位置不甚相同,以下以163邮箱和QQ邮箱为例说明。

163邮箱

进入邮箱首页,点击设置,选择下拉选项的POP3/SMTP/IMAP,点击进入“POP3/SMTP/IMAP”部分 ,点击“POP3/SMTP服务”后的【开启】,弹出页面,用绑定手机扫码发送短信,发送完毕后可获得授权码。

QQ邮箱

进入邮箱首页,点击设置,切换到账户页签,将页面滑动到“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务 ”部分,点击“POP3/SMTP服务”后的【开启】,会弹出页面提醒用QQ绑定的手机号发送短信,发送完毕后,点击“我已发送”,即可获得授权码。

简单例子

简单邮件就是纯文本邮件,格式简单,下面看看使用Java的API和Commons-Email的API的例子

Java API示例

// 使用Java Mail API
Properties prop = new Properties();
prop.setProperty("mail.host", "smtp.qq.com");
prop.setProperty("mail.transport.protocol", "smtp");
prop.setProperty("mail.smtp.auth", "true");
// 创建session
Session session = Session.getInstance(prop);
// Session的debug模式,设置为true能够查看到程序发送Email的运行状态
// session.setDebug(true);
Transport ts = session.getTransport();
// 以qq邮箱为例,需要开启POP3/SMTP服务,生成授权码供connect时使用
ts.connect("from@qq.com", "xxxxxxxxx");
// 创建邮件对象
MimeMessage message = new MimeMessage(session);
message.setFrom("from@qq.com");
message.setRecipient(Message.RecipientType.TO, new InternetAddress("to@163.com"));
message.setSubject("javamail简单文本邮件");
message.setContent("这是一封javamail发送的简单邮件", "text/html;charset=UTF-8");

// 发送
ts.sendMessage(message, message.getAllRecipients());
ts.close();

Commons Email 示例

// Commons Email 示例
Email email = new SimpleEmail();
// 发送邮件的SMTP服务器,如果不设置,默认查询系统变量mail.smtp.host的值,没有则会抛出异常
// org.apache.commons.mail.EmailException: Cannot find valid hostname for mail session
email.setHostName("smtp.qq.com");
email.setSmtpPort(465);
// javax.mail.AuthenticationFailedException: 535 Login fail. Authorization code is expired
email.setAuthenticator(new DefaultAuthenticator("from@qq.com", "xxxxxxxxx"));
email.setSSLOnConnect(true);
email.setFrom("from@qq.com");
email.setSubject("TestMail");
email.setMsg("这是一封简单的邮件This is the third test mail ... :-)");
email.addTo("to@163.com");
email.send();

复杂邮件

复杂邮件带格式,图片,附件等。

Java API示例

// 使用Java Mail API
Properties prop = new Properties();
prop.setProperty("mail.host", "smtp.qq.com");
prop.setProperty("mail.transport.protocol", "smtp");
prop.setProperty("mail.smtp.auth", "true");
// 创建session
Session session = Session.getInstance(prop);
Transport ts = session.getTransport();
ts.connect("smtp.qq.com", "from@qq.com", "xxxxxxxx");
MimeMessage message = new MimeMessage(session);
// 设置基本信息
message.setFrom(new InternetAddress("from@qq.com"));
message.setRecipient(Message.RecipientType.TO, new InternetAddress("to@163.com"));
message.setSubject("javamail发送的比较全面的带有附件和带图片及正文的邮件");
// 内容
MimeBodyPart text = new MimeBodyPart();
text.setContent("简单的头像:<br/><img src='cid:portrait.jpg'>", "text/html;charset=UTF-8");
// 图片
MimeBodyPart image = new MimeBodyPart();
image.setDataHandler(new DataHandler(new FileDataSource("D:\\commons mail\\images\\5.jpg")));
image.setContentID("portrait.jpg");
// 附件1
MimeBodyPart attachment = new MimeBodyPart();
DataHandler handler = new DataHandler(new FileDataSource("D:\\commons mail\\files\\test.docx"));
attachment.setDataHandler(handler);
attachment.setFileName(handler.getName());
// 附件2
MimeBodyPart attachment2 = new MimeBodyPart();
DataHandler handler2 = new DataHandler(new FileDataSource("D:\\commons mail\\files\\test2.txt"));
attachment2.setDataHandler(handler2);
attachment2.setFileName(MimeUtility.encodeText(handler2.getName()));
// 构建
MimeMultipart part = new MimeMultipart();
part.addBodyPart(text);
part.addBodyPart(image);
part.setSubType("related");
MimeMultipart part2 = new MimeMultipart();
part2.addBodyPart(attachment);
part2.addBodyPart(attachment2);
MimeBodyPart content = new MimeBodyPart();
content.setContent(part);
part2.addBodyPart(content);
part2.setSubType("mixed");
message.setContent(part2);
message.saveChanges();
// 发送
ts.sendMessage(message, message.getAllRecipients());
ts.close();

Commons Email 示例

// Commons Email 示例
HtmlEmail email = new HtmlEmail();
// 设置基本信息
email.setHostName("smtp.qq.com");
email.setAuthenticator(new DefaultAuthenticator("from@qq.com", "xxxxxxxx"));
email.setFrom("from@qq.com");
email.addTo("to@163.com");
email.setSubject("commons mail发送的比较全面的带有附件和带图片及正文的邮件");
// 生成图片标识
String cid = email.embed(new File("D:\\commons mail\\images\\5.jpg"));
email.setCharset(EmailConstants.UTF_8);
email.setHtmlMsg("<html>简单的头像:<br/><img src=\"cid:" + cid + "\"></html>");
// 额外提示
email.setTextMsg("您的邮件不支持html格式");
// 附件
EmailAttachment attachment = new EmailAttachment();
attachment.setPath("D:\\commons mail\\files\\test.docx");
EmailAttachment attachment2 = new EmailAttachment();
attachment2.setPath("D:\\commons mail\\files\\test2.txt");
email.attach(attachment);
email.attach(attachment2);
// 发送
email.send();

ImageHtmlEmail

HtmlEmail其实也可以发送图片,那ImageHtmlEmail存在的意义是什么呢?

其实HtmlEmail发送图片都需要调用其embed方法,并将其返回值设置到<img>标签的src上。

// embed方法有多个重载方法,支持URL,本地文件等
String cid = email.embed(new File("D:\\commons mail\\images\\5.jpg"));
email.setHtmlMsg("<html><img src=\"cid:" + cid + "\"></html>");

ImageHtmlEmail只需要设置对应的Resolver。如果发送图片较多,使用此类会方便不少。

// 省略部分代码 ...
ImageHtmlEmail email = new ImageHtmlEmail();
URL url = new URL("http://example.com");
email.setDataSourceResolver(new DataSourceUrlResolver(url));
email.setHtmlMsg("<img src=\"http://example.com/test.png\"> ...");
// 省略部分代码 ...

如果发送多张不同来源的图片需要依赖DataSourceCompositeResolver类,是组合模式的一种实现,支持同时设置多个resolver。

// 省略部分代码 ...
ImageHtmlEmail email = new ImageHtmlEmail();
// 本地路径
File file = new File("D:\\commons email");
// 网络上的路径
URL url = new URL("http://example.com");
// 类路径
String classpath = "META-INF/img";
DataSourceResolver r1 = new DataSourceFileResolver(file);
DataSourceResolver r2 = new DataSourceUrlResolver(url);
DataSourceResolver r3 = new DataSourceClassPathResolver(classpath);
DataSourceResolver[] resolvers = {r1, r2, r3};
// 组合resolver, 可以接收多个resolver
DataSourceResolver compositeResolver = new DataSourceCompositeResolver(resolvers);
email.setDataSourceResolver(compositeResolver);
email.setHtmlMsg("图片1: <br>" +
        "<img src=\"D:\\commons email\\t1.png\">" +
        "图片2: <br>" +
        "<img src=\"http://example.com/t2.png\">" +
        "图片3: <br>" +
        "<img src=\"META-INF/img/t3.png\">");
// 省略部分代码 ...

 

posted @   残城碎梦  阅读(881)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示