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 完成传输。

左:手写 RestTemplate 样板;右:JQuick-Curl 粘贴即用
- 0 行RestTemplate / OkHttp 样板代码
- 3 种调用方式:注解 / XML / 编程式
- 12 个完整支持的 curl 参数
- JDK 8+无框架依赖,普通 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 -X POST https://api.example.com/orders \ -H 'Content-Type: application/json' \ -d '{"sku":"A-1001","count":2}'命令与 API 文档、Postman 调试一一对应,终端里原样可跑;接口文档升级时只改这一条字符串。
<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 动态渲染请求头与请求体。
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 请求构建代码的团队。
Maven依赖
Section titled “Maven依赖”<dependency> <groupId>io.github.paohaijiao</groupId> <artifactId>jquick-curl</artifactId> <version>2.6.0</version></dependency>implementation 'io.github.paohaijiao:jquick-curl:2.6.0'环境要求:JDK 8+;传输层基于 OkHttp 4.10.0,相关依赖由 Maven 自动传递引入。
SpringBoot自动装配(最简Demo)
Section titled “SpringBoot自动装配(最简Demo)”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(等价于自动装配)@Configurationclass 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;
@RestControllerclass DemoController {
private final HttpBinApi httpBinApi;
// 构造器注入 DemoController(HttpBinApi httpBinApi) { this.httpBinApi = httpBinApi; }
@GetMapping("/demo/uuid") String uuid() { return httpBinApi.uuid(new JQuickCurlReq()); }}普通Java项目(无Spring)
Section titled “普通Java项目(无Spring)”不依赖任何容器,直接通过 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
Section titled “注解式声明调用 @JCurlCommand”定义API接口
Section titled “定义API接口”在自定义接口的方法上标注 @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' " + String createUser(JQuickCurlReq request);}注入并调用接口
Section titled “注入并调用接口”通过 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())); }}@JCurlCommand注解全参数说明
Section titled “@JCurlCommand注解全参数说明”| 属性 | 作用 | 必填 | 类型 | 默认值 |
|---|---|---|---|---|
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);}多方法接口示例
Section titled “多方法接口示例”一个接口可声明多个方法,分别对应不同的 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> |
多分支条件选择 | 使用分支时必填 | 无 |
编程式调用(JCurlInvoker)
Section titled “编程式调用(JCurlInvoker)”基础编程调用
Section titled “基础编程调用”除注解代理外,可以直接使用 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
Section titled “方法引用式调用 JCurlInvoker.invoke”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); }}批量执行类中所有命令
Section titled “批量执行类中所有命令”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())); }}占位符变量 $ 语法
Section titled “占位符变量 $ 语法”变量基础用法
Section titled “变量基础用法”JQuick-Curl 2.6.0 存在两条变量替换路径,适用位置不同:
- 方法参数名替换(全位置支持,推荐):在接口方法上声明普通参数,命令中的
${参数名}在解析前以纯文本方式替换,可放在 URL 路径、Query、请求头、请求体的任意位置。该机制依赖编译器保留参数名:继承spring-boot-starter-parent的项目默认开启-parameters;普通项目需要手动为maven-compiler-plugin添加-parameters参数。 JQuickCurlReq上下文变量:通过JQuickCurlReq.put(key, value)传值,由文法中的变量节点解析。2.6.0 中该方式仅在整个 URL(curl -X GET ${url})与-u凭据位置(-u ${user}:${password})生效;在引号包裹的字符串内部(含 JSON body、header 值)不会被解析。
凭据、动态路径等场景优先使用方式一;需要灵活键值集合时使用方式二并避开引号内嵌位置。
变量放在URL路径
Section titled “变量放在URL路径”通过方法参数声明路径变量,${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参数
Section titled “变量放在Query参数”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 处理。
变量放在Header
Section titled “变量放在Header”请求头值中的占位符在调用时替换;动态 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请求体
Section titled “变量放在JSON请求体”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入参详解
Section titled “JQuickCurlReq入参详解”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)); }}请求体各种场景示例
Section titled “请求体各种场景示例”GET请求(带query参数)
Section titled “GET请求(带query参数)”直接在 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)); }}POST JSON
Section titled “POST JSON”通过 -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())); }}POST Multipart 文件表单 -F
Section titled “POST Multipart 文件表单 -F”使用 -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())); }}文件上传与下载
Section titled “文件上传与下载”文件上传(本地文件)
Section titled “文件上传(本地文件)”-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")); }}文件下载,保存到本地
Section titled “文件下载,保存到本地”使用 -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); }}下载到内存byte数组
Section titled “下载到内存byte数组”不使用 --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); }}注意:响应字节会被全量缓冲在内存中,下载超大文件时请评估堆内存压力。
Basic Auth -u
Section titled “Basic Auth -u”-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)); }}Bearer Token(Header)
Section titled “Bearer Token(Header)”通过 -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")); }}自定义鉴权头
Section titled “自定义鉴权头”对接使用非标准鉴权方案的开放平台时,按其文档要求添加自定义请求头即可。
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")); }}Cookie与会话保持(-b / -c)
Section titled “Cookie与会话保持(-b / -c)”-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 / HTTPS 配置
Section titled “SSL / HTTPS 配置”忽略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())); }}加载自定义证书
Section titled “加载自定义证书”2.6.0 未提供在应用内加载自定义 KeyStore / TrustStore 的编程 API。生产环境中需要使用私有 CA 或双向证书时,推荐通过 JVM 标准系统属性指定信任库与密钥库。
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 重写客户端构建逻辑,参见“扩展:自定义解析器”章节。
HTTP代理配置
Section titled “HTTP代理配置”http/https代理
Section titled “http/https代理”通过 -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())); }}代理账号密码
Section titled “代理账号密码”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)); }}超时、重定向、重试
Section titled “超时、重定向、重试”连接超时、读取超时
Section titled “连接超时、读取超时”通过 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
Section titled “跟随重定向 -L”在命令中加入 -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())); }}请求重试配置
Section titled “请求重试配置”注解式代理在方法抛出异常时会自动重试,最大尝试次数为 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
Section titled “单方法超时覆盖 @JTimeout”在方法上额外标注 @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 直接修改全局单例配置,超时值会在调用后持续保留,对其他方法产生影响。
全局连接池配置
Section titled “全局连接池配置”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");代码手动配置连接池参数
Section titled “代码手动配置连接池参数”不使用任何配置文件时,直接通过链式 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 中固定,不对外暴露配置。
拦截器(请求前置/响应后置)
Section titled “拦截器(请求前置/响应后置)”自定义拦截器实现
Section titled “自定义拦截器实现”拦截器直接复用 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; }}全局注册拦截器
Section titled “全局注册拦截器”通过 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 |
空 |
单请求单独指定拦截器
Section titled “单请求单独指定拦截器”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())); }}异常体系与错误处理
Section titled “异常体系与错误处理”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结果进入后续流程。
捕获异常+获取原始响应
Section titled “捕获异常+获取原始响应”调用方可以在业务层统一捕获代理调用异常;由于失败时框架不再返回响应对象,如需保留非 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(); } }}扩展:自定义解析器
Section titled “扩展:自定义解析器”JQuickParseHandler扩展点
Section titled “JQuickParseHandler扩展点”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语法解析
Section titled “自定义curl语法解析”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 中实现对应选项的处理逻辑。
支持的Curl参数清单
Section titled “支持的Curl参数清单”下表列出 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())); }}2. 请求头语法(Header Syntax)
Section titled “2. 请求头语法(Header Syntax)”使用 -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);}9. 代理配置语法(Proxy Syntax)
Section titled “9. 代理配置语法(Proxy Syntax)”代理通过 -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); // 自动跟随重定向 }}Maven Dependency
Section titled “Maven Dependency”<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 运行时做适配与测试,不建议直接使用。
注意事项 & 已知限制
Section titled “注意事项 & 已知限制”| 项目 | 说明 |
|---|---|
| 当前版本 | 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 文件与密钥文件需妥善保管,禁止提交代码仓库。
仓库与License
Section titled “仓库与License”- GitHub 仓库:https://github.com/dromara/jquick-curl
- Maven Central:io.github.paohaijiao:jquick-curl
- Issue / Discussions:通过 GitHub 仓库提交问题与建议
Maven 坐标:
<dependency> <groupId>io.github.paohaijiao</groupId> <artifactId>jquick-curl</artifactId> <version>2.6.0</version></dependency>本项目基于 Apache License 2.0 开源发布,由 Dromara 开源社区维护。