diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java
index 89dea0133..e31563779 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/resource/InputStreamResource.java
@@ -13,11 +13,14 @@
package org.dromara.hutool.core.io.resource;
import org.dromara.hutool.core.io.IORuntimeException;
+import org.dromara.hutool.core.io.stream.ReaderInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.Reader;
import java.io.Serializable;
import java.net.URL;
+import java.nio.charset.Charset;
/**
* 基于{@link InputStream}的资源获取器
@@ -32,6 +35,16 @@ public class InputStreamResource implements Resource, Serializable {
private final InputStream in;
private final String name;
+ /**
+ * 构造
+ *
+ * @param reader {@link Reader}
+ * @param charset 编码
+ */
+ public InputStreamResource(final Reader reader, final Charset charset) {
+ this(new ReaderInputStream(reader, charset));
+ }
+
/**
* 构造
*
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/ReaderInputStream.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/ReaderInputStream.java
new file mode 100644
index 000000000..4e0716b06
--- /dev/null
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/ReaderInputStream.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2024. looly(loolly@aliyun.com)
+ * Hutool is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * https://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.hutool.core.io.stream;
+
+import org.dromara.hutool.core.io.IoUtil;
+import org.dromara.hutool.core.lang.Assert;
+import org.dromara.hutool.core.util.CharsetUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+/**
+ * {@link Reader}作为{@link InputStream}使用的实现。
+ * 参考:Apache Commons IO
+ *
+ * @author commons-io
+ */
+public class ReaderInputStream extends InputStream {
+ private final static int DEFAULT_BUFFER_SIZE = IoUtil.DEFAULT_BUFFER_SIZE;
+
+ private final Reader reader;
+ // 用于将字符转换为字节的CharsetEncoder
+ private final CharsetEncoder encoder;
+ private final CharBuffer encoderIn;
+ private final ByteBuffer encoderOut;
+ private CoderResult lastCoderResult;
+ private boolean endOfInput;
+
+ /**
+ * 构造,使用指定的字符集和默认缓冲区大小
+ *
+ * @param reader 提供字符数据的Reader
+ * @param charset 字符集,用于创建CharsetEncoder
+ */
+ public ReaderInputStream(final Reader reader, final Charset charset) {
+ this(reader, charset, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * 构造,使用指定的字符集和缓冲区大小
+ *
+ * @param reader 提供字符数据的Reader
+ * @param charset 字符集,用于创建CharsetEncoder
+ * @param bufferSize 缓冲区大小
+ */
+ public ReaderInputStream(final Reader reader, final Charset charset, final int bufferSize) {
+ this(reader, CharsetUtil.newEncoder(charset, CodingErrorAction.REPLACE), bufferSize);
+ }
+
+ /**
+ * 构造,使用默认的缓冲区大小
+ *
+ * @param reader 提供字符数据的Reader
+ * @param encoder 用于编码的CharsetEncoder
+ */
+ public ReaderInputStream(final Reader reader, final CharsetEncoder encoder) {
+ this(reader, encoder, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * 构造,允许指定缓冲区大小。
+ *
+ * @param reader 提供字符数据的Reader
+ * @param encoder 用于编码的CharsetEncoder
+ * @param bufferSize 缓冲区大小
+ */
+ public ReaderInputStream(final Reader reader, final CharsetEncoder encoder, final int bufferSize) {
+ this.reader = reader;
+ this.encoder = encoder;
+
+ encoderIn = CharBuffer.allocate(bufferSize);
+ encoderIn.flip();
+ encoderOut = ByteBuffer.allocate(bufferSize);
+ encoderOut.flip();
+ }
+
+ @Override
+ public int read(final byte[] b, int off, int len) throws IOException {
+ Assert.notNull(b, "Byte array must not be null");
+ if ((len < 0) || (off < 0) || (off + len > b.length)) {
+ throw new IndexOutOfBoundsException("Array Size=" + b.length + ", offset=" + off + ", length=" + len);
+ }
+
+ int read = 0;
+ if (len == 0) {
+ return 0;
+ }
+ while (len > 0) {
+ if (encoderOut.hasRemaining()) {
+ final int c = Math.min(encoderOut.remaining(), len);
+ encoderOut.get(b, off, c);
+ off += c;
+ len -= c;
+ read += c;
+ } else {
+ fillBuffer();
+ if ((endOfInput) && (!encoderOut.hasRemaining())) {
+ break;
+ }
+ }
+ }
+ return (read == 0) && (endOfInput) ? -1 : read;
+ }
+
+ @Override
+ public int read() throws IOException {
+ do {
+ if (encoderOut.hasRemaining()) {
+ return encoderOut.get() & 0xFF;
+ }
+ fillBuffer();
+ } while ((!endOfInput) || (encoderOut.hasRemaining()));
+ return -1;
+ }
+
+ @Override
+ public void close() throws IOException {
+ reader.close();
+ }
+
+ /**
+ * 填充缓冲区。
+ * 此方法用于从输入源读取数据,并将其编码后存储到输出缓冲区中。
+ * 它处理输入数据,直到达到输入的末尾或者编码过程中遇到需要停止的条件。
+ * 在这个过程中,它会更新编码器的状态以及输入输出缓冲区的状态。
+ *
+ * @throws IOException 如果在读取输入数据时发生IO异常。
+ */
+ private void fillBuffer() throws IOException {
+ // 如果输入未结束,并且上一次的编码结果是正常的(没有溢出或错误),则尝试读取更多数据
+ if ((!endOfInput) && ((lastCoderResult == null) || (lastCoderResult.isUnderflow()))) {
+ encoderIn.compact(); // 准备好输入缓冲区,以便接收新的数据
+ final int position = encoderIn.position(); // 记录当前读取位置
+
+ // 从reader中读取数据到encoderIn缓冲区
+ final int c = reader.read(encoderIn.array(), position, encoderIn.remaining());
+ if (c == -1) // 如果读取到输入末尾
+ endOfInput = true;
+ else {
+ // 更新读取位置,准备处理下一批数据
+ encoderIn.position(position + c);
+ }
+ encoderIn.flip(); // 反转输入缓冲区,使其准备好进行编码
+ }
+
+ // 准备输出缓冲区,以便接收编码后的数据
+ encoderOut.compact();
+ // 执行编码操作,将输入缓冲区的数据编码到输出缓冲区
+ lastCoderResult = encoder.encode(encoderIn, encoderOut, endOfInput);
+ // 反转输出缓冲区,使其准备好被写入到最终目的地
+ encoderOut.flip();
+ }
+}
+
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/WriterOutputStream.java b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/WriterOutputStream.java
new file mode 100644
index 000000000..7a132a8ad
--- /dev/null
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/io/stream/WriterOutputStream.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2024. looly(loolly@aliyun.com)
+ * Hutool is licensed under Mulan PSL v2.
+ * You can use this software according to the terms and conditions of the Mulan PSL v2.
+ * You may obtain a copy of Mulan PSL v2 at:
+ * https://license.coscl.org.cn/MulanPSL2
+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+ * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+ * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+ * See the Mulan PSL v2 for more details.
+ */
+
+package org.dromara.hutool.core.io.stream;
+
+import org.dromara.hutool.core.io.IoUtil;
+import org.dromara.hutool.core.util.CharsetUtil;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+
+/**
+ * 通过一个 Writer和一个CharsetDecoder实现将字节数据输出为字符数据。可以通过不同的构造函数配置缓冲区大小和是否立即写入。
+ * 来自:https://github.com/subchen/jetbrick-commons/blob/master/src/main/java/jetbrick/io/stream/WriterOutputStream.java
+ *
+ * @since 6.0.0
+ * @author subchen
+ */
+public class WriterOutputStream extends OutputStream {
+ private static final int DEFAULT_BUFFER_SIZE = IoUtil.DEFAULT_BUFFER_SIZE;
+
+ private final Writer writer;
+ private final CharsetDecoder decoder;
+ private final boolean writeImmediately;
+ private final ByteBuffer decoderIn;
+ private final CharBuffer decoderOut;
+
+ /**
+ * 构造函数,使用指定字符集和默认配置。
+ *
+ * @param writer 目标 Writer,用于写入字符数据
+ * @param charset 字符集,用于编码字节数据
+ */
+ public WriterOutputStream(final Writer writer, final Charset charset) {
+ this(writer, charset, DEFAULT_BUFFER_SIZE, false);
+ }
+
+ /**
+ * 构造函数,使用指定字符集、默认缓冲区大小和不立即写入配置。
+ *
+ * @param writer 目标 Writer,用于写入字符数据
+ * @param charset 字符集,用于编码字节数据
+ * @param bufferSize 缓冲区大小,用于控制字符数据的临时存储量
+ * @param writeImmediately 是否立即写入,如果为 true,则不使用内部缓冲区,每个字节立即被解码并写入
+ */
+ public WriterOutputStream(final Writer writer, final Charset charset, final int bufferSize, final boolean writeImmediately) {
+ this(writer, CharsetUtil.newDecoder(charset, CodingErrorAction.REPLACE), bufferSize, writeImmediately);
+ }
+
+ /**
+ * 构造,使用默认缓冲区大小和不立即写入配置。
+ *
+ * @param writer 目标 Writer,用于写入字符数据
+ * @param decoder 字符集解码器,用于将字节数据解码为字符数据
+ */
+ public WriterOutputStream(final Writer writer, final CharsetDecoder decoder) {
+ this(writer, decoder, DEFAULT_BUFFER_SIZE, false);
+ }
+
+ /**
+ * 构造,允许自定义缓冲区大小和是否立即写入的配置。
+ *
+ * @param writer 目标 Writer,用于写入字符数据
+ * @param decoder 字符集解码器,用于将字节数据解码为字符数据
+ * @param bufferSize 缓冲区大小,用于控制字符数据的临时存储量
+ * @param writeImmediately 是否立即写入,如果为 true,则不使用内部缓冲区,每个字节立即被解码并写入
+ */
+ public WriterOutputStream(final Writer writer, final CharsetDecoder decoder, final int bufferSize, final boolean writeImmediately) {
+ this.writer = writer;
+ this.decoder = decoder;
+ this.writeImmediately = writeImmediately;
+ this.decoderOut = CharBuffer.allocate(bufferSize);
+ this.decoderIn = ByteBuffer.allocate(128);
+ }
+
+ @Override
+ public void write(final byte[] b, int off, int len) throws IOException {
+ while (len > 0) {
+ final int c = Math.min(len, decoderIn.remaining());
+ decoderIn.put(b, off, c);
+ processInput(false);
+ len -= c;
+ off += c;
+ }
+ if (writeImmediately) flushOutput();
+ }
+
+ @Override
+ public void write(final byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ write(new byte[]{(byte) b}, 0, 1);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ flushOutput();
+ writer.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ processInput(true);
+ flushOutput();
+ writer.close();
+ }
+
+ private void processInput(final boolean endOfInput) throws IOException {
+ decoderIn.flip();
+ CoderResult coderResult;
+ while (true) {
+ coderResult = decoder.decode(decoderIn, decoderOut, endOfInput);
+ if (!coderResult.isOverflow()) break;
+ flushOutput();
+ }
+ if (!coderResult.isUnderflow()) {
+ throw new IOException("Unexpected coder result");
+ }
+
+ decoderIn.compact();
+ }
+
+ private void flushOutput() throws IOException {
+ if (decoderOut.position() > 0) {
+ writer.write(decoderOut.array(), 0, decoderOut.position());
+ decoderOut.rewind();
+ }
+ }
+}
diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java
index fabd0d897..c1acc20d1 100644
--- a/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java
+++ b/hutool-core/src/main/java/org/dromara/hutool/core/util/CharsetUtil.java
@@ -14,13 +14,12 @@ package org.dromara.hutool.core.util;
import org.dromara.hutool.core.io.CharsetDetector;
import org.dromara.hutool.core.io.file.FileUtil;
+import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.text.StrUtil;
import java.io.File;
import java.io.InputStream;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.nio.charset.UnsupportedCharsetException;
+import java.nio.charset.*;
/**
* 字符集工具类
@@ -236,4 +235,36 @@ public class CharsetUtil {
public static Charset detect(final int bufferSize, final InputStream in, final Charset... charsets) {
return CharsetDetector.detect(bufferSize, in, charsets);
}
+
+ /**
+ * 创建一个新的CharsetEncoder实例,配置指定的字符集和错误处理策略。
+ *
+ * @param charset 指定的字符集,不允许为null。
+ * @param action 对于不合法的字符或无法映射的字符的处理策略,不允许为null。
+ * @return 配置好的CharsetEncoder实例。
+ * @since 6.0.0
+ */
+ public static CharsetEncoder newEncoder(final Charset charset, final CodingErrorAction action) {
+ return Assert.notNull(charset)
+ .newEncoder()
+ .onMalformedInput(action)
+ .onUnmappableCharacter(action);
+ }
+
+ /**
+ * 创建一个新的CharsetDecoder实例,配置指定的字符集和错误处理行为。
+ *
+ * @param charset 指定的字符集,不允许为null。
+ * @param action 当遇到不合法的字符编码或不可映射字符时采取的行动,例如忽略、替换等。
+ * @return 配置好的CharsetDecoder实例,用于解码字符。
+ * @since 6.0.0
+ */
+ public static CharsetDecoder newDecoder(final Charset charset, final CodingErrorAction action) {
+ return Assert.notNull(charset)
+ .newDecoder()
+ .onMalformedInput(action)
+ .onUnmappableCharacter(action)
+ // 设置遇到无法解码的字符时的替换字符串。
+ .replaceWith("?");
+ }
}
diff --git a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java
index 7f89d4e0a..cb3337eea 100644
--- a/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java
+++ b/hutool-http/src/main/java/org/dromara/hutool/http/client/body/MultipartOutputStream.java
@@ -13,20 +13,17 @@
package org.dromara.hutool.http.client.body;
import org.dromara.hutool.core.convert.Convert;
-import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil;
-import org.dromara.hutool.core.io.resource.HttpResource;
-import org.dromara.hutool.core.io.resource.MultiResource;
-import org.dromara.hutool.core.io.resource.Resource;
-import org.dromara.hutool.core.io.resource.StringResource;
+import org.dromara.hutool.core.io.file.FileUtil;
+import org.dromara.hutool.core.io.resource.*;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.http.HttpGlobalConfig;
import org.dromara.hutool.http.meta.ContentType;
-import java.io.IOException;
-import java.io.OutputStream;
+import java.io.*;
import java.nio.charset.Charset;
+import java.nio.file.Path;
/**
* Multipart/form-data输出流封装
@@ -110,6 +107,16 @@ public class MultipartOutputStream extends OutputStream {
if (value instanceof Resource) {
appendResource(formFieldName, (Resource) value);
+ }else if(value instanceof File) {
+ appendResource(formFieldName, new FileResource((File) value));
+ }else if(value instanceof Path) {
+ appendResource(formFieldName, new FileResource((Path) value));
+ } else if(value instanceof byte[]) {
+ appendResource(formFieldName, new BytesResource((byte[]) value));
+ } else if(value instanceof InputStream) {
+ appendResource(formFieldName, new InputStreamResource((InputStream) value));
+ } else if(value instanceof Reader) {
+ appendResource(formFieldName, new InputStreamResource((Reader) value, this.charset));
} else {
appendResource(formFieldName,
new StringResource(Convert.toStr(value), null, this.charset));