跳转到内容

JQuick-Curl

纯 Java · Apache-2.0 · 传输层基于 OkHttp 4

把 curl 命令直接变成 Java API注解粘贴即用,也可以像 MyBatis 一样用 XML 集中管理 API

从浏览器开发者工具、Postman、Apifox 复制的 curl 命令原样粘贴到 @JCurlCommand 注解,方法即接口;需要集中治理 API 时,把 curl 模板维护在 apis.xml 里,像 MyBatis 一样用 if / foreach / choose 动态渲染,再以 3 行代码接到强类型接口。超时、重试、连接池、拦截器由 JQuickCurlConfig 一处配置,底层由带连接池的 OkHttp 完成传输。

同一个第三方接口调用的两种写法 — JQuick-Curl
同一个第三方接口调用的两种写法

左:手写 RestTemplate 样板;右:JQuick-Curl 粘贴即用

  • 0 行RestTemplate / OkHttp 样板代码
  • 3 种调用方式:注解 / XML / 编程式
  • 12 个完整支持的 curl 参数
  • JDK 8+无框架依赖,普通 Java 可用
11 行调用一个第三方 POST 接口的全部 Java 代码
public interface OrderApi {
// 粘贴 Postman「Copy as cURL」的原始命令即可
@JCurlCommand("curl -X POST https://api.example.com/orders "
+ "-H 'Content-Type: application/json' "
+ "-d '{\"sku\":\"A-1001\",\"count\":2}'")
String create(JQuickCurlReq request);
}
OrderApi api = JCurlInvoker.createProxy(OrderApi.class);
api.create(new JQuickCurlReq());

省略 import 与依赖坐标;接口由 JDK 动态代理生成,可注册为 Spring Bean 注入使用,代理实例无状态、可长期复用。

curl注解值就是这条命令本身
终端窗口
curl -X POST https://api.example.com/orders \
-H 'Content-Type: application/json' \
-d '{"sku":"A-1001","count":2}'

命令与 API 文档、Postman 调试一一对应,终端里原样可跑;接口文档升级时只改这一条字符串。

XML第二种姿势:类 MyBatis 的 XML 集中管理(apis.xml 节选)
<curls namespace="com.example.api.XmlUserApi">
<curl name="search" returnClass="java.lang.String">
curl -X GET #{host}/search
<if test="withTrace == true"> -H "X-Trace: #{traceId}" </if>
</curl>
<curl name="batchQuery" returnClass="java.lang.String">
curl -X POST #{host}/users/batch
-H "Content-Type: application/json"
-d '{"ids":[<foreach collection="ids" item="id" separator=",">#{id}</foreach>]}'
</curl>
</curls>

curl 模板集中在一份 XML 里,与 Java 代码解耦;#{变量} 占位符,if / foreach / choose 动态渲染请求头与请求体。

3 行XML 文件接到强类型接口
JQuickFactory factory = new JQuickXmlFactory(
new JQuickCurlXmlParseFactory(), "apis.xml");
XmlUserApi api = factory.createApi(XmlUserApi.class);
api.batchQuery(req); // 方法名即 XML 里的 <curl name>

方法名与 XML 里的 <curl name> 一一对应,JQuickXmlFactory 生成代理——接口上无需任何注解。

同样的第三方接口调用,手写 RestTemplate / OkHttp 要写多少

OkHttp 是优秀的传输层,JQuick-Curl 正是基于它构建——下表比较的是「写法」,不是「能力」。

对比维度JQuick-Curl手写 RestTemplate / OkHttp
上手方式粘贴 curl 即完成接口定义手写 HttpHeaders、HttpEntity 与 exchange 调用
文档与调试同步命令与 API 文档、Postman 一一对应,文档升级只改一条字符串翻译后的代码与文档脱节,变更需逐行比对
问题复现抓包或终端里的同一命令直接进代码需要人工把 curl 再翻译回 Java
超时 / 重试 / 连接池JQuickCurlConfig 一处链式配置,@JTimeout 方法级覆盖散落在 Client / RequestFactory 构造代码中
动态参数${} 占位符与 XML if / foreach / choose 模板手动字符串拼接并自行处理编码
API 集中管理curl 模板统一维护在 apis.xml,一处治理全部接口URL 与参数散落在各个调用点,无统一视图
Cookie / 代理 / 下载-b / -c / -x / -o 与 curl 语义一致逐个查找 API 重新实现
底层传输OkHttp 4.x + 连接池,与手写方案相同同为 OkHttp——差别只在写法

传输层就是 OkHttp:JQuick-Curl 只把「curl → 请求」的翻译层收进一条注解,OkHttp 的能力一个不少,两者同为成熟开源组件,可放心共存。

JQuick-Curl 是 JQuick 生态下的轻量级 Java HTTP 客户端。开发者将浏览器开发者工具、Postman、Apifox 中复制出的 curl 命令原样粘贴到 Java 代码中,框架通过自研的文法解析器自动解析命令,并基于带连接池的 OkHttp 传输层完成实际请求,无需手写 HttpClient / OkHttp 样板代码。

本项目已被 Awesome Java 收录,采用 Apache License 2.0 协议开源。

  • curl 命令即用:从浏览器、Postman、Apifox 复制 curl 命令,粘贴到注解值中即可调用,零改写成本。
  • 注解式声明 API:@JCurlCommand 标注在接口方法上,通过 JDK 动态代理生成可注入的客户端实例。
  • 编程式调用:JCurlInvoker 支持方法引用与完全动态的 curl 字符串两种编程入口。
  • 占位符变量:${变量名} 支持运行时替换,可覆盖 URL 路径、Query 参数、请求头、请求体。
  • XML 集中配置:请求模板可维护在 apis.xml 中,支持 <if>、<foreach>、<choose> 条件渲染。
  • 完备的请求能力:JSON、Form 表单、Multipart 文件上传、文件下载、Cookie、Basic Auth、代理、SSL。
  • 工程化配置:超时、重试、重定向、连接池、拦截器通过 JQuickCurlConfig 统一管理。
  • 扩展点开放:支持自定义 XML 解析处理器与自定义 Visitor 扩展 curl 语法。
  • Spring Boot / 普通 Java 项目中调用第三方 HTTP API,希望直接复用调试阶段的 curl 命令。
  • 对接外部开放平台(支付、征信、短信、地图等),接口以 curl 形式提供文档。
  • 数据采集、服务间内部调用,需要连接池、重试、代理、拦截器等工程能力。
  • 不希望引入并维护大量 RestTemplate / OkHttp 请求构建代码的团队。
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-curl</artifactId>
<version>2.6.0</version>
</dependency>

环境要求:JDK 8+;传输层基于 OkHttp 4.10.0,相关依赖由 Maven 自动传递引入。

JQuick-Curl 本身不依赖 Spring,也未发布官方 spring-boot-starter。在 Spring Boot 项目中,通过一个 @Configuration 类把动态代理注册为 Bean,即可获得与自动装配完全一致的注入体验。

package com.example.demo;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 声明 API 接口
interface HttpBinApi {
// 直接粘贴浏览器/Postman 中的 curl 命令
@com.github.paohaijiao.anno.JCurlCommand("curl -X GET https://httpbin.org/uuid")
String uuid(JQuickCurlReq request);
}
// 手动注册为 Spring Bean(等价于自动装配)
@Configuration
class JQuickCurlBeanConfig {
@Bean
public HttpBinApi httpBinApi() {
return JCurlInvoker.createProxy(HttpBinApi.class);
}
}

在任意 Spring 组件中通过构造器注入并调用。

package com.example.demo;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class DemoController {
private final HttpBinApi httpBinApi;
// 构造器注入
DemoController(HttpBinApi httpBinApi) {
this.httpBinApi = httpBinApi;
}
@GetMapping("/demo/uuid")
String uuid() {
return httpBinApi.uuid(new JQuickCurlReq());
}
}

不依赖任何容器,直接通过 JCurlInvoker.createProxy 创建代理实例即可调用。

package com.example.plain;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class PlainJavaDemo {
public interface SimpleApi {
// GET 请求,返回响应体字符串
@JCurlCommand("curl -X GET https://httpbin.org/get")
String get(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
SimpleApi api = JCurlInvoker.createProxy(SimpleApi.class);
String responseBody = api.get(new JQuickCurlReq());
System.out.println(responseBody);
}
}

在自定义接口的方法上标注 @JCurlCommand,注解值为完整的 curl 命令字符串(必须包含 curl 前缀与 -X 方法声明)。

package com.example.api;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface UserApi {
// 查询用户:GET
@JCurlCommand("curl -X GET https://httpbin.org/anything/user/1")
String getUser(JQuickCurlReq request);
// 创建用户:POST + JSON 请求体
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"Alice\",\"email\":\"[email protected]\"}'")
String createUser(JQuickCurlReq request);
}

通过 JCurlInvoker.createProxy 创建接口代理;在 Spring Boot 中通常将代理注册为单例 Bean 后注入使用。

package com.example.api;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class UserApiDemo {
public static void main(String[] args) throws Exception {
UserApi userApi = JCurlInvoker.createProxy(UserApi.class);
System.out.println(userApi.getUser(new JQuickCurlReq()));
System.out.println(userApi.createUser(new JQuickCurlReq()));
}
}
属性 作用 必填 类型 默认值
value curl 命令全文,须以 curl 开头,并显式包含 -X 请求方法 是 String 无
execute 为 false 时调用该方法将抛出 UnsupportedOperationException,用于占位或仅校验场景 否 boolean true
expectedStatus 期望的 HTTP 状态码。2.6.0 中为预留属性,当前版本不生效 否 int 200
expectedBusinessStatus 期望的业务状态码字符串。2.6.0 中为预留属性,当前版本不生效 否 String "200"
validationScript 基于路径表达式的校验脚本。2.6.0 中为预留属性,当前版本不生效 否 String ""

使用 execute = false 的示例。

package com.example.api;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface ReservedApi {
// 该接口尚未上线,调用将抛出 UnsupportedOperationException
@JCurlCommand(value = "curl -X GET https://httpbin.org/anything", execute = false)
String futureApi(JQuickCurlReq request);
}

一个接口可声明多个方法,分别对应不同的 HTTP 方法与请求形态。

package com.example.api;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
public interface CrudApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything/users")
String list(JQuickCurlReq request);
@JCurlCommand("curl -X POST https://httpbin.org/anything/users " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"Bob\"}'")
String create(JQuickCurlReq request);
@JCurlCommand("curl -X PUT https://httpbin.org/anything/users/1 " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"Bob Updated\"}'")
String update(JQuickCurlReq request);
@JCurlCommand("curl -X PATCH https://httpbin.org/anything/users/1 " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"B\"}'")
String patch(JQuickCurlReq request);
@JCurlCommand("curl -X DELETE https://httpbin.org/anything/users/1")
String delete(JQuickCurlReq request);
// 返回完整响应包装对象,可读取响应头
@JCurlCommand("curl -X HEAD https://httpbin.org/anything/users/1")
JQuickCurlResponseBody head(JQuickCurlReq request);
}

XML集中配置(apis.xml,支持 if/foreach/choose)

Section titled “XML集中配置(apis.xml,支持 if/foreach/choose)”

请求模板可以集中维护在类路径下的 XML 文件中,与 Java 代码解耦。XML 方式使用 #{变量名} 占位符。

在 src/main/resources/apis.xml 中定义模板。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE curls PUBLIC "-//PAOHAIJIAO//DTD API CURL 1.0//EN"
"classpath:paohaijiao/dtd/Jquick-curl.dtd">
<curls namespace="com.example.api.XmlUserApi">
<curl name="getUser" returnClass="java.lang.String">
curl -X GET #{host}/users/1
</curl>
<curl name="search" returnClass="java.lang.String">
curl -X GET #{host}/search
<if test="withTrace == true"> -H "X-Trace: #{traceId}" </if>
</curl>
<curl name="batchQuery" returnClass="java.lang.String">
curl -X POST #{host}/users/batch
-H "Content-Type: application/json"
-d '{"ids":[<foreach collection="ids" item="id" separator=",">#{id}</foreach>]}'
</curl>
<curl name="chooseDemo" returnClass="java.lang.String">
curl -X GET #{host}/users
<choose>
<when test="role == 'admin'"> -H "X-Scope: all" </when>
<otherwise> -H "X-Scope: self" </otherwise>
</choose>
</curl>
</curls>

定义与 <curl name> 同名的 Java 接口方法,并通过 JQuickXmlFactory 创建代理。

package com.example.api;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.xml.JQuickCurlXmlParseFactory;
import com.github.paohaijiao.xml.factory.JQuickFactory;
import com.github.paohaijiao.xml.factory.JQuickXmlFactory;
import com.github.paohaijiao.xml.handler.JQuickParseHandler;
public interface XmlUserApi {
String getUser(JQuickCurlReq request);
}
class XmlDemo {
public static void main(String[] args) throws Exception {
JQuickParseHandler parser = new JQuickCurlXmlParseFactory();
JQuickFactory factory = new JQuickXmlFactory(parser, "apis.xml");
XmlUserApi api = factory.createApi(XmlUserApi.class);
JQuickCurlReq req = new JQuickCurlReq();
req.put("host", "https://httpbin.org/anything");
System.out.println(api.getUser(req));
}
}

XML 元素属性说明:

元素/属性 作用 必填 默认值
<curls namespace> 命名空间,须与接口全限定名一致 是 无
<curl name> 方法名,须与接口方法名完全一致 是 无
<curl returnClass> 返回类型全限定名,如 java.lang.String、byte[] 是 无
<if test> OGNL 表达式,为真时渲染内容 使用条件渲染时必填 无
<foreach collection item> 集合遍历;另支持 open、close、separator 属性 使用循环时必填 无
<choose><when test><otherwise> 多分支条件选择 使用分支时必填 无

除注解代理外,可以直接使用 JQuickCurlExecutor 执行运行时动态拼接的 curl 命令字符串,返回 JQuickCurlResponseBody。

package com.example.programmatic;
import com.github.paohaijiao.executor.JQuickCurlExecutor;
import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
public class DynamicCurlDemo {
public static void main(String[] args) throws Exception {
String resourceId = "1001";
// 运行时动态构建 curl 命令
String curlCommand = "curl -X GET https://httpbin.org/anything/resource/" + resourceId;
JQuickCurlExecutor executor = new JQuickCurlExecutor(new JContext());
JQuickCurlResponseBody responseBody = executor.execute(curlCommand);
System.out.println(responseBody.asString());
}
}

获取完整响应元信息(状态码、响应头)

Section titled “获取完整响应元信息(状态码、响应头)”

将方法返回类型声明为 JQuickCurlResponseBody,即可读取响应头、媒体类型、内容长度与原始字节。

package com.example.programmatic;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
import java.util.List;
public class MetadataDemo {
public interface MetadataApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything")
JQuickCurlResponseBody fetch(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
MetadataApi api = JCurlInvoker.createProxy(MetadataApi.class);
JQuickCurlResponseBody response = api.fetch(new JQuickCurlReq());
String contentType = response.header("Content-Type"); // 单个响应头
List<String> varyAll = response.headers("Vary"); // 同名响应头全部值
long length = response.contentLength(); // 内容长度
String body = response.asString(); // 响应体字符串
byte[] bytes = response.getCachedBytes(); // 响应体字节
System.out.println(contentType);
System.out.println(varyAll);
System.out.println(length);
System.out.println(body);
System.out.println(bytes.length);
}
}

JQuickCurlResponseBody 主要方法说明:

| 方法 | 作用 | 返回类型 | | — | — | — | — | | header(String name) | 按名称获取第一个响应头 | String | | headers(String name) | 按名称获取全部响应头 | List<String> | | hasHeader(String name) | 是否存在指定响应头 | boolean | | getHeaders() | 获取全部响应头(OkHttp Headers) | Headers | | asString() / asString(Charset) | 以 UTF-8 或指定字符集读取响应体 | String | | getCachedBytes() | 获取响应体字节数组(副本) | byte[] | | asInputStream() | 获取响应体字节输入流 | InputStream | | asReader() / asBufferedReader() | 获取字符读取器 | Reader / BufferedReader | | contentType() | 获取响应媒体类型 | MediaType | | contentLength() | 获取内容长度 | long |

注意:2.6.0 的 JQuickCurlResponseBody 不提供 HTTP 状态码访问器(构造时未保存状态码字段),状态码仅会输出在执行日志中。需要基于状态码做分支处理时,请使用网络拦截器捕获,参见“异常体系与错误处理”章节。

JCurlInvoker.invoke 支持以方法引用定位 curl 模板,并显式传入 JQuickCurlReq、JContext、JQuickCurlConfig;通过 Type 参数还可以保留泛型返回类型。

package com.example.programmatic;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import com.github.paohaijiao.param.JContext;
import com.google.gson.reflect.TypeToken;
import java.util.List;
public class MethodRefDemo {
public interface RefApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything/users")
String listUsers(JQuickCurlReq request);
}
// 提供与接口同名的方法引用承载类(通常为同名实现类)
static class RefApiImpl {
public String listUsers(JQuickCurlReq request) {
return null;
}
}
public static void main(String[] args) throws Exception {
JQuickCurlReq req = new JQuickCurlReq();
JContext context = new JContext();
JQuickCurlConfig config = JQuickCurlConfig.getInstance();
// 简单返回类型
String result = JCurlInvoker.invoke(RefApiImpl::listUsers, req, String.class);
System.out.println(result);
// 泛型返回类型:通过 TypeToken 保留元素类型
TypeToken<List<String>> typeToken = new TypeToken<List<String>>() {};
List<String> list = JCurlInvoker.invoke(
RefApiImpl::listUsers, req, context, config, typeToken.getType());
System.out.println(list);
}
}

JQuickCurlBatchRunner 会扫描给定对象中所有标注了 @JCurlCommand 的 public 方法并顺序执行。

package com.example.programmatic;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
import com.github.paohaijiao.support.JQuickCurlBatchRunner;
import java.util.List;
public class BatchDemo {
public static class BatchCommands {
@JCurlCommand("curl -X GET https://httpbin.org/uuid")
public String first() {
return null;
}
@JCurlCommand("curl -X GET https://httpbin.org/anything")
public String second() {
return null;
}
}
public static void main(String[] args) throws Exception {
JQuickCurlBatchRunner runner = new JQuickCurlBatchRunner();
List<JQuickCurlResponseBody> results =
runner.runCurlCommands(new BatchCommands(), JQuickCurlResponseBody.class);
results.forEach(r -> System.out.println(r.asString()));
}
}

JQuick-Curl 2.6.0 存在两条变量替换路径,适用位置不同:

  1. 方法参数名替换(全位置支持,推荐):在接口方法上声明普通参数,命令中的 ${参数名} 在解析前以纯文本方式替换,可放在 URL 路径、Query、请求头、请求体的任意位置。该机制依赖编译器保留参数名:继承 spring-boot-starter-parent 的项目默认开启 -parameters;普通项目需要手动为 maven-compiler-plugin 添加 -parameters 参数。
  2. JQuickCurlReq 上下文变量:通过 JQuickCurlReq.put(key, value) 传值,由文法中的变量节点解析。2.6.0 中该方式仅在整个 URL(curl -X GET ${url})与 -u 凭据位置(-u ${user}:${password})生效;在引号包裹的字符串内部(含 JSON body、header 值)不会被解析。

凭据、动态路径等场景优先使用方式一;需要灵活键值集合时使用方式二并避开引号内嵌位置。

通过方法参数声明路径变量,${id} 将被实际值替换。

package com.example.placeholder;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class PathPlaceholderDemo {
public interface PathApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything/users/${id}")
String getById(JQuickCurlReq request, String id);
}
public static void main(String[] args) throws Exception {
PathApi api = JCurlInvoker.createProxy(PathApi.class);
System.out.println(api.getById(new JQuickCurlReq(), "1001"));
}
}

Query 参数名与参数值均支持占位符,替换以纯文本方式进行。

package com.example.placeholder;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class QueryPlaceholderDemo {
public interface QueryApi {
@JCurlCommand("curl -X GET 'https://httpbin.org/anything?keyword=${keyword}&page=${page}'")
String search(JQuickCurlReq request, String keyword, int page);
}
public static void main(String[] args) throws Exception {
QueryApi api = JCurlInvoker.createProxy(QueryApi.class);
System.out.println(api.search(new JQuickCurlReq(), "jquick", 2));
}
}

注意:通过方法参数替换的值不会自动进行 URL 编码,包含空格或特殊字符时请在 Java 侧先使用 URLEncoder.encode 处理。

请求头值中的占位符在调用时替换;动态 Token 场景推荐使用该方式或全局拦截器。

package com.example.placeholder;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class HeaderPlaceholderDemo {
public interface HeaderApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything -H 'X-Trace: ${traceId}'")
String withTrace(JQuickCurlReq request, String traceId);
}
public static void main(String[] args) throws Exception {
HeaderApi api = JCurlInvoker.createProxy(HeaderApi.class);
System.out.println(api.withTrace(new JQuickCurlReq(), "trace-001"));
}
}

JSON 体内的占位符同样在解析前替换。字符串类型变量需要保留命令中 JSON 值两侧的引号,变量值本身不要再加引号。

package com.example.placeholder;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class BodyPlaceholderDemo {
public interface BodyApi {
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"${name}\",\"age\":${age}}'")
String create(JQuickCurlReq request, String name, int age);
}
public static void main(String[] args) throws Exception {
BodyApi api = JCurlInvoker.createProxy(BodyApi.class);
System.out.println(api.create(new JQuickCurlReq(), "张三", 28));
}
}

注意:替换为纯文本拼接,变量值中的双引号、反斜杠不会自动转义,包含此类字符时建议在 Java 侧序列化为完整 JSON 后使用编程式调用。

JQuickCurlReq 继承自 HashMap<String, Object>,用于承载键值型参数:一方面作为接口方法的固定入参(代理实现要求方法至少声明一个参数),另一方面其中的条目会被写入 JContext,供整个 URL 与 -u 凭据位置的变量解析使用。

用法 说明
new JQuickCurlReq() 构造请求参数容器
put(String key, Object value) 写入变量(继承自 HashMap),值为 null 时变量解析将断言失败
get(String key) 读取变量
package com.example.placeholder;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class ReqDemo {
interface ReqApi {
// 整个 URL 来自上下文变量
@JCurlCommand("curl -X GET ${url} -u ${user}:${password}")
String call(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
ReqApi api = JCurlInvoker.createProxy(ReqApi.class);
JQuickCurlReq req = new JQuickCurlReq();
req.put("url", "https://httpbin.org/anything");
req.put("user", "ada");
req.put("password", System.getenv("API_PASSWORD"));
System.out.println(api.call(req));
}
}

直接在 URL 中携带 Query 参数,或使用方法参数动态拼接。

package com.example.requests;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class GetDemo {
interface GetApi {
@JCurlCommand("curl -X GET 'https://httpbin.org/anything?category=book&page=1'")
String fixedQuery(JQuickCurlReq request);
@JCurlCommand("curl -X GET 'https://httpbin.org/anything?category=${category}&page=${page}'")
String dynamicQuery(JQuickCurlReq request, String category, int page);
}
public static void main(String[] args) throws Exception {
GetApi api = JCurlInvoker.createProxy(GetApi.class);
System.out.println(api.fixedQuery(new JQuickCurlReq()));
System.out.println(api.dynamicQuery(new JQuickCurlReq(), "book", 1));
}
}

通过 -H 'Content-Type: application/json' 与 -d 发送 JSON 请求体,并可直接将返回值映射为自定义 POJO。

package com.example.requests;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class PostJsonDemo {
public static class Product {
private String sku;
private Integer count;
public Product() {
}
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}
interface ProductApi {
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: application/json' " +
"-d '{\"sku\":\"A-1001\",\"count\":2}'")
Product create(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
ProductApi api = JCurlInvoker.createProxy(ProductApi.class);
Product product = api.create(new JQuickCurlReq());
System.out.println(product);
}
}

POST Form表单 application/x-www-form-urlencoded

Section titled “POST Form表单 application/x-www-form-urlencoded”

通过显式指定 Content-Type 请求头并以 -d 提供 key=value&key2=value2 形式的表单内容。

package com.example.requests;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class FormDemo {
interface FormApi {
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: application/x-www-form-urlencoded' " +
"-d 'username=alice&age=20'")
String submit(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
FormApi api = JCurlInvoker.createProxy(FormApi.class);
System.out.println(api.submit(new JQuickCurlReq()));
}
}

使用 -F 发送 Multipart 表单,可同时包含普通表单字段与文件字段。

package com.example.requests;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class MultipartDemo {
interface MultipartApi {
// 普通字段与文件混合提交
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-F 'userId=1001' " +
"-F 'file=@C:/data/report.txt'")
String upload(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
MultipartApi api = JCurlInvoker.createProxy(MultipartApi.class);
System.out.println(api.upload(new JQuickCurlReq()));
}
}

-F '字段名=@本地文件路径' 表示文件字段,框架会读取本地文件并以 Multipart 方式上传。路径建议使用正斜杠;多个文件可重复使用 -F。

package com.example.file;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class FileUploadDemo {
interface FileApi {
// 单文件上传
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-F 'file=@C:/data/report.pdf'")
String upload(JQuickCurlReq request);
// 多文件上传(同字段名)
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-F 'files=@C:/data/a.txt' " +
"-F 'files=@C:/data/b.txt'")
String uploadMultiple(JQuickCurlReq request);
// 文件路径动态传入
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-F 'file=@${filePath}'")
String uploadDynamic(JQuickCurlReq request, String filePath);
}
public static void main(String[] args) throws Exception {
FileApi api = JCurlInvoker.createProxy(FileApi.class);
System.out.println(api.upload(new JQuickCurlReq()));
System.out.println(api.uploadDynamic(new JQuickCurlReq(), "C:/data/report.pdf"));
}
}

使用 -o 或 --output 指定本地输出路径,框架在执行器内部直接将响应字节写入文件;方法返回类型声明为 byte[] 时仍会同时返回字节内容。

package com.example.file;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class FileDownloadDemo {
interface DownloadApi {
// 下载并写入本地文件
@JCurlCommand("curl -X GET https://httpbin.org/anything/report.pdf " +
"--output 'C:/download/report.pdf'")
byte[] download(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
DownloadApi api = JCurlInvoker.createProxy(DownloadApi.class);
byte[] bytes = api.download(new JQuickCurlReq());
System.out.println("downloaded bytes: " + bytes.length);
}
}

不使用 --output 时,将返回类型声明为 byte[],响应内容将完整保存在内存中,由调用方自行决定后续处理。

package com.example.file;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class InMemoryDownloadDemo {
interface InMemoryApi {
@JCurlCommand("curl -X GET https://httpbin.org/image/png")
byte[] fetchImage(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
InMemoryApi api = JCurlInvoker.createProxy(InMemoryApi.class);
byte[] image = api.fetchImage(new JQuickCurlReq());
Path target = Paths.get("C:/download/image.png");
Files.createDirectories(target.getParent());
Files.write(target, image);
}
}

注意:响应字节会被全量缓冲在内存中,下载超大文件时请评估堆内存压力。

-u '用户名:密码' 会自动生成 Authorization: Basic ... 请求头;凭据建议通过变量传入,不要硬编码。

package com.example.auth;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class BasicAuthDemo {
interface BasicApi {
@JCurlCommand("curl -X GET ${url} -u ${username}:${password}")
String call(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
BasicApi api = JCurlInvoker.createProxy(BasicApi.class);
JQuickCurlReq req = new JQuickCurlReq();
req.put("url", "https://httpbin.org/anything");
req.put("username", "ada");
req.put("password", "secret");
System.out.println(api.call(req));
}
}

通过 -H 'Authorization: Bearer ${token}' 携带 Bearer Token;多接口共用时更推荐使用全局拦截器统一注入。

package com.example.auth;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class BearerTokenDemo {
interface BearerApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything " +
"-H 'Authorization: Bearer ${token}'")
String call(JQuickCurlReq request, String token);
}
public static void main(String[] args) throws Exception {
BearerApi api = JCurlInvoker.createProxy(BearerApi.class);
System.out.println(api.call(new JQuickCurlReq(), "eyJhbGciOiJIUzI1NiJ9.token.signature"));
}
}

对接使用非标准鉴权方案的开放平台时,按其文档要求添加自定义请求头即可。

package com.example.auth;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class CustomAuthDemo {
interface CustomAuthApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything " +
"-H 'X-Api-Key: ${apiKey}' " +
"-H 'X-Timestamp: ${timestamp}' " +
"-H 'X-Sign: ${sign}'")
String signedCall(JQuickCurlReq request, String apiKey, String timestamp, String sign);
}
public static void main(String[] args) throws Exception {
CustomAuthApi api = JCurlInvoker.createProxy(CustomAuthApi.class);
System.out.println(api.signedCall(new JQuickCurlReq(), "ak-001", "1758900000", "a1b2c3"));
}
}

-b(--cookie)用于发送 Cookie,支持 name=value; name2=value2 形式与 -b @文件 加载 Netscape 格式 Cookie 文件;-c(--cookie-jar)将响应中的 Set-Cookie 持久化到文件,供后续请求复用。

package com.example.auth;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
public class CookieDemo {
interface SessionApi {
// 第一步:登录并将 Set-Cookie 写入 cookie 文件
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: application/json' " +
"-d '{\"user\":\"ada\"}' " +
"-c C:/data/cookies.txt")
JQuickCurlResponseBody login(JQuickCurlReq request);
// 第二步:携带持久化 Cookie 访问
@JCurlCommand("curl -X GET https://httpbin.org/anything -b @C:/data/cookies.txt")
String profile(JQuickCurlReq request);
// 直接发送手工 Cookie
@JCurlCommand("curl -X GET https://httpbin.org/anything -b 'sessionId=abc123; theme=dark'")
String manualCookie(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
SessionApi api = JCurlInvoker.createProxy(SessionApi.class);
api.login(new JQuickCurlReq());
System.out.println(api.profile(new JQuickCurlReq()));
System.out.println(api.manualCookie(new JQuickCurlReq()));
}
}

Cookie 文件等同于登录凭据,应加入 .gitignore,禁止提交到代码仓库。

忽略SSL证书校验(开发调试用,禁止生产)

Section titled “忽略SSL证书校验(开发调试用,禁止生产)”

在 curl 命令中加入 -k 或 --insecure,框架将使用信任全部证书的 X509TrustManager 与目标站点建立连接。该方式会完全跳过证书链校验,存在中间人攻击风险,仅限本地开发调试,严禁用于生产环境。

package com.example.ssl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class InsecureSslDemo {
interface InsecureApi {
@JCurlCommand("curl -X GET https://self-signed.example.com/ -k")
String call(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
InsecureApi api = JCurlInvoker.createProxy(InsecureApi.class);
System.out.println(api.call(new JQuickCurlReq()));
}
}

2.6.0 未提供在应用内加载自定义 KeyStore / TrustStore 的编程 API。生产环境中需要使用私有 CA 或双向证书时,推荐通过 JVM 标准系统属性指定信任库与密钥库。

Terminal window
java \
-Djavax.net.ssl.trustStore=/etc/ssl/custom-truststore.jks \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.keyStore=/etc/ssl/client-keystore.jks \
-Djavax.net.ssl.keyStorePassword=changeit \
-jar application.jar

参数说明:

JVM 参数 作用 必填
javax.net.ssl.trustStore 自定义信任库路径(JKS/PKCS12) 使用私有 CA 时必填
javax.net.ssl.trustStorePassword 信任库口令 按信任库配置
javax.net.ssl.keyStore 客户端证书密钥库路径(双向 TLS) 服务端要求客户端证书时必填
javax.net.ssl.keyStorePassword 密钥库口令 按密钥库配置

如需完全自定义 SSL 上下文,可继承 JQuickCurlCoreVisitor 重写客户端构建逻辑,参见“扩展:自定义解析器”章节。

通过 -x 或 --proxy 指定代理,写法为 主机:端口(不要添加 http:// 协议前缀,否则地址无法正确解析)。

package com.example.proxy;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class ProxyDemo {
interface ProxyApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything -x proxy.example.com:8080")
String call(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
ProxyApi api = JCurlInvoker.createProxy(ProxyApi.class);
System.out.println(api.call(new JQuickCurlReq()));
}
}

2.6.0 的代理模型仅包含主机、端口与类型,不支持 --proxy-user 之类的代理认证选项。需要代理认证时,可通过 Proxy-Authorization 请求头手工携带 Basic 凭据。

package com.example.proxy;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import okhttp3.Credentials;
public class ProxyAuthDemo {
interface ProxyAuthApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything " +
"-x proxy.example.com:8080 " +
"-H 'Proxy-Authorization: ${proxyAuthorization}'")
String call(JQuickCurlReq request, String proxyAuthorization);
}
public static void main(String[] args) throws Exception {
ProxyAuthApi api = JCurlInvoker.createProxy(ProxyAuthApi.class);
// 生成 Basic 代理凭据
String authorization = Credentials.basic("proxyUser", "proxyPassword");
System.out.println(api.call(new JQuickCurlReq(), authorization));
}
}

通过 JQuickCurlConfig 全局配置连接超时、读取超时与写入超时。链式方法以 (long timeout, TimeUnit unit) 形式传参。

package com.example.timeout;
import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class TimeoutDemo {
public static void init() {
JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS) // 建立连接超时
.readTimeout(10, TimeUnit.SECONDS) // 读取响应超时
.writeTimeout(10, TimeUnit.SECONDS); // 写入请求超时
}
public static void main(String[] args) {
init();
}
}

超时配置项说明:

配置方法 作用 参数类型 2.6.0 默认值
connectTimeout(long, TimeUnit) TCP 连接建立超时 long / TimeUnit 1000000000 ms(约 11.6 天,等同不限)
readTimeout(long, TimeUnit) 两次数据包之间的读取超时 long / TimeUnit 1000000000 ms
writeTimeout(long, TimeUnit) 请求写入超时 long / TimeUnit 1000000000 ms
setCallTimeout(long) 整个请求的总耗时上限(Setter) long 1000000000 ms

注意:所有超时共用配置对象中的同一个 TimeUnit,建议统一使用相同单位设置,避免后一次设置改变前一次的单位解释。

在命令中加入 -L 或 --location 即可跟随 3xx 重定向;框架默认已开启重定向跟随。--max-redirs 在 2.6.0 中可被文法识别,但次数值不会实际生效。

package com.example.timeout;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class RedirectDemo {
interface RedirectApi {
@JCurlCommand("curl -X GET https://httpbin.org/redirect/2 -L")
String follow(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
RedirectApi api = JCurlInvoker.createProxy(RedirectApi.class);
System.out.println(api.follow(new JQuickCurlReq()));
}
}

注解式代理在方法抛出异常时会自动重试,最大尝试次数为 maxRetryCount + 1(默认 maxRetryCount = 3,即最多执行 4 次)。重试不区分 HTTP 方法,POST 等非幂等请求失败时同样会被重试,业务侧需自行评估幂等性。

package com.example.timeout;
import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class RetryDemo {
public static void init() {
JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) // 传输层连接失败自动重试
.maxRetryCount(2); // 代理层异常重试次数(总尝试 3 次)
}
public static void main(String[] args) {
init();
}
}

重试相关配置项说明:

配置方法 作用 类型 默认值
retryOnConnectionFailure(boolean) 是否允许 OkHttp 传输层在连接失败时静默重试 boolean true
maxRetryCount(int) 注解代理层的异常重试次数,实际尝试次数为该值 + 1 int 3

注意:2.6.0 中网络 IO 异常在 Visitor 内部被捕获,最终以 NullPointerException 的形式触发代理层重试;重试耗尽后抛出的也是该异常,排查时请结合日志中的底层堆栈。

在方法上额外标注 @JTimeout 可覆盖全局超时。三个属性的单位均为毫秒,未显式设置的属性值为 -1 并同样会覆盖全局配置,因此建议三个属性同时显式指定。

package com.example.timeout;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.anno.JTimeout;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class MethodTimeoutDemo {
interface TimeoutApi {
@JCurlCommand("curl -X GET https://httpbin.org/anything")
@JTimeout(connect = 2000, read = 5000, write = 5000)
String call(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
TimeoutApi api = JCurlInvoker.createProxy(TimeoutApi.class);
System.out.println(api.call(new JQuickCurlReq()));
}
}

@JTimeout 参数说明:

属性 作用 类型 默认值
connect 连接超时(毫秒) long -1
read 读取超时(毫秒) long -1
write 写入超时(毫秒) long -1

注意:2.6.0 中 @JTimeout 直接修改全局单例配置,超时值会在调用后持续保留,对其他方法产生影响。

SpringBoot配置文件(application.yml)

Section titled “SpringBoot配置文件(application.yml)”

JQuick-Curl 不提供官方 @ConfigurationProperties 自动绑定,但可以在 application.yml 中自定义配置前缀,并通过一个初始化 Bean 将值写入 JQuickCurlConfig 单例。

application.yml 示例。

jquick:
curl:
connect-timeout: 3000
read-timeout: 10000
write-timeout: 10000
max-retry-count: 2
follow-redirects: true
pool:
max-idle: 50
keep-alive: 300000

配置属性绑定与初始化类。

package com.example.demo;
import com.github.paohaijiao.config.JQuickCurlConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@ConfigurationProperties(prefix = "jquick.curl")
public class JQuickCurlProperties {
private long connectTimeout = 3000;
private long readTimeout = 10000;
private long writeTimeout = 10000;
private int maxRetryCount = 2;
private boolean followRedirects = true;
private Pool pool = new Pool();
@Bean
public JQuickCurlConfig jQuickCurlConfig() {
JQuickCurlConfig config = JQuickCurlConfig.getInstance();
config.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.maxRetryCount(maxRetryCount)
.followRedirects(followRedirects)
.connectionPool(pool.getMaxIdle(), pool.getKeepAlive(), TimeUnit.MILLISECONDS);
return config;
}
public long getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(long connectTimeout) {
this.connectTimeout = connectTimeout;
}
public long getReadTimeout() {
return readTimeout;
}
public void setReadTimeout(long readTimeout) {
this.readTimeout = readTimeout;
}
public long getWriteTimeout() {
return writeTimeout;
}
public void setWriteTimeout(long writeTimeout) {
this.writeTimeout = writeTimeout;
}
public int getMaxRetryCount() {
return maxRetryCount;
}
public void setMaxRetryCount(int maxRetryCount) {
this.maxRetryCount = maxRetryCount;
}
public boolean isFollowRedirects() {
return followRedirects;
}
public void setFollowRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
}
public Pool getPool() {
return pool;
}
public void setPool(Pool pool) {
this.pool = pool;
}
public static class Pool {
private int maxIdle = 50;
private long keepAlive = 300000;
public int getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public long getKeepAlive() {
return keepAlive;
}
public void setKeepAlive(long keepAlive) {
this.keepAlive = keepAlive;
}
}
}

框架另外内置了属性文件加载能力,支持的键为 quick.curl.connect.timeout、quick.curl.read.timeout、quick.curl.write.timeout、quick.curl.pool.max.idle、quick.curl.pool.keep.alive、quick.curl.max.retry.count,数值均按毫秒解释。

// 在类路径放置 jquick-curl.properties 后加载
JQuickCurlConfig.getInstance().loadFromClasspathResource("jquick-curl.properties");

不使用任何配置文件时,直接通过链式 API 手动配置连接池与传输参数;建议在应用启动阶段执行一次。

package com.example.demo;
import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class PoolCodeDemo {
public static void init() {
JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
// 最大空闲连接数 50,空闲连接保活 5 分钟
.connectionPool(50, 5, TimeUnit.MINUTES)
.retryOnConnectionFailure(true)
.maxRetryCount(2)
.followRedirects(true)
.followSslRedirects(true);
}
public static void main(String[] args) {
init();
}
}

连接池配置项说明:

配置方法 作用 参数类型 2.6.0 默认值
connectionPool(int, long, TimeUnit) 配置连接池最大空闲连接数与空闲保活时长 int / long / TimeUnit 1000000000 / 1000000000 ms
followRedirects(boolean) 是否跟随普通重定向 boolean true
followSslRedirects(boolean) 是否在 HTTP/HTTPS 之间跟随重定向 boolean true

注解代理内部还维护了一个命令处理器对象池(基于 commons-pool2,最大总数 8、最大空闲 4、最小空闲 1),该参数在 2.6.0 中固定,不对外暴露配置。

拦截器直接复用 OkHttp 的 okhttp3.Interceptor 接口,可在请求发出前修改请求,或在响应返回后记录响应信息。

package com.example.interceptor;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
// 请求前置:追加自定义请求头
public class HeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request newRequest = original.newBuilder()
.addHeader("X-Client", "jquick-curl")
.build();
return chain.proceed(newRequest);
}
}

同时处理请求前置与响应后置的示例。

package com.example.interceptor;
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
public class TimingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
long start = System.nanoTime();
// 请求前置处理
Response response = chain.proceed(chain.request());
// 响应后置处理
long costMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("request " + response.request().url() + " cost " + costMs + " ms");
return response;
}
}

通过 addInterceptor 注册应用拦截器,通过 addNetworkInterceptor 注册网络拦截器,对所有请求生效。

package com.example.interceptor;
import com.github.paohaijiao.config.JQuickCurlConfig;
import okhttp3.Interceptor;
public class GlobalInterceptorDemo {
public static void init() {
// 全局追加 Bearer Token
Interceptor authInterceptor = chain -> chain.proceed(
chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + System.getenv("API_TOKEN"))
.build());
JQuickCurlConfig.getInstance()
.addInterceptor(authInterceptor)
.addInterceptor(new HeaderInterceptor());
}
public static void main(String[] args) {
init();
}
}

拦截器注册方法说明:

方法 作用 参数类型 默认值
addInterceptor(Interceptor) 注册应用拦截器,只调用一次,可查看最终请求与最终响应 OkHttp Interceptor 默认含内置 JLoggingInterceptor(输出请求耗时)
addNetworkInterceptor(Interceptor) 注册网络拦截器,逐跳调用,可查看重定向与实际网络响应 OkHttp Interceptor 空

2.6.0 的拦截器为全局配置,不支持在 @JCurlCommand 中为单个方法单独声明拦截器。需要按请求区分行为时,推荐使用“标记头”方案:命令中携带仅作标记的请求头,由全局拦截器读取该标记决定是否执行增强逻辑。

package com.example.interceptor;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class PerRequestInterceptorDemo {
// 拦截器读取 X-Add-Client 标记头,为真时追加业务头,并移除标记
static class ConditionalHeaderInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if ("true".equalsIgnoreCase(request.header("X-Add-Client"))) {
request = request.newBuilder()
.removeHeader("X-Add-Client")
.addHeader("X-Client", "jquick-curl")
.build();
} else {
request = request.newBuilder()
.removeHeader("X-Add-Client")
.build();
}
return chain.proceed(request);
}
}
interface MarkApi {
// 仅该请求通过标记头触发拦截器增强
@JCurlCommand("curl -X GET https://httpbin.org/anything -H 'X-Add-Client: true'")
String enhanced(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
JQuickCurlConfig.getInstance().addInterceptor(new ConditionalHeaderInterceptor());
MarkApi api = JCurlInvoker.createProxy(MarkApi.class);
System.out.println(api.enhanced(new JQuickCurlReq()));
}
}

2.6.0 中可能出现的异常与触发条件如下:

异常类型 触发条件 来源模块
IllegalArgumentException 方法未标注 @JCurlCommand;HTTP 方法名非法;代理地址解析失败 jquick-curl
UnsupportedOperationException 调用 execute = false 的方法;代理调用未标注解的方法 jquick-curl
JAntlrExecutionException curl 命令存在语法错误,可通过 getErrors() 获取行号、列号、规则栈等明细 javelin-core
NullPointerException 语法错误或网络 IO 失败导致原始响应为 null,响应转换阶段触发 jquick-curl
RuntimeException SSL 上下文初始化失败时包装底层异常抛出 jquick-curl

需要特别注意的默认行为:

  • HTTP 状态码为 4xx / 5xx 时不会抛出异常,响应体会被正常返回。
  • 网络 IO 异常(连接拒绝、超时、重置)在 Visitor 内部被捕获并打印堆栈,原始结果为 null,随后在响应转换阶段以 NullPointerException 的形式触发重试或抛出。
  • curl 语法错误在处理器层被捕获并打印错误明细,同样以 null 结果进入后续流程。

调用方可以在业务层统一捕获代理调用异常;由于失败时框架不再返回响应对象,如需保留非 2xx 响应或网络层信息,可通过网络拦截器将最近一次响应状态记录到上下文容器中。

package com.example.exception;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
public class ExceptionHandleDemo {
// 记录最近一次响应状态码的持有器
static class LastResponseHolder {
private static final ThreadLocal<Integer> LAST_CODE = new ThreadLocal<>();
static void set(int code) {
LAST_CODE.set(code);
}
static Integer get() {
return LAST_CODE.get();
}
static void clear() {
LAST_CODE.remove();
}
}
// 网络拦截器:无论状态码如何都会执行
static class StatusRecordingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
LastResponseHolder.set(response.code());
return response;
}
}
interface StatusApi {
@JCurlCommand("curl -X GET https://httpbin.org/status/500")
String serverError(JQuickCurlReq request);
}
public static void main(String[] args) {
JQuickCurlConfig.getInstance()
.maxRetryCount(0)
.addNetworkInterceptor(new StatusRecordingInterceptor());
StatusApi api = JCurlInvoker.createProxy(StatusApi.class);
try {
String body = api.serverError(new JQuickCurlReq());
System.out.println("body = " + body + ", http status = " + LastResponseHolder.get());
} catch (Exception e) {
System.err.println("call failed: " + e.getClass().getSimpleName() + ": " + e.getMessage());
} finally {
LastResponseHolder.clear();
}
}
}

JQuickParseHandler 是 XML 工厂层的 SPI 接口,负责创建 XML 元素描述对象与调用处理器。通过自定义实现,可以替换 XML 标签命名规则或接管调用处理逻辑。

package com.example.extension;
import com.github.paohaijiao.xml.JQuickCurlXmlParseFactory;
import com.github.paohaijiao.xml.element.JQuickXmlElement;
import com.github.paohaijiao.xml.handler.JQuickParseHandler;
import com.github.paohaijiao.xml.invocation.JQuickXmlInvocationHandler;
// 自定义解析处理器:在默认实现外包一层,扩展元素标签命名
public class CustomParseHandler implements JQuickParseHandler {
private final JQuickCurlXmlParseFactory delegate = new JQuickCurlXmlParseFactory();
@Override
public JQuickXmlElement createJQuickXmlElement() {
JQuickXmlElement element = delegate.createJQuickXmlElement();
// 可在此包装/替换元素描述,自定义标签名、属性名
return element;
}
@Override
public JQuickXmlInvocationHandler createlInvocationHandler() {
return delegate.createlInvocationHandler();
}
}

自定义处理器可直接传入 JQuickXmlFactory 使用。

package com.example.extension;
import com.github.paohaijiao.xml.factory.JQuickFactory;
import com.github.paohaijiao.xml.factory.JQuickXmlFactory;
public class CustomHandlerDemo {
public static void main(String[] args) {
JQuickParseHandler handler = new CustomParseHandler();
JQuickFactory factory = new JQuickXmlFactory(handler, "apis.xml");
System.out.println(factory);
}
}

curl 命令的实际语义由 Visitor 决定。继承 JQuickCurlCommonVisitor(或 JQuickCurlBaseVisitor)重写对应 visit 方法,即可在不改动框架代码的前提下扩展或修改命令解析行为,随后自行组装 Lexer、Parser 完成调用。

package com.example.extension;
import com.github.paohaijiao.antlr.JCurlErrorListener;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.parser.JQuickCurlLexer;
import com.github.paohaijiao.parser.JQuickCurlParser;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
import com.github.paohaijiao.visitor.JQuickCurlCommonVisitor;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
public class CustomVisitorDemo {
// 自定义 Visitor:扩展 -d 处理,例如统一去除首尾空白
static class TrimDataVisitor extends JQuickCurlCommonVisitor {
TrimDataVisitor(JContext context, JQuickCurlConfig config) {
super(context, config);
}
@Override
public Object visitDataOption(JQuickCurlParser.DataOptionContext ctx) {
Object result = super.visitDataOption(ctx);
if (this.data != null) {
this.data = this.data.trim();
}
return result;
}
}
public static void main(String[] args) {
String curlCommand = "curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: application/json' " +
"-d ' {\"k\":\"v\"} '";
JContext context = new JContext();
JQuickCurlConfig config = JQuickCurlConfig.getInstance();
JQuickCurlLexer lexer = (JQuickCurlLexer) new JQuickCurlLexer(
CharStreams.fromString(curlCommand));
lexer.addErrorListener(new JCurlErrorListener());
JQuickCurlParser parser = new JQuickCurlParser(new CommonTokenStream(lexer));
JQuickCurlParser.CurlCommandContext tree = parser.curlCommand();
TrimDataVisitor visitor = new TrimDataVisitor(context, config);
JQuickCurlResponseBody responseBody = visitor.visitCurlCommand(tree);
System.out.println(responseBody.asString());
}
}

如需扩展全新的 curl 选项,需要同步修改文法文件(JQuickCurl.g4)并重新生成 Lexer / Parser,再在自定义 Visitor 中实现对应选项的处理逻辑。

下表列出 2.6.0 文法覆盖的全部 curl 参数及实际支持状态。

curl 参数 等价长参数 支持状态 说明
-X <METHOD> --request <METHOD> 支持 指定 HTTP 方法;2.6.0 中为必填项
-H 'K: V' --header 支持 追加请求头,Content-Type 会同步更新内部媒体类型
-d '...' --data、--data-ascii 支持 发送请求体文本
— --data-binary、--data-raw 语法接受 按普通文本处理,不读取 @文件 二进制内容
— --data-urlencode 存在缺陷 可被解析并切换 Content-Type,但表单内容不会写入请求体(实际发送 {})
-F 'k=@f' --form 支持 Multipart 表单,支持文件字段与普通字段混用
-u 'u:p' --user 支持 生成 Basic Authorization 头,支持变量
-b 'a=b' --cookie 支持 发送 Cookie,多个以分号分隔
-b @file — 支持 加载 Netscape 格式 Cookie 文件
-c <file> --cookie-jar 支持 将 Set-Cookie 持久化为 Netscape 格式文件
-L --location 支持 跟随重定向(默认已开启)
— --max-redirs <N> 语法接受 可识别,但次数值在 2.6.0 不生效
-x host:port --proxy 支持 HTTP 代理,地址不可带协议前缀
— --socks5-hostname 存在缺陷 可被解析,但内部仍按 HTTP 代理类型处理
-k --insecure 支持 跳过 SSL 证书校验
-o <file> --output 支持 将响应字节写入本地文件
— --http2 语法接受 可识别,但不影响实际客户端(OkHttp 默认协商 HTTP/2)
-v --verbose 语法接受 不产生原生 curl 风格的详细报文输出
-s --silent 语法接受 空操作

以下常用 curl 选项在 2.6.0 中不支持:--max-time、--connect-timeout、--retry、--retry-delay、-G、-T、-C、-r、--limit-rate、-A、-e、--compressed、--cacert、-E、--proxy-user、-n、-0、-K、-z、-w、-I(-I 会被忽略,请使用 -X HEAD)。

本章为 JQuick-Curl Syntax Demos。每一节只演示一项独立语法,围绕链式注解、命令参数与请求构建展开,便于开发者按语法点快速查阅写法。所有示例均基于 @JCurlCommand 注解式声明,语法核心是:注解值即一条完整 curl 命令,方法即一个可调用 API。

1. 基础请求语法(Basic Request Syntax)

Section titled “1. 基础请求语法(Basic Request Syntax)”

@JCurlCommand 的注解值是一条完整 curl 命令,必须以 curl 开头,并用 -X 显式声明请求方法。JCurlInvoker.createProxy 把接口编译为动态代理客户端;方法返回类型决定响应解析方式,String 表示直接返回响应体文本。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface BasicApi {
// 语法:curl -X <METHOD> <URL>
// -X 声明方法;返回值类型 = 响应体解析目标
@JCurlCommand("curl -X GET https://httpbin.org/get")
String get(JQuickCurlReq request);
}
class BasicDemo {
public static void main(String[] args) throws Exception {
// 语法:接口 -> 代理客户端
BasicApi api = JCurlInvoker.createProxy(BasicApi.class);
// 每个方法至少声明一个 JQuickCurlReq 入参
System.out.println(api.get(new JQuickCurlReq()));
}
}

使用 -H 'Name: value' 添加请求头,--header 为等价写法;一个命令可重复出现多个 -H,同名请求头会合并而非覆盖。请求头是后续所有请求体语法的内容类型基础。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface HeaderApi {
// 语法:-H 'Name: value',可重复多次
@JCurlCommand("curl -X GET https://httpbin.org/anything " +
"-H 'Accept: application/json' " +
"-H 'X-Trace-Id: trace-001'")
String withHeaders(JQuickCurlReq request);
}
class HeaderDemo {
public static void main(String[] args) throws Exception {
HeaderApi api = JCurlInvoker.createProxy(HeaderApi.class);
System.out.println(api.withHeaders(new JQuickCurlReq()));
}
}

3. URL 查询参数语法(Query Parameter Syntax)

Section titled “3. URL 查询参数语法(Query Parameter Syntax)”

查询参数直接写在 URL 的 ?key=value&key2=value2 中。参数名与值均可使用 ${变量} 占位符,由方法参数在解析前做纯文本替换;替换值不会自动 URL 编码,含空格或特殊字符时需在 Java 侧先行编码。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface QueryApi {
// 语法:URL 内 ?k=v&k2=v2,固定值
@JCurlCommand("curl -X GET 'https://httpbin.org/anything?category=book&page=1'")
String fixed(JQuickCurlReq request);
// 语法:?k=${var},占位符由方法参数替换
@JCurlCommand("curl -X GET 'https://httpbin.org/anything?keyword=${keyword}&page=${page}'")
String dynamic(JQuickCurlReq request, String keyword, int page);
}
class QueryDemo {
public static void main(String[] args) throws Exception {
QueryApi api = JCurlInvoker.createProxy(QueryApi.class);
System.out.println(api.fixed(new JQuickCurlReq()));
System.out.println(api.dynamic(new JQuickCurlReq(), "jquick", 2));
}
}

4. 路径变量语法(Path Variable Syntax)

Section titled “4. 路径变量语法(Path Variable Syntax)”

URL 路径段中使用 ${变量名} 声明路径变量,调用时由对应方法参数替换。该机制依赖编译期保留参数名,普通 Maven 项目需为 maven-compiler-plugin 开启 -parameters。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface PathApi {
// 语法:路径中的 ${id} 由同名方法参数替换
@JCurlCommand("curl -X GET https://httpbin.org/anything/users/${id}")
String getById(JQuickCurlReq request, String id);
}
class PathDemo {
public static void main(String[] args) throws Exception {
PathApi api = JCurlInvoker.createProxy(PathApi.class);
System.out.println(api.getById(new JQuickCurlReq(), "1001"));
}
}

5. Raw 文本请求体语法(Raw Text Body Syntax)

Section titled “5. Raw 文本请求体语法(Raw Text Body Syntax)”

使用 -d / --data / --data-raw 发送原始文本请求体,并用 -H 'Content-Type: text/plain' 声明内容类型。-d 仅负责携带报文,不做任何序列化处理。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface RawApi {
// 语法:-H 'Content-Type: text/plain' + -d '原始文本'
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: text/plain' " +
"-d 'hello jquick-curl'")
String postRaw(JQuickCurlReq request);
}
class RawDemo {
public static void main(String[] args) throws Exception {
RawApi api = JCurlInvoker.createProxy(RawApi.class);
System.out.println(api.postRaw(new JQuickCurlReq()));
}
}

6. JSON 请求体语法(JSON Body Syntax)

Section titled “6. JSON 请求体语法(JSON Body Syntax)”

JSON 请求体语法为 -H 'Content-Type: application/json' 配合 -d '{...}'。JSON 字符串使用单引号包裹,内部键值使用双引号;方法返回类型可直接声明为自定义 POJO,由结果转换器完成反序列化。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class JsonBodyDemo {
public static class Product {
private String sku;
private Integer count;
public String getSku() { return sku; }
public void setSku(String sku) { this.sku = sku; }
public Integer getCount() { return count; }
public void setCount(Integer count) { this.count = count; }
}
public interface JsonApi {
// 语法:-H 'Content-Type: application/json' + -d '{json}'
// 返回值声明为 POJO,自动完成反序列化
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-H 'Content-Type: application/json' " +
"-d '{\"sku\":\"A-1001\",\"count\":2}'")
Product create(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
JsonApi api = JCurlInvoker.createProxy(JsonApi.class);
System.out.println(api.create(new JQuickCurlReq()));
}
}

7. Multipart 表单语法(Multipart Form Syntax)

Section titled “7. Multipart 表单语法(Multipart Form Syntax)”

使用 -F 声明 Multipart 字段:-F '字段名=值' 为普通表单字段,-F '字段名=@文件路径' 为文件字段。两类字段可在同一命令中混用;文件路径建议使用正斜杠,多个文件可重复 -F。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface MultipartApi {
// 语法:-F 'field=value' 普通字段 + -F 'file=@path' 文件字段
@JCurlCommand("curl -X POST https://httpbin.org/anything " +
"-F 'userId=1001' " +
"-F 'file=@C:/data/report.txt'")
String upload(JQuickCurlReq request);
}
class MultipartDemo {
public static void main(String[] args) throws Exception {
MultipartApi api = JCurlInvoker.createProxy(MultipartApi.class);
System.out.println(api.upload(new JQuickCurlReq()));
}
}

8. 超时配置语法(Timeout Config Syntax)

Section titled “8. 超时配置语法(Timeout Config Syntax)”

超时通过全局单例 JQuickCurlConfig 的链式方法配置,签名统一为 (long timeout, TimeUnit unit),可配置连接、读取、写入三个维度。JQuickCurlConfig 为全局单例,建议在应用启动阶段初始化一次;方法级可用 @JTimeout 覆盖,其单位固定为毫秒。

import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class TimeoutSyntaxDemo {
// 语法:JQuickCurlConfig.getInstance().<xxx>Timeout(long, TimeUnit)
public static void init() {
JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS) // 连接建立超时
.readTimeout(10, TimeUnit.SECONDS) // 读取响应超时
.writeTimeout(10, TimeUnit.SECONDS); // 请求写入超时
}
}
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.anno.JTimeout;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface TimeoutApi {
// 语法:@JTimeout 覆盖全局超时;三个属性单位均为毫秒
@JTimeout(connect = 3000, read = 10000, write = 10000)
@JCurlCommand("curl -X GET https://httpbin.org/delay/1")
String call(JQuickCurlReq request);
}

代理通过 -x 或 --proxy 声明,写法为 主机:端口,不要附加 http:// 协议前缀。2.6.0 的代理模型仅包含主机、端口与类型,不支持 --proxy-user 一类认证选项;需要代理认证时,用 -H 'Proxy-Authorization: ...' 手工携带 Basic 凭据。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import okhttp3.Credentials;
public interface ProxyApi {
// 语法:-x host:port(不带协议前缀) + Proxy-Authorization 头
@JCurlCommand("curl -X GET https://httpbin.org/anything " +
"-x proxy.example.com:8080 " +
"-H 'Proxy-Authorization: ${proxyAuthorization}'")
String call(JQuickCurlReq request, String proxyAuthorization);
}
class ProxyDemo {
public static void main(String[] args) throws Exception {
ProxyApi api = JCurlInvoker.createProxy(ProxyApi.class);
String authorization = Credentials.basic("proxyUser", "proxyPassword");
System.out.println(api.call(new JQuickCurlReq(), authorization));
}
}

10. HTTP/2 开启语法(HTTP/2 Enable Syntax)

Section titled “10. HTTP/2 开启语法(HTTP/2 Enable Syntax)”

JQuick-Curl 未提供独立的 HTTP/2 开关语法。传输层基于 OkHttp,在 HTTPS 场景下由 OkHttp 通过 ALPN 自动与支持 HTTP/2 的服务端协商,命中即使用 HTTP/2,无需在 curl 命令中显式开启;curl 原生的 --http2 / --http1.1 选项在 2.6.0 文法中未实现,语法层面只需正常发起 HTTPS 请求。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface Http2Api {
// 语法:HTTPS 请求即可,HTTP/2 由 OkHttp 经 ALPN 自动协商
// 无需 --http2;如需强制协议列表,需扩展 OkHttpClient 构建逻辑
@JCurlCommand("curl -X GET https://httpbin.org/get")
String get(JQuickCurlReq request);
}
class Http2Demo {
public static void main(String[] args) throws Exception {
Http2Api api = JCurlInvoker.createProxy(Http2Api.class);
System.out.println(api.get(new JQuickCurlReq()));
}
}

11. SSL / 双向证书语法(SSL / Mutual TLS Syntax)

Section titled “11. SSL / 双向证书语法(SSL / Mutual TLS Syntax)”

跳过证书校验使用 -k 或 --insecure,框架将采用信任全部证书的 X509TrustManager,仅限本地调试,严禁用于生产。自定义信任库与双向 TLS 客户端证书,2.6.0 不提供应用内编程 API,标准做法是通过 JVM 系统属性指定信任库与密钥库。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface SslApi {
// 语法:-k / --insecure 跳过证书链校验(仅开发调试)
@JCurlCommand("curl -X GET https://self-signed.example.com/ -k")
String call(JQuickCurlReq request);
}
class SslDemo {
public static void main(String[] args) throws Exception {
SslApi api = JCurlInvoker.createProxy(SslApi.class);
System.out.println(api.call(new JQuickCurlReq()));
}
}

双向证书场景通过 JVM 参数指定密钥库与信任库(-Djavax.net.ssl.keyStore、-Djavax.net.ssl.trustStore 及对应口令),无需改动 curl 命令。

12. 响应读取语法(Response Read Syntax)

Section titled “12. 响应读取语法(Response Read Syntax)”

同步响应读取由方法返回类型决定:返回 String 得到响应体文本;返回 JQuickCurlResponseBody 可进一步读取响应头与字节流。常用读取方法为 asString()、header(name)、headers(name)、asInputStream()。调用为阻塞方式,方法返回即代表响应已读取完毕。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
import java.util.List;
public interface ReadApi {
// 语法:返回 String = 直接取响应体文本
@JCurlCommand("curl -X GET https://httpbin.org/get")
String raw(JQuickCurlReq request);
// 语法:返回 JQuickCurlResponseBody = 可读响应头与字节流
@JCurlCommand("curl -X GET 'https://httpbin.org/cookies/set?sessionId=abc123'")
JQuickCurlResponseBody detailed(JQuickCurlReq request);
}
class ReadDemo {
public static void main(String[] args) throws Exception {
ReadApi api = JCurlInvoker.createProxy(ReadApi.class);
System.out.println(api.raw(new JQuickCurlReq()));
JQuickCurlResponseBody response = api.detailed(new JQuickCurlReq());
String first = response.header("Set-Cookie"); // 首个同名响应头
List<String> all = response.headers("Set-Cookie"); // 全部同名响应头
String body = response.asString(); // 响应体文本
System.out.println(first);
System.out.println(all);
System.out.println(body);
}
}

13. 异步回调语法(Async Callback Syntax)

Section titled “13. 异步回调语法(Async Callback Syntax)”

JQuick-Curl 的代理调用为同步阻塞模型,2.6.0 未提供内置的异步 API 或回调接口(JFunction 是供方法引用定位使用的 Function 包装,并非异步回调)。需要异步时,由调用方在 Java 侧用 CompletableFuture 包装同步调用,实现并发与回调编排。

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import java.util.concurrent.CompletableFuture;
public interface AsyncApi {
@JCurlCommand("curl -X GET https://httpbin.org/get")
String get(JQuickCurlReq request);
}
class AsyncDemo {
public static void main(String[] args) throws Exception {
AsyncApi api = JCurlInvoker.createProxy(AsyncApi.class);
// 语法:调用方用 CompletableFuture 包装同步代理调用
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
try {
return api.get(new JQuickCurlReq());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// 回调编排
future.thenAccept(System.out::println).join();
}
}

14. 连接池配置语法(Connection Pool Config Syntax)

Section titled “14. 连接池配置语法(Connection Pool Config Syntax)”

连接池通过全局单例 JQuickCurlConfig 的 connectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit unit) 配置,参数依次为最大空闲连接数、空闲保活时长与时间单位。与超时配置同源,建议在应用启动阶段一次性完成。

import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class PoolSyntaxDemo {
public static void init() {
// 语法:connectionPool(最大空闲连接数, 保活时长, 时间单位)
JQuickCurlConfig.getInstance()
.connectionPool(50, 5, TimeUnit.MINUTES)
.maxRetryCount(2) // 失败重试次数
.followRedirects(true); // 自动跟随重定向
}
}
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-curl</artifactId>
<version>2.6.0</version>
</dependency>

以上示例均为语法级最小片段。完整可运行工程示例参见 GitHub 仓库测试源码目录:https://github.com/dromara/jquick-curl/tree/main/src/test/java/com/github/paohaijiao ,仓库主页:https://github.com/dromara/jquick-curl 。

和RestTemplate / OkHttp / WebClient对比

Section titled “和RestTemplate / OkHttp / WebClient对比”
对比项 JQuick-Curl RestTemplate OkHttp WebClient
请求描述方式 直接粘贴 curl 命令 手写 URI/实体/回调 手写 Request 构建代码 手写链式调用
注解声明式 API 支持(@JCurlCommand) 不支持 不支持 不支持(需配合 OpenFeign)
Spring 依赖 无 强依赖 Spring Web 无 强依赖 Spring WebFlux
底层传输 OkHttp 4.10 + 连接池 JDK HttpURLConnection / OkHttp 等可切换 OkHttp 自身 Reactor Netty 等
变量占位符 内置 ${} 需自行实现 需自行实现 需自行实现
重试 内置(代理层) 需自行实现 仅连接失败重试 需自行实现
拦截器 OkHttp 拦截器 ClientHttpRequestInterceptor OkHttp 拦截器 ExchangeFilterFunction
响应式/异步 不支持(同步调用) 同步 同步 + 异步 响应式
JDK 要求 JDK 8+ JDK 8+ JDK 8+ JDK 8+

JQuick-Curl 的核心价值在于消除“curl 调试 → Java 重写”之间的翻译成本;需要全异步、响应式编程模型的场景仍建议使用 WebClient。

1. 为什么必须显式写 -X?

2.6.0 的解析器要求命令中显式包含 -X <METHOD>,缺失时会断言失败。从浏览器复制的 curl 通常已包含请求方法信息,补全即可。

2. 从 Postman 复制的命令包含换行与反斜杠,如何处理?

文法支持行连接符(行尾 \ 后接换行),可以直接保留多行形式;也可以在 Java 中使用字符串拼接写成单行。

3. Windows 下 curl 命令中的引号如何转义?

在 Java 字符串中,单引号无需转义,双引号使用 \" 转义;注解中的命令优先使用单引号包裹参数(如 -H 'Content-Type: application/json'),可避免与 Java 字符串转义冲突。

4. 调用返回 null 或抛出 NullPointerException 是什么原因?

通常是命令语法错误或网络连接失败。2.6.0 会在日志中打印解析错误明细或网络异常堆栈,请优先查看控制台日志确认根因。

5. -v 为什么没有打印请求/响应报文?

-v / --verbose 在 2.6.0 中仅被文法接受,不产生原生 curl 的报文级输出;默认的 JLoggingInterceptor 仅输出请求耗时,需要完整报文日志时请自行实现拦截器。

6. 是否读取 http_proxy / https_proxy 环境变量?

不读取。代理必须通过命令中的 -x 显式指定。

7. 为什么 --data-urlencode 提交后服务端收不到表单?

这是 2.6.0 的已知缺陷,表单内容不会进入请求体。请改用 -H 'Content-Type: application/x-www-form-urlencoded' -d 'k=v'。

8. 返回自定义 POJO 时使用什么 JSON 库?

POJO 反序列化由内置 jquick-json 完成,其底层基于 Jackson 2.15.2,可按 Jackson 规则在字段上使用 @JsonProperty 等注解。

9. 是否支持在一条命令中写多个 URL?

不支持。文法虽然允许出现多个 URL 节点,但仅最后一个会被保留使用。

10. 可以在 Android 中使用吗?

核心库仅依赖 JDK 8 字节码与 OkHttp,但未针对 Android 运行时做适配与测试,不建议直接使用。

项目 说明
当前版本 2.6.0
底层传输 OkHttp 4.10.0、Okio 3.0.0
命令解析 自研文法解析器
JSON 处理 jquick-json 1.3.0(Jackson 2.15.2)
JDK 要求 JDK 8+
Spring 支持 无官方 Starter,通过 @Bean 注册代理;无 application.yml 自动绑定
  • 必须显式声明 -X 请求方法,缺失将导致解析断言失败。
  • JQuickCurlReq 上下文变量仅在整个 URL 与 -u 凭据位置生效;引号字符串(JSON body、header 值)内的变量不会被解析,此类场景请使用方法参数名替换。
  • 方法参数名替换依赖 -parameters 编译参数,普通 Java 项目需手动开启。
  • 接口方法至少应声明一个参数,无参方法通过代理调用会触发空指针异常。
  • @JCurlCommand 的 expectedStatus、expectedBusinessStatus、validationScript 在 2.6.0 中为预留属性,不产生实际校验。
  • HTTP 4xx / 5xx 默认不抛异常;响应包装对象不提供状态码访问器,需要借助网络拦截器获取状态码。
  • 网络 IO 异常会被转换为 NullPointerException 触发重试或抛出,异常语义不直观。
  • 代理层重试不区分 HTTP 方法,非幂等请求存在重复提交风险。
  • JQuickCurlConfig 为全局单例:@JTimeout 与链式配置会全局生效并可能相互覆盖;所有超时共用一个 TimeUnit。
  • 响应字节全量缓冲在内存中,不适合下载超大文件;不支持断点续传(-C)与限速(--limit-rate)。
  • --data-urlencode 不发送表单内容;--socks5-hostname 实际按 HTTP 代理处理;--max-redirs、--http2、-v、-s 不产生实际效果。
  • 不支持异步与响应式调用;不支持原生 curl 的多 URL、Cookie 域名自动匹配之外的完整 CookieJar 策略。
  • -k 跳过证书校验仅限调试,禁止用于生产环境;Cookie 文件与密钥文件需妥善保管,禁止提交代码仓库。

Maven 坐标:

<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-curl</artifactId>
<version>2.6.0</version>
</dependency>

本项目基于 Apache License 2.0 开源发布,由 Dromara 开源社区维护。