diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26f56fd72..5ee14fef3 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
### 🐣新特性
* 【core 】 `ListUtil`增加`zip`方法(pr#4052@Github)
+* 【http 】 增加`JakartaSoapClient`(issue#4103@Github)
### 🐞Bug修复
* 【jwt 】 修复verify方法在定义alg为`none`时验证失效问题(issue#4105@Github)
diff --git a/hutool-http/pom.xml b/hutool-http/pom.xml
index 778648d7d..6406ece8a 100755
--- a/hutool-http/pom.xml
+++ b/hutool-http/pom.xml
@@ -38,5 +38,11 @@
1.4.0
provided
+
+ jakarta.xml.soap
+ jakarta.xml.soap-api
+ 2.0.1
+ provided
+
diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapClient.java b/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapClient.java
new file mode 100644
index 000000000..5970307cd
--- /dev/null
+++ b/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapClient.java
@@ -0,0 +1,654 @@
+package cn.hutool.http.webservice;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.XmlUtil;
+import cn.hutool.http.HttpBase;
+import cn.hutool.http.HttpGlobalConfig;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import jakarta.xml.soap.*;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * SOAP客户端
+ *
+ *
+ * 此对象用于构建一个SOAP消息,并通过HTTP接口发出消息内容。
+ * SOAP消息本质上是一个XML文本,可以通过调用{@link #getMsgStr(boolean)} 方法获取消息体
+ *
{
+
+ /**
+ * XML消息体的Content-Type
+ * soap1.1 : text/xml
+ * soap1.2 : application/soap+xml
+ * soap1.1与soap1.2区别: https://www.cnblogs.com/qlqwjy/p/7577147.html
+ */
+ private static final String CONTENT_TYPE_SOAP11_TEXT_XML = "text/xml;charset=";
+ private static final String CONTENT_TYPE_SOAP12_SOAP_XML = "application/soap+xml;charset=";
+
+ /**
+ * 请求的URL地址
+ */
+ private String url;
+
+ /**
+ * 默认连接超时
+ */
+ private int connectionTimeout = HttpGlobalConfig.getTimeout();
+ /**
+ * 默认读取超时
+ */
+ private int readTimeout = HttpGlobalConfig.getTimeout();
+
+ /**
+ * 消息工厂,用于创建消息
+ */
+ private MessageFactory factory;
+ /**
+ * SOAP消息
+ */
+ private SOAPMessage message;
+ /**
+ * 消息方法节点
+ */
+ private SOAPBodyElement methodEle;
+ /**
+ * 应用于方法上的命名空间URI
+ */
+ private final String namespaceURI;
+ /**
+ * Soap协议
+ * soap1.1 : text/xml
+ * soap1.2 : application/soap+xml
+ */
+ private final SoapProtocol protocol;
+
+ /**
+ * 创建SOAP客户端,默认使用soap1.1版本协议
+ *
+ * @param url WS的URL地址
+ * @return this
+ */
+ public static JakartaSoapClient create(String url) {
+ return new JakartaSoapClient(url);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @return this
+ */
+ public static JakartaSoapClient create(String url, SoapProtocol protocol) {
+ return new JakartaSoapClient(url, protocol);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @param namespaceURI 方法上的命名空间URI
+ * @return this
+ * @since 4.5.6
+ */
+ public static JakartaSoapClient create(String url, SoapProtocol protocol, String namespaceURI) {
+ return new JakartaSoapClient(url, protocol, namespaceURI);
+ }
+
+ /**
+ * 构造,默认使用soap1.1版本协议
+ *
+ * @param url WS的URL地址
+ */
+ public JakartaSoapClient(String url) {
+ this(url, SoapProtocol.SOAP_1_1);
+ }
+
+ /**
+ * 构造
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议版本,见{@link SoapProtocol}
+ */
+ public JakartaSoapClient(String url, SoapProtocol protocol) {
+ this(url, protocol, null);
+ }
+
+ /**
+ * 构造
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议版本,见{@link SoapProtocol}
+ * @param namespaceURI 方法上的命名空间URI
+ * @since 4.5.6
+ */
+ public JakartaSoapClient(String url, SoapProtocol protocol, String namespaceURI) {
+ this.url = url;
+ this.namespaceURI = namespaceURI;
+ this.protocol = protocol;
+ init(protocol);
+ }
+
+ /**
+ * 初始化
+ *
+ * @param protocol 协议版本枚举,见{@link SoapProtocol}
+ * @return this
+ */
+ public JakartaSoapClient init(SoapProtocol protocol) {
+ // 创建消息工厂
+ try {
+ this.factory = MessageFactory.newInstance(protocol.getValue());
+ // 根据消息工厂创建SoapMessage
+ this.message = factory.createMessage();
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ return this;
+ }
+
+ /**
+ * 重置SOAP客户端,用于客户端复用
+ *
+ *
+ * 重置后需调用serMethod方法重新指定请求方法,并调用setParam方法重新定义参数
+ *
+ * @return this
+ * @since 4.6.7
+ */
+ public JakartaSoapClient reset() {
+ try {
+ this.message = factory.createMessage();
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+ this.methodEle = null;
+
+ return this;
+ }
+
+ /**
+ * 设置编码
+ *
+ * @param charset 编码
+ * @return this
+ * @see #charset(Charset)
+ */
+ public JakartaSoapClient setCharset(Charset charset) {
+ return this.charset(charset);
+ }
+
+ @Override
+ public JakartaSoapClient charset(Charset charset) {
+ super.charset(charset);
+ try {
+ this.message.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, this.charset());
+ this.message.setProperty(SOAPMessage.WRITE_XML_DECLARATION, "true");
+ } catch (SOAPException e) {
+ // ignore
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置Webservice请求地址
+ *
+ * @param url Webservice请求地址
+ * @return this
+ */
+ public JakartaSoapClient setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param name 头信息标签名
+ * @param actorURI 中间的消息接收者
+ * @param roleUri Role的URI
+ * @param mustUnderstand 标题项对于要对其进行处理的接收者来说是强制的还是可选的
+ * @param relay relay属性
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.4
+ */
+ public SOAPHeaderElement addSOAPHeader(QName name, String actorURI, String roleUri, Boolean mustUnderstand, Boolean relay) {
+ final SOAPHeaderElement ele = addSOAPHeader(name);
+ try {
+ if (StrUtil.isNotBlank(roleUri)) {
+ ele.setRole(roleUri);
+ }
+ if (null != relay) {
+ ele.setRelay(relay);
+ }
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ if (StrUtil.isNotBlank(actorURI)) {
+ ele.setActor(actorURI);
+ }
+ if (null != mustUnderstand) {
+ ele.setMustUnderstand(mustUnderstand);
+ }
+
+ return ele;
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param localName 头节点名称
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.7
+ */
+ public SOAPHeaderElement addSOAPHeader(String localName) {
+ return addSOAPHeader(new QName(localName));
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param localName 头节点名称
+ * @param value 头节点的值
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.7
+ */
+ public SOAPHeaderElement addSOAPHeader(String localName, String value) {
+ final SOAPHeaderElement soapHeaderElement = addSOAPHeader(localName);
+ soapHeaderElement.setTextContent(value);
+ return soapHeaderElement;
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param name 头节点名称
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.4
+ */
+ public SOAPHeaderElement addSOAPHeader(QName name) {
+ SOAPHeaderElement ele;
+ try {
+ ele = this.message.getSOAPHeader().addHeaderElement(name);
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+ return ele;
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param name 方法名及其命名空间
+ * @param params 参数
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ */
+ public JakartaSoapClient setMethod(Name name, Map params, boolean useMethodPrefix) {
+ return setMethod(new QName(name.getURI(), name.getLocalName(), name.getPrefix()), params, useMethodPrefix);
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param name 方法名及其命名空间
+ * @param params 参数
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ */
+ public JakartaSoapClient setMethod(QName name, Map params, boolean useMethodPrefix) {
+ setMethod(name);
+ final String prefix = useMethodPrefix ? name.getPrefix() : null;
+ final SOAPBodyElement methodEle = this.methodEle;
+ for (Entry entry : MapUtil.wrap(params)) {
+ setParam(methodEle, entry.getKey(), entry.getValue(), prefix);
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置请求方法
+ * 方法名自动识别前缀,前缀和方法名使用“:”分隔
+ * 当识别到前缀后,自动添加xmlns属性,关联到默认的namespaceURI
+ *
+ * @param methodName 方法名
+ * @return this
+ */
+ public JakartaSoapClient setMethod(String methodName) {
+ return setMethod(methodName, ObjectUtil.defaultIfNull(this.namespaceURI, XMLConstants.NULL_NS_URI));
+ }
+
+ /**
+ * 设置请求方法
+ * 方法名自动识别前缀,前缀和方法名使用“:”分隔
+ * 当识别到前缀后,自动添加xmlns属性,关联到传入的namespaceURI
+ *
+ * @param methodName 方法名(可有前缀也可无)
+ * @param namespaceURI 命名空间URI
+ * @return this
+ */
+ public JakartaSoapClient setMethod(String methodName, String namespaceURI) {
+ final List methodNameList = StrUtil.split(methodName, ':');
+ final QName qName;
+ if (2 == methodNameList.size()) {
+ qName = new QName(namespaceURI, methodNameList.get(1), methodNameList.get(0));
+ } else {
+ qName = new QName(namespaceURI, methodName);
+ }
+ return setMethod(qName);
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param name 方法名及其命名空间
+ * @return this
+ */
+ public JakartaSoapClient setMethod(QName name) {
+ try {
+ this.methodEle = this.message.getSOAPBody().addBodyElement(name);
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置方法参数,使用方法的前缀
+ *
+ * @param name 参数名
+ * @param value 参数值,可以是字符串或Map或{@link SOAPElement}
+ * @return this
+ */
+ public JakartaSoapClient setParam(String name, Object value) {
+ return setParam(name, value, true);
+ }
+
+ /**
+ * 设置方法参数
+ *
+ * @param name 参数名
+ * @param value 参数值,可以是字符串或Map或{@link SOAPElement}
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ */
+ public JakartaSoapClient setParam(String name, Object value, boolean useMethodPrefix) {
+ setParam(this.methodEle, name, value, useMethodPrefix ? this.methodEle.getPrefix() : null);
+ return this;
+ }
+
+ /**
+ * 批量设置参数,使用方法的前缀
+ *
+ * @param params 参数列表
+ * @return this
+ * @since 4.5.6
+ */
+ public JakartaSoapClient setParams(Map params) {
+ return setParams(params, true);
+ }
+
+ /**
+ * 批量设置参数
+ *
+ * @param params 参数列表
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ * @since 4.5.6
+ */
+ public JakartaSoapClient setParams(Map params, boolean useMethodPrefix) {
+ for (Entry entry : MapUtil.wrap(params)) {
+ setParam(entry.getKey(), entry.getValue(), useMethodPrefix);
+ }
+ return this;
+ }
+
+ /**
+ * 获取方法节点
+ * 用于创建子节点等操作
+ *
+ * @return {@link SOAPBodyElement}
+ * @since 4.5.6
+ */
+ public SOAPBodyElement getMethodEle() {
+ return this.methodEle;
+ }
+
+ /**
+ * 获取SOAP消息对象 {@link SOAPMessage}
+ *
+ * @return {@link SOAPMessage}
+ * @since 4.5.6
+ */
+ public SOAPMessage getMessage() {
+ return this.message;
+ }
+
+ /**
+ * 获取SOAP请求消息
+ *
+ * @param pretty 是否格式化
+ * @return 消息字符串
+ */
+ public String getMsgStr(boolean pretty) {
+ return JakartaSoapUtil.toString(this.message, pretty, this.charset);
+ }
+
+ /**
+ * 将SOAP消息的XML内容输出到流
+ *
+ * @param out 输出流
+ * @return this
+ * @since 4.5.6
+ */
+ public JakartaSoapClient write(OutputStream out) {
+ try {
+ this.message.writeTo(out);
+ } catch (SOAPException | IOException e) {
+ throw new SoapRuntimeException(e);
+ }
+ return this;
+ }
+
+ /**
+ * 设置超时,单位:毫秒
+ * 超时包括:
+ *
+ *
+ * 1. 连接超时
+ * 2. 读取响应超时
+ *
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @see #setConnectionTimeout(int)
+ * @see #setReadTimeout(int)
+ */
+ public JakartaSoapClient timeout(int milliseconds) {
+ setConnectionTimeout(milliseconds);
+ setReadTimeout(milliseconds);
+ return this;
+ }
+
+ /**
+ * 设置连接超时,单位:毫秒
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @since 4.5.6
+ */
+ public JakartaSoapClient setConnectionTimeout(int milliseconds) {
+ this.connectionTimeout = milliseconds;
+ return this;
+ }
+
+ /**
+ * 设置连接超时,单位:毫秒
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @since 4.5.6
+ */
+ public JakartaSoapClient setReadTimeout(int milliseconds) {
+ this.readTimeout = milliseconds;
+ return this;
+ }
+
+ /**
+ * 执行Webservice请求,即发送SOAP内容
+ *
+ * @return 返回结果
+ */
+ public SOAPMessage sendForMessage() {
+ final HttpResponse res = sendForResponse();
+ final MimeHeaders headers = new MimeHeaders();
+ for (Entry> entry : res.headers().entrySet()) {
+ if (StrUtil.isNotEmpty(entry.getKey())) {
+ headers.setHeader(entry.getKey(), CollUtil.get(entry.getValue(), 0));
+ }
+ }
+ try {
+ return this.factory.createMessage(headers, res.bodyStream());
+ } catch (IOException | SOAPException e) {
+ throw new SoapRuntimeException(e);
+ } finally {
+ IoUtil.close(res);
+ }
+ }
+
+ /**
+ * 执行Webservice请求,即发送SOAP内容
+ *
+ * @return 返回结果
+ */
+ public String send() {
+ return send(false);
+ }
+
+ /**
+ * 执行Webservice请求,即发送SOAP内容
+ *
+ * @param pretty 是否格式化
+ * @return 返回结果
+ */
+ public String send(boolean pretty) {
+ final String body = sendForResponse().body();
+ return pretty ? XmlUtil.format(body) : body;
+ }
+
+ // -------------------------------------------------------------------------------------------------------- Private method start
+
+ /**
+ * 发送请求,获取异步响应
+ *
+ * @return 响应对象
+ */
+ public HttpResponse sendForResponse() {
+ return HttpRequest.post(this.url)//
+ .setFollowRedirects(true)//
+ .setConnectionTimeout(this.connectionTimeout)
+ .setReadTimeout(this.readTimeout)
+ .contentType(getXmlContentType())//
+ .header(this.headers())
+ .body(getMsgStr(false))//
+ .executeAsync();
+ }
+
+ /**
+ * 获取请求的Content-Type,附加编码信息
+ *
+ * @return 请求的Content-Type
+ */
+ private String getXmlContentType() {
+ switch (this.protocol){
+ case SOAP_1_1:
+ return CONTENT_TYPE_SOAP11_TEXT_XML.concat(this.charset.toString());
+ case SOAP_1_2:
+ return CONTENT_TYPE_SOAP12_SOAP_XML.concat(this.charset.toString());
+ default:
+ throw new SoapRuntimeException("Unsupported protocol: {}", this.protocol);
+ }
+ }
+
+ /**
+ * 设置方法参数
+ *
+ * @param ele 方法节点
+ * @param name 参数名
+ * @param value 参数值
+ * @param prefix 命名空间前缀, {@code null}表示不使用前缀
+ * @return {@link SOAPElement}子节点
+ */
+ @SuppressWarnings("rawtypes")
+ private static SOAPElement setParam(SOAPElement ele, String name, Object value, String prefix) {
+ final SOAPElement childEle;
+ try {
+ if (StrUtil.isNotBlank(prefix)) {
+ childEle = ele.addChildElement(name, prefix);
+ } else {
+ childEle = ele.addChildElement(name);
+ }
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ if (null != value) {
+ if (value instanceof SOAPElement) {
+ // 单个子节点
+ try {
+ ele.addChildElement((SOAPElement) value);
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+ } else if (value instanceof Map) {
+ // 多个字节点
+ Entry entry;
+ for (Object obj : ((Map) value).entrySet()) {
+ entry = (Entry) obj;
+ setParam(childEle, entry.getKey().toString(), entry.getValue(), prefix);
+ }
+ } else {
+ // 单个值
+ childEle.setValue(value.toString());
+ }
+ }
+
+ return childEle;
+ }
+ // -------------------------------------------------------------------------------------------------------- Private method end
+}
diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapProtocol.java b/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapProtocol.java
new file mode 100644
index 000000000..08f202fc6
--- /dev/null
+++ b/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapProtocol.java
@@ -0,0 +1,36 @@
+package cn.hutool.http.webservice;
+
+import jakarta.xml.soap.SOAPConstants;
+
+/**
+ * SOAP协议版本枚举
+ *
+ * @author looly
+ *
+ */
+public enum JakartaSoapProtocol {
+ /** SOAP 1.1协议 */
+ SOAP_1_1(SOAPConstants.SOAP_1_1_PROTOCOL),
+ /** SOAP 1.2协议 */
+ SOAP_1_2(SOAPConstants.SOAP_1_2_PROTOCOL);
+
+ /**
+ * 构造
+ *
+ * @param value {@link SOAPConstants} 中的协议版本值
+ */
+ JakartaSoapProtocol(String value) {
+ this.value = value;
+ }
+
+ private final String value;
+
+ /**
+ * 获取版本值信息
+ *
+ * @return 版本值信息
+ */
+ public String getValue() {
+ return this.value;
+ }
+}
diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapUtil.java b/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapUtil.java
new file mode 100644
index 000000000..f8bf9faae
--- /dev/null
+++ b/hutool-http/src/main/java/cn/hutool/http/webservice/JakartaSoapUtil.java
@@ -0,0 +1,91 @@
+package cn.hutool.http.webservice;
+
+import cn.hutool.core.exceptions.UtilException;
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.XmlUtil;
+
+import jakarta.xml.soap.SOAPException;
+import jakarta.xml.soap.SOAPMessage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * SOAP相关工具类
+ *
+ * @author looly
+ * @since 4.5.7
+ */
+public class JakartaSoapUtil {
+
+ /**
+ * 创建SOAP客户端,默认使用soap1.1版本协议
+ *
+ * @param url WS的URL地址
+ * @return {@link SoapClient}
+ */
+ public static SoapClient createClient(String url) {
+ return SoapClient.create(url);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @return {@link SoapClient}
+ */
+ public static SoapClient createClient(String url, SoapProtocol protocol) {
+ return SoapClient.create(url, protocol);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @param namespaceURI 方法上的命名空间URI
+ * @return {@link SoapClient}
+ * @since 4.5.6
+ */
+ public static SoapClient createClient(String url, SoapProtocol protocol, String namespaceURI) {
+ return SoapClient.create(url, protocol, namespaceURI);
+ }
+
+ /**
+ * {@link SOAPMessage} 转为字符串
+ *
+ * @param message SOAP消息对象
+ * @param pretty 是否格式化
+ * @return SOAP XML字符串
+ */
+ public static String toString(SOAPMessage message, boolean pretty) {
+ return toString(message, pretty, CharsetUtil.CHARSET_UTF_8);
+ }
+
+ /**
+ * {@link SOAPMessage} 转为字符串
+ *
+ * @param message SOAP消息对象
+ * @param pretty 是否格式化
+ * @param charset 编码
+ * @return SOAP XML字符串
+ * @since 4.5.7
+ */
+ public static String toString(SOAPMessage message, boolean pretty, Charset charset) {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ message.writeTo(out);
+ } catch (SOAPException | IOException e) {
+ throw new SoapRuntimeException(e);
+ }
+ String messageToString;
+ try {
+ messageToString = out.toString(charset.toString());
+ } catch (UnsupportedEncodingException e) {
+ throw new UtilException(e);
+ }
+ return pretty ? XmlUtil.format(messageToString) : messageToString;
+ }
+}
diff --git a/hutool-http/src/test/java/cn/hutool/http/webservice/JakartaSoapClientTest.java b/hutool-http/src/test/java/cn/hutool/http/webservice/JakartaSoapClientTest.java
new file mode 100644
index 000000000..8069b2f51
--- /dev/null
+++ b/hutool-http/src/test/java/cn/hutool/http/webservice/JakartaSoapClientTest.java
@@ -0,0 +1,41 @@
+package cn.hutool.http.webservice;
+
+import cn.hutool.core.lang.Console;
+import cn.hutool.core.util.CharsetUtil;
+import jakarta.xml.soap.SOAPException;
+import jakarta.xml.soap.SOAPMessage;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/**
+ * SOAP相关单元测试
+ *
+ * @author looly
+ *
+ */
+public class JakartaSoapClientTest {
+
+ @Test
+ @Disabled
+ public void requestTest() {
+ JakartaSoapClient client = JakartaSoapClient.create("http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx")
+ .setMethod("web:getCountryCityByIp", "http://WebXml.com.cn/")
+ .setCharset(CharsetUtil.CHARSET_GBK)
+ .setParam("theIpAddress", "218.21.240.106");
+
+ Console.log(client.getMsgStr(true));
+
+ Console.log(client.send(true));
+ }
+
+ @Test
+ @Disabled
+ public void requestForMessageTest() throws SOAPException {
+ JakartaSoapClient client = JakartaSoapClient.create("http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx")
+ .setMethod("web:getCountryCityByIp", "http://WebXml.com.cn/")
+ .setParam("theIpAddress", "218.21.240.106");
+
+ SOAPMessage message = client.sendForMessage();
+ Console.log(message.getSOAPBody().getTextContent());
+ }
+}