Skip to content

JQuick-Curl

JQuick-Curl solves exactly one problem: the API docs give you a curl command, while Java asks you to write something else entirely.

The typical integration workflow is: open the third-party API docs → copy the sample curl → translate it by hand into HttpClient code → then fill in Content-Type, Authorization, timeouts, retries and connection pooling one by one. That translation is tedious and error-prone, and whenever the docs are upgraded and the command changes, the code has to change with it.

JQuick-Curl removes that step:

Browser / Postman / Apifox
│ Copy as cURL
▼
curl -X POST https://api.example.com/orders \
-H 'Content-Type: application/json' \
-u user:pass \
-d '{"sku":"A-1001","count":2}'
│ paste into Java
▼
@JCurlCommand("curl -X POST https://api.example.com/orders ...")
String create(JQuickCurlReq request);

The whole pipeline has three layers:

Layer Implementation Responsibility
Parsing layer JQuickCurlLexer / JQuickCurlParser Parses the curl command text into a syntax tree
Assembly layer JQuickCurlCommonVisitor / JQuickCurlCoreVisitor Walks the syntax tree, collects the method, headers, body, upload/download, proxy and other options, and builds a Request
Transport layer OkHttp 4.x + ConnectionPool Executes the request and wraps the response as JQuickCurlResponseBody

On top of that, three calling styles are provided:

  1. Declarative annotation-based calls — write @JCurlCommand on an interface method; JCurlInvoker.createProxy generates a dynamic proxy, and calling the method issues the request.
  2. Centralized XML configuration — request templates are maintained centrally in apis.xml, decoupled from Java code, with support for the <if> / <foreach> / <choose> dynamic tags.
  3. Programmatic calls — construct an executor directly or invoke through method references; suited to scripts, tests and no-interface scenarios.
  • A curl-native API: paste the command as-is, with no more “curl → Java” translation. Commands map one-to-one onto the API docs, so an upgrade is a one-string change.
  • Annotation-driven declarative interfaces: @JCurlCommand goes on the interface method, and JCurlInvoker.createProxy(XxxApi.class) gives you an injectable client in one line.
  • Dynamic proxy + connection pool: the dynamic proxy reuses handler instances managed by GenericObjectPool, while JQuickCurlConfig manages the OkHttp connection pool centrally, avoiding rebuilding the client on every request.
  • Dual XML and annotation modes: commands can live in code or be maintained centrally in apis.xml, with runtime variable substitution via the ${} (annotation) and #{} (XML) placeholders.
  • Dynamic tags: XML mode supports <if test="..."> / <foreach> / <choose><when><otherwise> to assemble headers and options from runtime conditions.
  • Variable substitution instead of hardcoding: credentials, URLs and traceIds are passed in through JQuickCurlReq, so they never enter the source tree or the repository.
  • Complete request body support: JSON, forms, the --data family, -F multipart forms and multi-file uploads.
  • File upload and download: upload with -F, and write response bytes to a local file with -o / --output.
  • Cookies and cookie jars: send with -b, persist in Netscape format with -c, reuse with -b @file — login state can be kept across requests.
  • Unified timeout / retry / redirect / connection-pool configuration: a single JQuickCurlConfig singleton, configured once with chained calls and overridable per method with @JTimeout.
  • Global and network interceptors: attach OkHttp interceptors directly with addInterceptor / addNetworkInterceptor for uniform signing, tracing and logging.
  • HTTP proxy: specify the proxy host and port with -x / --proxy.
  • Pure Java, no framework coupling: JDK 8+, with no Spring dependency and no coupling to any container; in a Spring Boot project just wire it up as an ordinary bean.
  • Apache 2.0 licensed: commercially usable, and published to Maven Central.

Good fit:

  • Integrating third-party HTTP APIs: payments, SMS, maps, logistics, open platforms and the like, where the official docs give curl samples you can copy directly.
  • API joint debugging and troubleshooting: take the same curl command from the docs, the terminal or a test case into Java to reproduce the issue, so the command and the symptom correspond one-to-one.
  • Traffic capture and data scraping: scraping tasks that need cookie persistence, proxy egress or custom headers.
  • An in-house API client layer: maintain all downstream APIs centrally in apis.xml; business code injects only the interface and never cares about request details.
  • Sharing one set of interface definitions between Spring Boot and traditional Java projects: request declarations are decoupled from business code and shared by both.
  • Lightweight HTTP calls: scenarios where you don’t want to pull in a whole WebClient / Feign stack for a few requests.

Not a good fit:

  • Scenarios needing HTTP/2 multiplexing, long-lived streaming SSE connections, WebSocket or gRPC: JQuick-Curl targets request-response HTTP calls; for long connections and streaming protocols, use a dedicated client.
  • Scenarios needing fine-grained control over OkHttp internals (custom DNS, custom SSLSocketFactory, streaming large files as the request body): the current version does not expose these extension points.
  • Scenarios needing lifecycle hooks beyond interceptors (connection established, DNS resolution callbacks): use OkHttp directly.

Just declare a single dependency; okhttp, gson, commons-pool2 and the rest are pulled in transitively by Maven.

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

Transitive dependency list (for reference in offline environments):

Dependency Version Purpose
com.squareup.okhttp3:okhttp 4.10.0 HTTP transport layer
org.antlr:antlr4-runtime 4.13.2 curl command parsing (internal grammar engine)
com.google.code.gson:gson 2.8.0 JSON processing
org.apache.commons:commons-pool2 2.12.0 Handler object pool
org.slf4j:slf4j-api 2.0.9 Logging facade
io.github.paohaijiao:jquick-json 1.3.0 JSON serialization and POJO conversion
io.github.paohaijiao:javelin-core 2.7.0 Foundational context, exception and type-conversion capabilities

Step 1: define the interface and write the curl command.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
/**
* GitHub API client.
* The command on the method can be pasted straight from the browser or Postman's Copy as cURL.
*/
public interface GitHubApi {
// Fetch repository info
@JCurlCommand("curl -X GET https://api.github.com/repos/dromara/jquick-curl " +
"-H 'Accept: application/vnd.github+json' " +
"-H 'User-Agent: JQuick-Demo'")
String repoInfo(JQuickCurlReq request);
}

Step 2: register the configuration and proxy beans.

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.executor.JCurlInvoker;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* JQuick-Curl integration configuration: initialise the global configuration and register the interface proxy as a bean.
*/
@Configuration
public class CurlConfiguration {
/**
* The global configuration is initialised only once at startup and shared by all subsequent requests.
*/
@Bean
public JQuickCurlConfig jquickCurlConfig() {
return JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.maxRetryCount(2)
.followRedirects(true);
}
/**
* Register the interface proxy as a bean; business code only needs to inject it by type.
*/
@Bean
public GitHubApi gitHubApi() {
return JCurlInvoker.createProxy(GitHubApi.class);
}
}

Step 3: inject and call.

package com.example.curl;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RepoController {
private final GitHubApi gitHubApi;
public RepoController(GitHubApi gitHubApi) {
this.gitHubApi = gitHubApi;
}
@GetMapping("/repo")
public String repo() {
// Parameters are all carried by JQuickCurlReq; when there are no variables, just pass an empty object
return gitHubApi.repoInfo(new JQuickCurlReq());
}
}

Optional: externalise the configuration into a properties file.

The configuration class can read parameters from a .properties file on the classpath; put it in src/main/resources/quick-curl.properties:

quick.curl.connect.timeout=3000
quick.curl.read.timeout=10000
quick.curl.write.timeout=10000
quick.curl.pool.max.idle=50
quick.curl.pool.keep.alive=300000
quick.curl.max.retry.count=2

Load it explicitly in the @Bean (note: it still has to be called manually and will not take effect automatically):

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Configuration
public class CurlPropertiesConfiguration {
@Bean
public JQuickCurlConfig jquickCurlConfig() throws IOException {
return JQuickCurlConfig.getInstance().loadFromClasspathResource("quick-curl.properties");
}
}

No container is needed; after JCurlInvoker.createProxy you can call the interface directly.

package com.example.curl;
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 java.util.concurrent.TimeUnit;
public class PlainJavaDemo {
public interface UserApi {
@JCurlCommand("curl -X GET https://httpbin.org/get -H 'Accept: application/json'")
String list(JQuickCurlReq request);
}
public static void main(String[] args) {
// 1. Optional: adjust the global configuration first (timeouts, retries, connection pool, interceptors)
JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.connectionPool(50, 5, TimeUnit.MINUTES);
// 2. Create the interface proxy
UserApi api = JCurlInvoker.createProxy(UserApi.class);
// 3. Calling the method issues the request
String body = api.list(new JQuickCurlReq());
System.out.println(body);
}
}

Annotation mode is the most common approach: write the curl command on an interface method, generate a JDK dynamic proxy with JCurlInvoker.createProxy, and calling the method issues the request. The interface only describes “what to call” and contains no request-building code.

Annotate the interface method with @JCurlCommand, whose value is the complete curl command. The command may span lines, or be assembled from string constants with +.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface UserApi {
// Fetch user details: the path parameter uses the ${id} placeholder
@JCurlCommand("curl -X GET https://api.example.com/v1/users/${id} " +
"-H 'Accept: application/json'")
String getUser(JQuickCurlReq request);
// Paged query: the query parameters are written directly in the URL
@JCurlCommand("curl -X GET 'https://api.example.com/v1/users?page=1&size=20' " +
"-H 'Accept: application/json'")
String page(JQuickCurlReq request);
}

When calling, put the variables into JQuickCurlReq:

JQuickCurlReq request = new JQuickCurlReq();
request.put("id", 1001);
String user = api.getUser(request);

The proxy object is stateless and can be reused long-term once created (for example registered as a Spring bean); there is no need to call createProxy on every invocation.

package com.example.curl;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class AnnotationDemo {
public static void main(String[] args) {
// Create the proxy; it can be reused
UserApi api = JCurlInvoker.createProxy(UserApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("id", 1001);
// Calling the method issues the HTTP request; the return value is determined by the method signature
String user = api.getUser(request);
System.out.println(user);
}
}

The method return value determines how the response is converted; for the available types, see the “Supported return types” section.

Full @JCurlCommand annotation parameter reference

Section titled “Full @JCurlCommand annotation parameter reference”

The annotation is defined in com.github.paohaijiao.anno.JCurlCommand, targets methods (@Target(ElementType.METHOD)) and has runtime retention (@Retention(RetentionPolicy.RUNTIME)).

Parameter Type Required Default Purpose
value String Yes none The full curl command, i.e. what you copy from the browser / Postman / Apifox.
execute boolean No true Whether the method is executable. When set to false, calling the method throws UnsupportedOperationException; useful for temporarily disabling an endpoint.
expectedStatus int No 200 The expected HTTP status code. In the current version this is a reserved parameter and does not take part in actual validation.
expectedBusinessStatus String No "200" The expected business status code. In the current version this is a reserved parameter and does not take part in actual validation.
validationScript String No "" Response validation script. In the current version this is a reserved parameter and is not implemented.

Usage example:

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface OrderApi {
// Normal endpoint
@JCurlCommand("curl -X GET https://api.example.com/v1/orders")
String list(JQuickCurlReq request);
// Temporarily disabled: calling it throws UnsupportedOperationException
@JCurlCommand(value = "curl -X DELETE https://api.example.com/v1/orders/1", execute = false)
String remove(JQuickCurlReq request);
}

@JTimeout is also a method-level annotation, used to override the global timeout configuration. It acts on the proxy invocation chain, so it only takes effect in JCurlInvoker.createProxy mode.

Parameter Type Required Default Purpose
connect long No -1 Connect timeout, in the same unit as the global configuration’s timeUnit (milliseconds by default).
read long No -1 Read timeout.
write long No -1 Write timeout.
package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.anno.JTimeout;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface ReportApi {
// Report export is slow, so the read timeout is relaxed to 60 seconds just for this method
@JCurlCommand("curl -X GET https://api.example.com/v1/reports/export --output './report.xlsx'")
@JTimeout(connect = 3000, read = 60000, write = 60000)
byte[] export(JQuickCurlReq request);
}

An interface can declare any number of @JCurlCommand methods sharing a single proxy instance, covering CRUD plus uploads and downloads.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
public interface ProductApi {
// List: GET + query parameters
@JCurlCommand("curl -X GET 'https://api.example.com/v1/products?page=${page}&size=${size}' " +
"-H 'Accept: application/json'")
String list(JQuickCurlReq request);
// Detail: path variable
@JCurlCommand("curl -X GET https://api.example.com/v1/products/${id} " +
"-H 'Accept: application/json'")
Product detail(JQuickCurlReq request);
// Create: JSON request body
@JCurlCommand("curl -X POST https://api.example.com/v1/products " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"Desk\",\"price\":199.00}'")
Product create(JQuickCurlReq request);
// Update: PUT
@JCurlCommand("curl -X PUT https://api.example.com/v1/products/${id} " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"Desk Pro\",\"price\":299.00}'")
Product update(JQuickCurlReq request);
// Delete: no response body
@JCurlCommand("curl -X DELETE https://api.example.com/v1/products/${id}")
void remove(JQuickCurlReq request);
// Case needing access to response headers: return JQuickCurlResponseBody
@JCurlCommand("curl -X GET https://api.example.com/v1/products/${id}")
JQuickCurlResponseBody raw(JQuickCurlReq request);
}

Custom POJO return values (JSON is deserialized automatically):

package com.example.curl;
public class Product {
private Long id;
private String name;
private Double price;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}

If you want request definitions fully decoupled from Java code — for instance, when operations or QA maintain the endpoint list — you can keep all curl commands in XML. XML mode reuses the same parsing and transport pipeline; only the “source of the command” changes from an annotation to a file.

The file goes under src/main/resources/ by default, for example 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.curl.UserApi">
<curl name="list" returnClass="java.lang.String">
curl -X GET 'https://api.example.com/v1/users?page=1&size=20'
</curl>
<curl name="getUser" returnClass="com.example.curl.User">
curl -X GET https://api.example.com/v1/users/#{id}
</curl>
<curl name="create" returnClass="com.example.curl.User">
curl -X POST https://api.example.com/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"#{name}","email":"#{email}"}'
</curl>
<curl name="remove" returnClass="java.lang.Void">
curl -X DELETE https://api.example.com/v1/users/#{id}
</curl>
</curls>

Element and attribute reference:

Node / attribute Required Type Description
curls Yes Root node The XML root element, corresponding to JQuickCurlXmlElement#getRootElementTagName.
curls/@namespace Yes String The fully qualified class name of the interface; the proxy uses it to match the Java interface.
curl Yes Child node One method corresponds to one curl node; several may be declared.
curl/@name Yes String Must exactly match the Java interface method name, otherwise binding fails.
curl/@returnClass Yes String Fully qualified name of the method return type, e.g. java.lang.String, java.lang.Void, byte[], java.util.List, com.example.User.
curl/@paramClass No String A reserved attribute for declaring the parameter type.
if No Dynamic tag Renders the tag’s content only when the test attribute is true; can be nested.
foreach No Dynamic tag The collection and item attributes are required and open, close, separator are optional; iterates a collection to concatenate content.
choose / when / otherwise No Dynamic tag Multi-branch selection; when requires a test attribute.

The DTD is shipped inside the module at classpath:paohaijiao/dtd/Jquick-curl.dtd; adding the <!DOCTYPE> gives you element and attribute completion in the IDE.

Use JQuickCurlXmlParseFactory as the parse handler, together with JQuickXmlFactory to load the XML and create an interface proxy.

package com.example.curl;
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 class XmlDemo {
public static void main(String[] args) {
// 1. Specify the XML parse handler (provided by jquick-curl)
JQuickParseHandler parser = new JQuickCurlXmlParseFactory();
// 2. Load apis.xml from the classpath
JQuickFactory factory = new JQuickXmlFactory(parser, "apis.xml");
// 3. Create the proxy for the interface type
UserApi api = factory.createApi(UserApi.class);
// 4. Invoke
JQuickCurlReq request = new JQuickCurlReq();
request.put("id", 1001);
System.out.println(api.getUser(request));
}
public interface UserApi {
String list(JQuickCurlReq request);
User getUser(JQuickCurlReq request);
User create(JQuickCurlReq request);
void remove(JQuickCurlReq request);
}
}

XML mode supports assembling commands from runtime conditions, which suits “optional” parts such as headers and sort parameters.

<?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.curl.SearchApi">
<!-- if: append the tracing header only when withTrace is true -->
<curl name="search" returnClass="java.lang.String">
curl -X GET #{host}/search
<if test="withTrace == true"> -H "X-Trace-Id: #{traceId}" </if>
</curl>
<!-- foreach: concatenate several same-name query parameters from a tag collection -->
<curl name="byTags" returnClass="java.lang.String">
curl -X GET '#{host}/search?page=1'
<foreach collection="tags" item="tag" separator="&amp;"> -H "X-Tag: #{tag}" </foreach>
</curl>
<!-- choose / when / otherwise: pick a different header per environment -->
<curl name="env" returnClass="java.lang.String">
curl -X GET #{host}/ping
<choose>
<when test="env == 'prod'"> -H "X-Env: production" </when>
<otherwise> -H "X-Env: development" </otherwise>
</choose>
</curl>
</curls>

When calling, put the condition variables into JQuickCurlReq:

package com.example.curl;
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;
import java.util.Arrays;
import java.util.List;
public class XmlDynamicDemo {
public interface SearchApi {
String search(JQuickCurlReq request);
String byTags(JQuickCurlReq request);
String env(JQuickCurlReq request);
}
public static void main(String[] args) {
JQuickParseHandler parser = new JQuickCurlXmlParseFactory();
JQuickFactory factory = new JQuickXmlFactory(parser, "apis.xml");
SearchApi api = factory.createApi(SearchApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("host", "https://api.example.com");
request.put("withTrace", true);
request.put("traceId", "8f3c1a");
request.put("env", "prod");
List<String> tags = Arrays.asList("java", "http");
request.put("tags", tags);
System.out.println(api.search(request));
System.out.println(api.byTags(request));
System.out.println(api.env(request));
}
}

In some scenarios it is inconvenient to inject an interface — for example one-off scripts, unit tests, or dynamically selecting an interface. In addition to createProxy, JCurlInvoker provides a set of invoke overloads that let you trigger an annotated method directly through a method reference, without creating a proxy object.

The first parameter of JCurlInvoker.invoke is a JFunction (which extends java.util.function.Function and is serializable); pass a method reference of the form Interface::method. The method the method reference points to must be annotated with @JCurlCommand.

package com.example.curl;
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;
public class InvokeDemo {
public interface UserApi {
@JCurlCommand("curl -X GET https://api.example.com/v1/users/${id} " +
"-H 'Accept: application/json'")
String getUser(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
JQuickCurlReq request = new JQuickCurlReq();
request.put("id", 1001);
// Simplest form: use the global default configuration
String body = JCurlInvoker.invoke(UserApi::getUser, request, String.class);
System.out.println(body);
// Specify a configuration: this call uses a standalone configuration
JQuickCurlConfig config = JQuickCurlConfig.getInstance().readTimeout(5000, java.util.concurrent.TimeUnit.MILLISECONDS);
String body2 = JCurlInvoker.invoke(UserApi::getUser, request, config, String.class);
System.out.println(body2);
}
}

All invoke overloads (generic wildcards omitted):

Method signature Description
invoke(JFunction methodRef, Class<T> returnType) Uses an empty JQuickCurlReq, a new JContext, and the global configuration.
invoke(JFunction methodRef, JQuickCurlReq req, Class<T> returnType) Specifies the variable input.
invoke(JFunction methodRef, JContext context, Class<T> returnType) Specifies the variable context.
invoke(JFunction methodRef, JQuickCurlConfig config, Class<T> returnType) Specifies the configuration.
invoke(JFunction methodRef, JQuickCurlReq req, JContext context, Class<T> returnType) Specifies the input and the context.
invoke(JFunction methodRef, JQuickCurlReq req, JQuickCurlConfig config, Class<T> returnType) Specifies the input and the configuration.
invoke(JFunction methodRef, JQuickCurlConfig config, JContext context, Class<T> returnType) Specifies the configuration and the context.
invoke(JFunction methodRef, JQuickCurlReq req, JContext context, JQuickCurlConfig config, Class<T> returnType) All-parameter form, recommended when you need complete control.
invoke(JFunction methodRef, JQuickCurlReq req, JContext context, JQuickCurlConfig config, Type type) The return type is passed as a java.lang.reflect.Type, used for generic return values.
createProxy(Class<T> interfaceClass) Generates a dynamic proxy for the interface, equivalent to annotation mode.

Without annotations: use the executor directly

Section titled “Without annotations: use the executor directly”

If you don’t even want to define an interface, you can bypass the annotations and hand the curl string directly to JQuickCurlExecutor. This is the lowest-level, most flexible usage.

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.executor.JQuickCurlExecutor;
import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
public class ExecutorDemo {
public static void main(String[] args) throws Exception {
// 1. Variable context: the source of ${id} / ${env} for this request
JContext context = new JContext();
context.put("id", 1001);
// 2. Create the executor
JQuickCurlExecutor executor = new JQuickCurlExecutor(context, JQuickCurlConfig.getInstance());
// 3. Execute the curl string directly
String curl = "curl -X GET https://api.example.com/v1/users/${id} -H 'Accept: application/json'";
JQuickCurlResponseBody response = executor.execute(curl);
System.out.println(response.asString());
}
}

The method’s return type determines how the response is converted. The conversion logic is centralized in JQuickCurlResponseConvert and dispatches on the declared type:

Method return type Conversion result
void / Void Returns null; the request is executed but the response body is not read.
String The response text decoded as UTF-8.
byte[] The raw response bytes.
InputStream A byte input stream backed by the cached byte array.
Reader A character input stream, UTF-8.
BufferedReader A buffered character input stream.
JQuickCurlResponseBody The raw response body object, from which response headers and bytes can be read further.
okhttp3.ResponseBody Same as above.
Custom POJO The response text is deserialized as JSON, with fields mapping one-to-one to JSON keys.
Map / List The response text is deserialized as JSON into the corresponding collection type.
package com.example.curl;
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.io.BufferedReader;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
public interface ReturnTypeApi {
// Text
@JCurlCommand("curl -X GET https://api.example.com/v1/ping")
String asString(JQuickCurlReq request);
// Binary: suitable for downloads, images, archives
@JCurlCommand("curl -X GET https://api.example.com/v1/logo.png")
byte[] asBytes(JQuickCurlReq request);
// Streaming read
@JCurlCommand("curl -X GET https://api.example.com/v1/big-file")
InputStream asStream(JQuickCurlReq request);
@JCurlCommand("curl -X GET https://api.example.com/v1/big-file")
BufferedReader asReader(JQuickCurlReq request);
// Raw response body: use when you need the response headers
@JCurlCommand("curl -X GET https://api.example.com/v1/users/1001")
JQuickCurlResponseBody asResponse(JQuickCurlReq request);
// Single object
@JCurlCommand("curl -X GET https://api.example.com/v1/users/1001")
User asUser(JQuickCurlReq request);
// Collection
@JCurlCommand("curl -X GET https://api.example.com/v1/users")
List<User> asUserList(JQuickCurlReq request);
// Dynamic structure
@JCurlCommand("curl -X GET https://api.example.com/v1/users/1001")
Map<String, Object> asMap(JQuickCurlReq request);
// Only trigger the request, ignore the response
@JCurlCommand("curl -X POST https://api.example.com/v1/jobs/refresh")
void trigger(JQuickCurlReq request);
}

Getting full response metadata (status code, headers)

Section titled “Getting full response metadata (status code, headers)”

When you need the response headers, declare the return type as JQuickCurlResponseBody:

package com.example.curl;
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 ResponseMetaDemo {
public interface UserApi {
@JCurlCommand("curl -X GET 'https://api.example.com/v1/users/1001?debug=1'")
JQuickCurlResponseBody getUser(JQuickCurlReq request);
}
public static void main(String[] args) {
UserApi api = JCurlInvoker.createProxy(UserApi.class);
JQuickCurlResponseBody response = api.getUser(new JQuickCurlReq());
// Response headers
String contentType = response.header("Content-Type");
List<String> setCookie = response.headers("Set-Cookie");
boolean hasTrace = response.hasHeader("X-Trace-Id");
// Response body
String body = response.asString();
byte[] bytes = response.getCachedBytes();
long length = response.contentLength();
System.out.println("Content-Type = " + contentType);
System.out.println("Set-Cookie = " + setCookie);
System.out.println("X-Trace-Id present = " + hasTrace);
System.out.println("body length = " + length);
System.out.println(body);
System.out.println("bytes = " + bytes.length);
}
}

The complete method list of JQuickCurlResponseBody (inherits from okhttp3.ResponseBody):

Method Return type Description
getHeaders() okhttp3.Headers All response headers.
header(String name) String The first response header with the given name, or null if it does not exist.
headers(String name) List<String> All response headers with the given name (there may be several for redirects or Set-Cookie).
hasHeader(String name) boolean Whether the response header exists.
asString() String Decodes the response body as UTF-8.
asString(Charset) String Decodes with the specified charset.
asInputStream() InputStream A byte stream of the response body.
asReader() / asReader(Charset) InputStreamReader A character stream of the response body.
asBufferedReader() / asBufferedReader(Charset) BufferedReader A buffered character stream.
getCachedBytes() byte[] A copy of the cached response bytes.
toByteString() okio.ByteString A ByteString view of the response bytes.
contentType() okhttp3.MediaType The response content type.
contentLength() long The response body length.
source() okio.BufferedSource A buffered source backed by the cached bytes.

Variable parts of a command are placeholders of the form ${variableName}, replaced at runtime by values passed through JQuickCurlReq. The point of variables is to move credentials, IDs, environment addresses and so on out of the source code — only structure remains in the code, while the values come from a configuration center, environment variables, or the caller.

Variable values are always carried by JQuickCurlReq. It is essentially a HashMap<String, Object>, and values are converted to strings via toString(), so numbers and booleans can be put in directly.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class VariableDemo {
public interface UserApi {
@JCurlCommand("curl -X GET https://${host}/v1/users/${id} " +
"-H 'X-Tenant: ${tenant}' " +
"-H 'Accept: application/json'")
String getUser(JQuickCurlReq request);
}
public static void main(String[] args) {
JQuickCurlReq request = new JQuickCurlReq();
request.put("host", "api.example.com");
request.put("id", 1001);
request.put("tenant", "acme");
UserApi api = JCurlInvoker.createProxy(UserApi.class);
System.out.println(api.getUser(request));
}
}

Path variables are the most common usage and suit RESTful-style interfaces.

public interface OrderApi {
@JCurlCommand("curl -X GET https://api.example.com/v1/orders/${orderId}/items/${itemId}")
String item(JQuickCurlReq request);
}
JQuickCurlReq request = new JQuickCurlReq();
request.put("orderId", "SO-20260301-001");
request.put("itemId", 7);

The query part also supports variables. Values are not URL-encoded automatically; handle spaces or special characters yourself.

public interface SearchApi {
@JCurlCommand("curl -X GET 'https://api.example.com/v1/search?keyword=${keyword}&page=${page}&size=20'")
String search(JQuickCurlReq request);
}
JQuickCurlReq request = new JQuickCurlReq();
request.put("keyword", "java");
request.put("page", 1);

Header values support variables, which is commonly used for traceId, tenant ID, token prefix, and so on.

public interface HeaderApi {
@JCurlCommand("curl -X GET https://api.example.com/v1/profile " +
"-H 'Authorization: Bearer ${token}' " +
"-H 'X-Trace-Id: ${traceId}' " +
"-H 'X-Tenant: ${tenant}'")
String profile(JQuickCurlReq request);
}
JQuickCurlReq request = new JQuickCurlReq();
request.put("token", System.getenv("API_TOKEN"));
request.put("traceId", java.util.UUID.randomUUID().toString());
request.put("tenant", "acme");

Variables in the request body fall into two modes:

XML mode uses #{variableName}; it performs a plain-text substitution before parsing, so a variable may appear inside a JSON string — this is the most reliable approach.

<curl name="create" returnClass="java.lang.String">
curl -X POST #{host}/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"#{name}","email":"#{email}","dept":"#{dept}"}'
</curl>

Annotation mode uses ${variableName}. Prefer passing the whole request body as a variable, or splitting the body into string constants that you concatenate, rather than nesting a variable inside a JSON literal:

public interface UserApi {
// Option 1: pass the whole request body as one variable
@JCurlCommand("curl -X POST https://api.example.com/v1/users " +
"-H 'Content-Type: application/json' " +
"-d '${payload}'")
String create(JQuickCurlReq request);
// Option 2: the variable appears only in a value position and is recognizable by the parser
@JCurlCommand("curl -X POST https://api.example.com/v1/users " +
"-H 'Content-Type: application/json' " +
"-d '{\"name\":\"${name}\"}'")
String createByField(JQuickCurlReq request);
}
// Option 1: assemble the JSON yourself on the Java side
JQuickCurlReq request = new JQuickCurlReq();
request.put("payload", "{\"name\":\"Ada\",\"email\":\"[email protected]\"}");
// Option 2: a single field variable
JQuickCurlReq fieldRequest = new JQuickCurlReq();
fieldRequest.put("name", "Ada");

JQuickCurlReq is defined in the com.github.paohaijiao.domain.req package:

package com.github.paohaijiao.domain.req;
import java.util.HashMap;
public class JQuickCurlReq extends HashMap<String, Object> {
}
Item Description
Type A class that directly extends HashMap<String, Object>.
Construction new JQuickCurlReq(); there is no Builder and no fields are defined.
Value access Entirely Map semantics: put / get / containsKey / keySet, etc.
Relationship to variables During a proxy call, every key-value pair in JQuickCurlReq is injected one by one into the variable context (JContext) for ${} and #{} resolution.
Value types Any Object, treated via toString() when substituted; collection values such as List / arrays can be used by XML’s <foreach>.
Can it be null It cannot be null in an interface method call (the proxy iterates the parameters and reads their key-value pairs); when there are no variables, pass new JQuickCurlReq().

Common patterns:

// Put entries one by one
JQuickCurlReq request = new JQuickCurlReq();
request.put("id", 1001);
request.put("tenant", "acme");
// Put entries in bulk
java.util.Map<String, Object> values = new java.util.HashMap<>();
values.put("page", 1);
values.put("size", 20);
request.putAll(values);
// Read and check
Object id = request.get("id");
boolean hasTenant = request.containsKey("tenant");

The request body is described by two option families: -d (including --data / --data-ascii / --data-binary / --data-raw) and -F. Understanding two defaults saves a lot of trouble:

  • Content-Type defaults to application/json, and only when -H 'Content-Type: ...' appears in the command does the header take precedence.
  • Without -d, the request body is the literal {} (only for methods that allow a request body; GET / HEAD / TRACE / CONNECT carry no request body).

GET requests put parameters directly in the URL, and they may also be placed inside quotes. GET is a method that “does not allow a request body”; the parser explicitly builds a body-less request.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class GetDemo {
public interface UserApi {
// Fixed query parameters
@JCurlCommand("curl -X GET 'https://api.example.com/v1/users?page=1&size=20' " +
"-H 'Accept: application/json'")
String page(JQuickCurlReq request);
// Dynamic query parameters
@JCurlCommand("curl -X GET 'https://api.example.com/v1/search?keyword=${keyword}&page=${page}' " +
"-H 'Accept: application/json'")
String search(JQuickCurlReq request);
// Multiple custom headers passed at the same time
@JCurlCommand("curl -X GET https://api.example.com/v1/me " +
"-H 'Accept: application/json' " +
"-H 'X-Trace-Id: ${traceId}' " +
"-H 'X-Tenant: ${tenant}'")
String me(JQuickCurlReq request);
}
public static void main(String[] args) {
UserApi api = JCurlInvoker.createProxy(UserApi.class);
System.out.println(api.page(new JQuickCurlReq()));
JQuickCurlReq request = new JQuickCurlReq();
request.put("keyword", "java");
request.put("page", 1);
System.out.println(api.search(request));
}
}

JSON is the most common request body. Content-Type can be omitted (it defaults to application/json), but writing it explicitly is clearer. Note that double quotes in JSON must be escaped as \" inside Java strings.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class PostJsonDemo {
public interface OrderApi {
// Static JSON request body
@JCurlCommand("curl -X POST https://api.example.com/v1/orders " +
"-H 'Content-Type: application/json' " +
"-d '{\"sku\":\"A-1001\",\"count\":2,\"remark\":\"first order\"}'")
String create(JQuickCurlReq request);
// Nested structures are likewise written directly in the JSON
@JCurlCommand("curl -X POST https://api.example.com/v1/orders " +
"-H 'Content-Type: application/json' " +
"-d '{\"sku\":\"A-1001\",\"buyer\":{\"id\":1001,\"name\":\"Ada\"},\"tags\":[\"vip\",\"new\"]}'")
String createNested(JQuickCurlReq request);
// The whole request body is supplied by a variable, convenient for generating it via object serialization on the Java side
@JCurlCommand("curl -X POST https://api.example.com/v1/orders " +
"-H 'Content-Type: application/json' " +
"-d '${payload}'")
String createByPayload(JQuickCurlReq request);
// PUT
@JCurlCommand("curl -X PUT https://api.example.com/v1/orders/${id} " +
"-H 'Content-Type: application/json' " +
"-d '{\"count\":5}'")
String update(JQuickCurlReq request);
// PATCH
@JCurlCommand("curl -X PATCH https://api.example.com/v1/orders/${id} " +
"-H 'Content-Type: application/json' " +
"-d '{\"remark\":\"urgent\"}'")
String patch(JQuickCurlReq request);
// DELETE
@JCurlCommand("curl -X DELETE https://api.example.com/v1/orders/${id}")
void remove(JQuickCurlReq request);
}
public static void main(String[] args) {
OrderApi api = JCurlInvoker.createProxy(OrderApi.class);
System.out.println(api.create(new JQuickCurlReq()));
JQuickCurlReq request = new JQuickCurlReq();
request.put("payload", "{\"sku\":\"A-1001\",\"count\":2}");
System.out.println(api.createByPayload(request));
}
}

Generating the request body with Gson (the module brings Gson in transitively, so no extra declaration is needed):

package com.example.curl;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import com.google.gson.Gson;
import java.util.LinkedHashMap;
import java.util.Map;
public class JsonPayloadDemo {
public static void main(String[] args) {
Map<String, Object> order = new LinkedHashMap<>();
order.put("sku", "A-1001");
order.put("count", 2);
order.put("remark", "first order");
JQuickCurlReq request = new JQuickCurlReq();
request.put("payload", new Gson().toJson(order));
OrderApi api = JCurlInvoker.createProxy(OrderApi.class);
System.out.println(api.createByPayload(request));
}
}

POST Form application/x-www-form-urlencoded

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

A form requires explicitly declaring Content-Type: application/x-www-form-urlencoded and joining the key-value pairs into a single piece of text.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class PostFormDemo {
public interface LoginApi {
// Form login
@JCurlCommand("curl -X POST https://api.example.com/v1/login " +
"-H 'Content-Type: application/x-www-form-urlencoded' " +
"-d 'username=ada&password=secret'")
String login(JQuickCurlReq request);
// Form + variables
@JCurlCommand("curl -X POST https://api.example.com/v1/login " +
"-H 'Content-Type: application/x-www-form-urlencoded' " +
"-d 'username=${user}&password=${password}'")
String loginByVariable(JQuickCurlReq request);
}
public static void main(String[] args) {
LoginApi api = JCurlInvoker.createProxy(LoginApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("user", "ada");
request.put("password", System.getenv("LOGIN_PASSWORD"));
System.out.println(api.loginByVariable(request));
}
}

When URL encoding is needed, process it on the Java side before putting it into a variable:

package com.example.curl;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class FormEncodeDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
String keyword = "java http client";
JQuickCurlReq request = new JQuickCurlReq();
request.put("keyword", URLEncoder.encode(keyword, "UTF-8"));
}
}

-F is used for multipart/form-data and can mix ordinary fields with file fields. A field value starting with @ is recognized as a local file path.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class MultipartDemo {
public interface UploadApi {
// Mixed ordinary and file fields
@JCurlCommand("curl -X POST https://api.example.com/v1/users/upload-with-params " +
"-F 'userId=123' " +
"-F 'username=john' " +
"-F 'file=@D:/data/report.xlsx'")
String uploadWithFields(JQuickCurlReq request);
// Multiple files uploaded under the same name
@JCurlCommand("curl -X POST https://api.example.com/v1/users/upload-multiple " +
"-F 'files=@D:/data/part1.xlsx' " +
"-F 'files=@D:/data/part2.xlsx'")
String uploadMultiple(JQuickCurlReq request);
// Ordinary field (not a file)
@JCurlCommand("curl -X POST https://api.example.com/v1/feedback " +
"-F 'title=bug report' " +
"-F 'content=cannot upload'")
String feedback(JQuickCurlReq request);
}
public static void main(String[] args) {
UploadApi api = JCurlInvoker.createProxy(UploadApi.class);
System.out.println(api.uploadWithFields(new JQuickCurlReq()));
}
}

Handling rules for -F:

Form Recognized as How it is sent
-F 'file=@/path/to/file' File field Sent with application/octet-stream as the file content; the file name is taken from the local file name.
-F 'key=value' Ordinary field Sent with application/json as the part’s content type.

Uploads use -F, and paths support both absolute and relative paths (relative to the JVM working directory). Prefer writing the path as a variable to avoid hard-coding environment-specific paths into the source.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class UploadDemo {
public interface FileApi {
// Single file upload
@JCurlCommand("curl -X POST https://api.example.com/v1/files " +
"-F 'file=@${path}'")
String upload(JQuickCurlReq request);
// Upload with authentication
@JCurlCommand("curl -X POST https://api.example.com/v1/files " +
"-H 'Authorization: Bearer ${token}' " +
"-F 'file=@${path}'")
String uploadWithAuth(JQuickCurlReq request);
}
public static void main(String[] args) {
FileApi api = JCurlInvoker.createProxy(FileApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("path", "D:/data/report.xlsx");
request.put("token", System.getenv("API_TOKEN"));
System.out.println(api.uploadWithAuth(request));
}
}

When a file must be generated dynamically, write it to disk first and then upload:

package com.example.curl;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public class DynamicUploadDemo {
public static void main(String[] args) throws Exception {
// 1. Generate a temporary file
File temp = File.createTempFile("report-", ".json");
Files.write(temp.toPath(), "{\"ok\":true}".getBytes(StandardCharsets.UTF_8));
// 2. Put the path into a variable and upload
JQuickCurlReq request = new JQuickCurlReq();
request.put("path", temp.getAbsolutePath());
FileApi api = JCurlInvoker.createProxy(FileApi.class);
System.out.println(api.upload(request));
// 3. Clean up
temp.delete();
}
}

Use -o / --output to specify the local save path. The download is completed inside the module: the response bytes are written to the target file, and the method’s return value also contains the response content.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class DownloadDemo {
public interface FileApi {
// Download and write to disk; the method returns the response bytes
@JCurlCommand("curl -X GET https://api.example.com/v1/files/pie.xlsx " +
"-H 'Accept: application/octet-stream' " +
"--output 'D:/download/pie.xlsx'")
byte[] download(JQuickCurlReq request);
// The path is supplied by a variable
@JCurlCommand("curl -X GET ${fileUrl} --output '${targetPath}'")
byte[] downloadByVariable(JQuickCurlReq request);
}
public static void main(String[] args) {
FileApi api = JCurlInvoker.createProxy(FileApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("fileUrl", "https://api.example.com/v1/files/pie.xlsx");
request.put("targetPath", "D:/download/pie.xlsx");
byte[] bytes = api.downloadByVariable(request);
System.out.println("downloaded bytes = " + bytes.length);
}
}

When you don’t need to write to disk, declare the return type as byte[], or return JQuickCurlResponseBody and call getCachedBytes().

package com.example.curl;
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.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
public class InMemoryDownloadDemo {
public interface FileApi {
// Return the byte array directly
@JCurlCommand("curl -X GET https://api.example.com/v1/files/logo.png")
byte[] bytes(JQuickCurlReq request);
// When you also need the response headers
@JCurlCommand("curl -X GET https://api.example.com/v1/files/logo.png")
JQuickCurlResponseBody raw(JQuickCurlReq request);
}
public static void main(String[] args) throws Exception {
FileApi api = JCurlInvoker.createProxy(FileApi.class);
JQuickCurlReq request = new JQuickCurlReq();
// Option 1: get the bytes directly
byte[] data = api.bytes(request);
System.out.println("size = " + data.length);
// Option 2: read the response headers first, then get the bytes
JQuickCurlResponseBody response = api.raw(request);
String contentType = response.header("Content-Type");
byte[] cached = response.getCachedBytes();
// Use in memory: for example, write to a local file
File target = new File("D:/download/logo.png");
try (OutputStream out = new FileOutputStream(target)) {
out.write(cached);
}
System.out.println(contentType + ", bytes = " + cached.length);
// Or convert it into an input stream and hand it to another component
ByteArrayInputStream in = (ByteArrayInputStream) response.asInputStream();
System.out.println("available = " + in.available());
// Or write it out in one go: Files.write(target.toPath(), cached);
}
}

-u / --user Base64-encodes the credentials and writes them into the Authorization: Basic ... request header. Credentials can be quoted or unquoted, and variables are supported as well.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class BasicAuthDemo {
public interface AuthApi {
// Write the credentials inline (recommended only for local debugging)
@JCurlCommand("curl -X GET https://api.example.com/v1/me -u ada:secret")
String me(JQuickCurlReq request);
// Quoted
@JCurlCommand("curl -X GET https://api.example.com/v1/me -u 'ada:P@ssw0rd!'")
String meQuoted(JQuickCurlReq request);
// Credentials from variables (recommended)
@JCurlCommand("curl -X GET https://api.example.com/v1/me -u '${user}:${password}'")
String meByVariable(JQuickCurlReq request);
}
public static void main(String[] args) {
AuthApi api = JCurlInvoker.createProxy(AuthApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("user", "ada");
request.put("password", System.getenv("API_PASSWORD"));
System.out.println(api.meByVariable(request));
}
}

Bearer authentication writes the Authorization header through -H, which is the most general approach.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class BearerAuthDemo {
public interface AuthApi {
@JCurlCommand("curl -X GET https://api.example.com/v1/profile " +
"-H 'Authorization: Bearer ${token}' " +
"-H 'Accept: application/json'")
String profile(JQuickCurlReq request);
// OAuth2 client_credentials flow: exchange for a token first, then call
@JCurlCommand("curl -X POST https://api.example.com/oauth/token " +
"-H 'Content-Type: application/x-www-form-urlencoded' " +
"-d 'grant_type=client_credentials&client_id=${clientId}&client_secret=${clientSecret}'")
String token(JQuickCurlReq request);
}
public static void main(String[] args) {
AuthApi api = JCurlInvoker.createProxy(AuthApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("token", System.getenv("API_TOKEN"));
System.out.println(api.profile(request));
}
}

You can also inject it centrally with a global interceptor, so that business code does not have to write -H on every command:

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import okhttp3.Interceptor;
public class BearerInterceptorDemo {
public static void main(String[] args) {
Interceptor bearer = chain -> chain.proceed(
chain.request().newBuilder()
.header("Authorization", "Bearer " + System.getenv("API_TOKEN"))
.build());
JQuickCurlConfig.getInstance().addInterceptor(bearer);
}
}

Some platforms use proprietary authentication headers, for example X-Api-Key, X-Sign or X-Timestamp. Simply add them with -H; for signature-style headers it is better to compute them centrally in an interceptor.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class CustomAuthDemo {
public interface PlatformApi {
// Proprietary key headers
@JCurlCommand("curl -X GET https://api.example.com/v1/orders " +
"-H 'X-Api-Key: ${apiKey}' " +
"-H 'X-Trace-Id: ${traceId}'")
String orders(JQuickCurlReq request);
// Two-token scenario
@JCurlCommand("curl -X POST https://api.example.com/v1/pay " +
"-H 'X-App-Id: ${appId}' " +
"-H 'X-App-Secret: ${appSecret}' " +
"-H 'Content-Type: application/json' " +
"-d '{\"amount\":100}'")
String pay(JQuickCurlReq request);
}
public static void main(String[] args) {
PlatformApi api = JCurlInvoker.createProxy(PlatformApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("apiKey", System.getenv("PLATFORM_API_KEY"));
request.put("traceId", java.util.UUID.randomUUID().toString());
System.out.println(api.orders(request));
}
}

Signature headers generated centrally by an interceptor:

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class SignInterceptorDemo {
/**
* Append signature headers before the request is sent, so business commands do not need to declare them repeatedly.
*/
public static void main(String[] args) {
Interceptor signer = chain -> {
Request request = chain.request();
String timestamp = String.valueOf(System.currentTimeMillis());
String raw = request.url().encodedPath() + "|" + timestamp + "|" + System.getenv("APP_SECRET");
Request signed = request.newBuilder()
.header("X-Timestamp", timestamp)
.header("X-Sign", md5(raw))
.build();
return chain.proceed(signed);
};
JQuickCurlConfig.getInstance().addInterceptor(signer);
}
private static String md5(String text) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(text.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new IllegalStateException("md5 failed", e);
}
}
}

Ignoring SSL certificate validation (for development debugging only, forbidden in production)

Section titled “Ignoring SSL certificate validation (for development debugging only, forbidden in production)”

Writing -k or --insecure in a command turns on the “ignore certificate validation” switch: the parser marks the current request as “skip certificate validation” and therefore takes the OkHttp build branch that ignores validation.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class InsecureDemo {
public interface DevApi {
// A test environment using a self-signed certificate
@JCurlCommand("curl -X GET -k https://self-signed.example.com/v1/ping")
String ping(JQuickCurlReq request);
// Equivalent form
@JCurlCommand("curl -X GET --insecure https://self-signed.example.com/v1/ping")
String pingByLongOption(JQuickCurlReq request);
}
public static void main(String[] args) {
DevApi api = JCurlInvoker.createProxy(DevApi.class);
System.out.println(api.ping(new JQuickCurlReq()));
}
}

Use -x / --proxy to specify the proxy address in host:port form. When the port is omitted it defaults to 80.

package com.example.curl;
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public class ProxyDemo {
public interface ProxyApi {
// Short option
@JCurlCommand("curl -X GET -x 127.0.0.1:7890 https://api.example.com/v1/ip")
String viaProxyShort(JQuickCurlReq request);
// Long option
@JCurlCommand("curl -X GET --proxy 127.0.0.1:7890 https://api.example.com/v1/ip")
String viaProxyLong(JQuickCurlReq request);
// Proxy address supplied by a variable
@JCurlCommand("curl -X GET -x ${proxyHost} https://api.example.com/v1/ip")
String viaProxyVariable(JQuickCurlReq request);
}
public static void main(String[] args) {
ProxyApi api = JCurlInvoker.createProxy(ProxyApi.class);
JQuickCurlReq request = new JQuickCurlReq();
request.put("proxyHost", "127.0.0.1:7890");
System.out.println(api.viaProxyVariable(request));
}
}
Option Parameter Default Description
-x host:port Port defaults to 80 Use an HTTP proxy.
--proxy host:port Port defaults to 80 The same, written as a long option.
--socks5-hostname host:port Port defaults to 80 It can be parsed, but the proxy type is pinned to HTTP, so it is effectively the same as -x.

If the proxy only requires the standard Proxy-Authorization header, you can add it with a global interceptor:

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import okhttp3.Credentials;
import okhttp3.Interceptor;
public class ProxyAuthDemo {
public static void main(String[] args) {
String proxyUser = System.getenv("PROXY_USER");
String proxyPassword = System.getenv("PROXY_PASSWORD");
Interceptor proxyAuth = chain -> chain.proceed(
chain.request().newBuilder()
.header("Proxy-Authorization", Credentials.basic(proxyUser, proxyPassword))
.build());
JQuickCurlConfig.getInstance().addInterceptor(proxyAuth);
}
}

All timeouts are set on the global JQuickCurlConfig, and the unit is determined by timeUnit (milliseconds by default).

Config item Type Default Description
connectTimeout long 1000000000 Connection establishment timeout; the default is close to “no timeout”, so it must be set explicitly in production.
readTimeout long 1000000000 Read timeout.
writeTimeout long 1000000000 Write timeout.
callTimeout long 1000000000 Total timeout for the whole call.
timeUnit TimeUnit MILLISECONDS Time unit for the four items above.
retryOnConnectionFailure Boolean true OkHttp retries automatically when a connection fails.
maxRetryCount int 3 Business-level retry count; see “Request retry configuration” below.
followRedirects Boolean true Whether to follow redirects.
followSslRedirects Boolean true Whether to follow HTTPS→HTTP redirects.
package com.example.curl;
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 java.util.concurrent.TimeUnit;
public class TimeoutDemo {
public interface SlowApi {
@JCurlCommand("curl -X GET https://api.example.com/v1/slow-report")
String report(JQuickCurlReq request);
}
public static void main(String[] args) {
// Global settings: connect 3 s, read 30 s, write 30 s
JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS);
SlowApi api = JCurlInvoker.createProxy(SlowApi.class);
System.out.println(api.report(new JQuickCurlReq()));
}
}

How a method-level setting relates to the global one:

Setting Scope Description
JQuickCurlConfig.getInstance() Global A single instance shared by all requests (the constructor is private, so the configuration itself is a singleton).
@JTimeout A single method Overrides the global timeout; only effective in proxy mode, and all three values must be supplied together.

followRedirects defaults to true; writing -L / --location in a command sets it explicitly to true.

package com.example.curl;
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;
public class RedirectDemo {
public interface RedirectApi {
// -L: follow redirects
@JCurlCommand("curl -X GET -L https://api.example.com/v1/short-link")
String follow(JQuickCurlReq request);
// Long-option form
@JCurlCommand("curl -X GET --location https://api.example.com/v1/short-link")
String followByLongOption(JQuickCurlReq request);
}
public static void main(String[] args) {
// Disable redirect-following globally; -L in the command turns it back on
JQuickCurlConfig.getInstance().followRedirects(false);
RedirectApi api = JCurlInvoker.createProxy(RedirectApi.class);
System.out.println(api.follow(new JQuickCurlReq()));
}
}

There are two retry layers with different scopes:

Layer Config item Default Trigger Scope
Connection layer retryOnConnectionFailure true OkHttp decides the connection can be retried (for example, connection reuse fails) All requests
Business layer maxRetryCount 3 The request execution throws an exception Only JCurlInvoker.createProxy proxy mode

The actual number of requests performed by business-layer retries is maxRetryCount + 1: with the default configuration it attempts at most 4 times.

package com.example.curl;
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;
public class RetryDemo {
public interface FlakyApi {
@JCurlCommand("curl -X GET https://api.example.com/v1/unstable")
String unstable(JQuickCurlReq request);
}
public static void main(String[] args) {
// Retry at most 2 times (at most 3 requests in total) and let OkHttp retry automatically on connection failure
JQuickCurlConfig.getInstance()
.maxRetryCount(2)
.retryOnConnectionFailure(true);
FlakyApi api = JCurlInvoker.createProxy(FlakyApi.class);
System.out.println(api.unstable(new JQuickCurlReq()));
}
}

Spring Boot configuration file (application.yml)

Section titled “Spring Boot configuration file (application.yml)”

Bind the YAML with @ConfigurationProperties, then apply it to the global configuration inside a @Bean:

jquick:
curl:
connect-timeout: 3000
read-timeout: 10000
write-timeout: 10000
max-retry-count: 2
follow-redirects: true
pool:
max-idle-connections: 50
keep-alive-duration: 300000
package com.example.curl;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Binds jquick.curl.* from application.yml to this object.
*/
@Component
@ConfigurationProperties(prefix = "jquick.curl")
public class CurlProperties {
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();
public static class Pool {
private int maxIdleConnections = 50;
private long keepAliveDuration = 300000;
public int getMaxIdleConnections() {
return maxIdleConnections;
}
public void setMaxIdleConnections(int maxIdleConnections) {
this.maxIdleConnections = maxIdleConnections;
}
public long getKeepAliveDuration() {
return keepAliveDuration;
}
public void setKeepAliveDuration(long keepAliveDuration) {
this.keepAliveDuration = keepAliveDuration;
}
}
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;
}
}
package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CurlPoolConfiguration {
@Bean
public JQuickCurlConfig jquickCurlConfig(CurlProperties properties) {
return JQuickCurlConfig.getInstance()
.connectTimeout(properties.getConnectTimeout(), TimeUnit.MILLISECONDS)
.readTimeout(properties.getReadTimeout(), TimeUnit.MILLISECONDS)
.writeTimeout(properties.getWriteTimeout(), TimeUnit.MILLISECONDS)
.connectionPool(
properties.getPool().getMaxIdleConnections(),
properties.getPool().getKeepAliveDuration(),
TimeUnit.MILLISECONDS)
.maxRetryCount(properties.getMaxRetryCount())
.followRedirects(properties.isFollowRedirects());
}
}

Configuring connection pool parameters in code

Section titled “Configuring connection pool parameters in code”

Connection pool parameters are set in a single call to connectionPool(maxIdleConnections, keepAliveDuration, unit).

Parameter Type Default Description
maxIdleConnections int 1000000000 Maximum number of idle connections to keep; corresponds to the first argument of OkHttp’s ConnectionPool.
keepAliveDuration long 1000000000 How long idle connections live.
unit TimeUnit MILLISECONDS Unit for the duration.
package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class ConnectionPoolDemo {
public static void main(String[] args) {
JQuickCurlConfig.getInstance()
// Keep at most 50 idle connections, evicted after 5 minutes idle
.connectionPool(50, 5, TimeUnit.MINUTES)
// Timeouts are worth setting at the same time
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS);
System.out.println("pool configured");
}
}

The module sends requests through OkHttp, so interceptors follow OkHttp’s okhttp3.Interceptor contract, and existing OkHttp interceptors from the community can be reused directly.

JQuickCurlConfig provides two registration methods, both returning itself so they can be chained:

Method Parameter type Description
addInterceptor(Interceptor) okhttp3.Interceptor Registers an application interceptor. It runs only once per request and can read the request body and response body; suitable for signing, header injection and unified logging.
addNetworkInterceptor(Interceptor) okhttp3.Interceptor Registers a network interceptor. It stays closer to the real connection and runs again on redirects and connection retries; suitable for observing protocol version, connection reuse and so on.
import com.github.paohaijiao.config.JQuickCurlConfig;
JQuickCurlConfig config = JQuickCurlConfig.getInstance();
config.addInterceptor(new MySignInterceptor())
.addNetworkInterceptor(new MyNetworkInterceptor())
.build();

The configuration is read every time an OkHttpClient is built for a request, so registration takes effect immediately for all subsequent requests, without rebuilding the invoker or proxy objects.

The constructor of JQuickCurlConfig registers a JLoggingInterceptor by default; its behaviour is:

  • print the duration of a single request, in the form the request cost : 128 ms,
  • print the exception stack at error level when a request fails

It does not print the URL, request headers, request body or response body, so it is only useful for rough timing observation and cannot replace full HTTP debug logging. The constructor also provides a JLoggingInterceptor(JCurlLevelLog level) overload, but the current implementation never uses that level parameter.

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.executor.JCurlInvoker;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.UUID;
public class InterceptorDemo {
public static void main(String[] args) {
// 1. Register the interceptors (on the global singleton, effective for subsequent requests immediately)
JQuickCurlConfig.getInstance()
.addInterceptor(new TraceIdInterceptor())
.addNetworkInterceptor(new ProtocolInterceptor());
// 2. Make a normal call; the interceptors take part automatically
GitHubApi api = JCurlInvoker.createProxy(GitHubApi.class);
System.out.println(api.user("paohaijiao"));
}
/** Application interceptor: inject a tracing header into every request */
static class TraceIdInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.header("X-Trace-Id", UUID.randomUUID().toString())
.build();
return chain.proceed(request);
}
}
/** Network interceptor: observe the protocol version of the real connection */
static class ProtocolInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
System.out.println("protocol = " + response.protocol() + ", code = " + response.code());
return response;
}
}
}

In the current implementation, building the OkHttpClient for an ordinary request works like this: config.getInterceptors() is added to the base client, then newBuilder() is called and they are appended once more. The result is:

Interceptor source Actual number of executions
Custom interceptors registered via addInterceptor(...) 2
The built-in JLoggingInterceptor 3
Network interceptors registered via addNetworkInterceptor(...) 1

The module does not define its own exception types; exceptions come from three places: parameter assertions, the grammar parsing layer, and the underlying OkHttp.

Exception Type Trigger Swallowed by the module
IllegalArgumentException RuntimeException Missing -X, invalid URL, failure to parse -u credentials, a placeholder value that is null No, it is thrown to the caller
JAntlrExecutionException RuntimeException The curl command does not conform to the lexical/grammar rules Yes, caught, printed to System.err and null is returned
java.io.IOException Checked exception Connect timeout, read/write timeout, DNS failure, connection refused Yes, caught at the request execution site, the stack is printed, and null is returned
Other runtime exceptions RuntimeException Internal errors in the reflection/proxy call chain No, thrown upwards

JAntlrExecutionException carries a List<JAntlrError> errors, and each error contains a line number, a column number and the rule stack, which can be used to build custom error messages:

package com.example.curl;
import com.github.paohaijiao.exception.JAntlrExecutionException;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.executor.JQuickCurlExecutor;
import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
public class ParseErrorDemo {
public static void main(String[] args) {
JQuickCurlExecutor executor = new JQuickCurlExecutor(new JContext(), JQuickCurlConfig.getInstance());
try {
JQuickCurlResponseBody body = executor.execute("curl -X GET https://api.github.com/users/paohaijiao");
System.out.println(body.asString());
} catch (JAntlrExecutionException e) {
System.err.println("Parse failed: " + e.getMessage());
e.getErrors().forEach(error -> System.err.println(
" line " + error.getLine() + ":" + error.getCharPosition() + " -> " + error.getMessage()));
}
}
}

Both parsing failures and network failures surface as a null return

Section titled “Both parsing failures and network failures surface as a null return”

This is the easiest trap when using the module: neither kind of “failure” throws an exception.

  • Annotation-style calls: JCurlCommandProcessor.process(...) catches JAntlrExecutionException, prints Parse Fail: ... to the standard error stream, and then return null.
  • Network failures: at the request execution site catch (IOException e) { e.printStackTrace(); } runs, and then return null.

As a result, the following style hides problems:

// Not recommended: on a parse failure or a network error, user is null either way, so the cause cannot be told apart
User user = gitHubApi.user("paohaijiao");

It is better to check and wrap centrally in the business layer:

package com.example.curl;
import com.github.paohaijiao.executor.JCurlInvoker;
public class SafeCallDemo {
public static void main(String[] args) {
GitHubApi api = JCurlInvoker.createProxy(GitHubApi.class);
try {
User user = api.user("paohaijiao");
if (user == null) {
// It could be a parse failure or a network failure; the stack is already in the logs
throw new IllegalStateException("GitHub API call failed; check the logs for a Parse Fail message or an IOException stack");
}
System.out.println(user.getLogin());
} catch (IllegalArgumentException e) {
// The command itself is wrong (for example, a missing -X)
throw new IllegalStateException("Invalid curl command: " + e.getMessage(), e);
}
}
}

Status code and business code validation is not enabled yet

Section titled “Status code and business code validation is not enabled yet”

@JCurlCommand declares four attributes—expectedStatus, expectedBusinessStatus, validationScript and execute—and none of them take effect in the current implementation:

  • a non-2xx response does not throw; it only prints a single line Cannot request the resource and then returns the response body as usual;
  • validateStatusCode(...) is a private method that is never called;
  • expectedBusinessStatus, validationScript and execute are not referenced anywhere in the source.

When you need to validate status codes, write an interceptor or switch to a programmatic call combined with OkHttp’s Response.code():

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
public class StatusCheckInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
if (!response.isSuccessful()) {
throw new IOException("HTTP " + response.code() + " for " + response.request().url());
}
return response;
}
public static void main(String[] args) {
JQuickCurlConfig.getInstance().addNetworkInterceptor(new StatusCheckInterceptor());
}
}

The module’s parsing pipeline has two layers, both of which are extensible:

Layer Responsibility Extension
curl grammar layer Parse a curl command string into an HTTP request Built-in grammar engine + Visitor; extend JQuickCurlCommonVisitor / JQuickCurlExecutor and override
XML description layer Map interfaces configured in XML to proxy methods Implement JQuickParseHandler (the built-in implementation is JQuickCurlXmlParseFactory)

XML protocol extension: implementing JQuickParseHandler

Section titled “XML protocol extension: implementing JQuickParseHandler”

JQuickParseHandler comes from jquick-xmlProxy and is responsible for describing “what the XML looks like” and “how nodes become calls”:

Method Return type Responsibility
createJQuickXmlElement() JQuickXmlElement Describes the XML structure: the namespace attribute name, the root tag, the child tag, the method name attribute, the return type attribute and the parameter type attribute
createlInvocationHandler() JQuickXmlInvocationHandler Provides the actual invocation handler, that is, wrapping node content into an HTTP request and converting the return value

The module’s default conventions are: root tag curls, child tag curl, namespace attribute namespace, method name attribute name, return type attribute returnClass, parameter type attribute paramClass. If your team’s XML convention differs, you can subclass the built-in element class and rename:

package com.example.curl;
import com.github.paohaijiao.xml.JQuickCurlXmlElement;
import com.github.paohaijiao.xml.JQuickCurlXmlInvocationHandler;
import com.github.paohaijiao.xml.element.JQuickXmlElement;
import com.github.paohaijiao.xml.factory.JQuickXmlFactory;
import com.github.paohaijiao.xml.handler.JQuickParseHandler;
import com.github.paohaijiao.xml.invocation.JQuickXmlInvocationHandler;
import java.util.Arrays;
import java.util.List;
public class CustomXmlProtocolDemo {
/** Custom XML structs: change the root tag to apis and the child tag to api */
static class ApiXmlElement extends JQuickCurlXmlElement {
@Override
public String getRootElementTagName() {
return "apis";
}
@Override
public List<String> getChildElementTagName() {
return Arrays.asList("api");
}
}
/** Custom parse handler */
static class ApiParseHandler implements JQuickParseHandler {
@Override
public JQuickXmlElement createJQuickXmlElement() {
return new ApiXmlElement();
}
@Override
public JQuickXmlInvocationHandler createlInvocationHandler() {
return new JQuickCurlXmlInvocationHandler();
}
}
public static void main(String[] args) {
// apis.xml goes in the classpath root; the custom handler decides the tag conventions
JQuickXmlFactory factory = new JQuickXmlFactory(new ApiParseHandler(), "apis.xml");
GitHubApi api = factory.createApi(GitHubApi.class);
System.out.println(api.user("paohaijiao"));
}
}

The constructor argument of JQuickXmlFactory is a classpath resource path (read internally through getResourceAsStream); when the file is not found it throws IllegalArgumentException("Resource not found in classpath: xxx").

Grammar-layer extension: overriding the Visitor and the Executor

Section titled “Grammar-layer extension: overriding the Visitor and the Executor”

If you need to observe or rewrite how curl commands are parsed, you can extend JQuickCurlCommonVisitor and override the various visitXxxOption methods; then write a custom JQuickCurlExecutor that overrides parse(...) to swap the default visitor for your own implementation.

package com.example.curl;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.exception.JAntlrExecutionException;
import com.github.paohaijiao.executor.JQuickCurlExecutor;
import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.parser.JQuickCurlParser;
import com.github.paohaijiao.responseBody.JQuickCurlResponseBody;
import com.github.paohaijiao.visitor.JQuickCurlCommonVisitor;
import org.antlr.v4.runtime.Parser;
public class CustomVisitorDemo {
/** Custom visitor: log while walking each option */
static class TracingVisitor extends JQuickCurlCommonVisitor {
public TracingVisitor(JContext context, JQuickCurlConfig config) {
super(context, config);
}
@Override
public Object visitOption(JQuickCurlParser.OptionContext ctx) {
System.out.println("option => " + ctx.getText());
return super.visitOption(ctx);
}
}
/** Custom executor: replace the default implementation with a custom visitor */
static class TracingExecutor extends JQuickCurlExecutor {
private final JContext context;
private final JQuickCurlConfig config;
TracingExecutor(JContext context, JQuickCurlConfig config) {
super(context, config);
this.context = context;
this.config = config;
}
@Override
protected JQuickCurlResponseBody parse(Parser parser) throws JAntlrExecutionException {
JQuickCurlParser curlParser = (JQuickCurlParser) parser;
TracingVisitor visitor = new TracingVisitor(this.context, this.config);
return visitor.visitCurlCommand(curlParser.curlCommand());
}
}
public static void main(String[] args) {
TracingExecutor executor = new TracingExecutor(new JContext(), JQuickCurlConfig.getInstance());
JQuickCurlResponseBody body = executor.execute(
"curl -X GET -H \"Accept: application/json\" https://api.github.com/users/paohaijiao");
System.out.println(body.asString());
}
}

Key visitor methods you can override:

Method What it handles
visitCurlCommand(...) The assembly entry point for the whole command
visitOption(...) Dispatching of a single option
visitRequestMethod(...) / visitUrl(...) -X and the target address
visitHeaderOption(...) -H request headers (including Content-Type recognition)
visitDataOption(...) / visitDataUrlEncodeOption(...) / visitFormData(...) -d, --data-urlencode, -F
visitUserOption(...) / visitCredentials(...) -u authentication
visitUploadOption(...) / visitDownloadOption(...) Upload and -o download
visitProxryOption(...) / visitSocketOption(...) -x and --socks5-hostname
visitLocationOption(...) / visitIgnoreOption(...) -L and -k
visitVariable(...) ${...} placeholder resolution

The table below lists, one by one, the lexical units the parser actually supports. ✅ means available, ⚠️ means parseable but with incomplete behaviour, and ❌ means that writing it causes a parse failure or has no effect at all.

Option Status Behaviour
curl ✅ The command’s starting keyword; it must be written.
-X / --request ✅ required Specifies the HTTP method; supports GET POST PUT DELETE PATCH HEAD OPTIONS TRACE CONNECT. Omitting it throws IllegalArgumentException straight away.
-H / --header ✅ Request headers, repeatable; a header with the same name is appended. Content-Type is recognised separately and used as the request body’s media type.
-d / --data / --data-ascii / --data-binary / --data-raw ✅ Request body; the five forms are equivalent. With several -d options the later one overwrites the earlier one instead of concatenating. When -d is absent the request body is the literal {}.
--data-urlencode ⚠️ It is parsed and it switches Content-Type to a form type, but the value is not written into the request body.
-F / --form ✅ multipart form, repeatable. When the value is key=@file-path it is handled as a file upload. When -F is present the request is forced to POST.
-u / --user ✅ Basic Auth, in username:password form.
-b / --cookie ✅ A value starting with @ is treated as a cookie file (Netscape format); otherwise it is parsed as k=v; k2=v2 into request headers.
-c / --cookie-jar ✅ Writes the response’s Set-Cookie to a file in Netscape format.
-o / --output ✅ Writes the response body to the given file name.
-x / --proxy ✅ HTTP proxy in host:port form; when only host is given the port defaults to 80.
--socks5-hostname ⚠️ It can be parsed, but the internal proxy type is hard-coded to HTTP, so it does not go through SOCKS.
--http2 ❌ It can be parsed and builds a client that declares HTTP/2, but that client is never used, and the actual protocol is still negotiated by OkHttp itself.
-k / --insecure ❌ It can be parsed and flips an internal switch, but the SSLContext is never wired into the client, so it does not actually skip certificate validation.
-L / --location ✅ Sets the global followRedirects to true.
--max-redirs ❌ It is lexically recognisable, but the grammar rule has no matching alternative branch, so writing it in a command makes parsing fail.
-v / --verbose, -s / --silent ❌ no-op They can be parsed, but have no behaviour at all.
${ } ✅ Placeholder syntax; see the “Placeholder variables” section above for details.

The following options are outside the parsing scope; writing them in a command causes a parse failure. Implement them in code or in an interceptor instead:

Category Unsupported options
Timeouts -m / --max-time, --connect-timeout
Retries --retry, --retry-delay, --retry-max-time
Transfer control --compressed, --limit-rate, -G / --get, -T / --upload-file
Certificates and TLS --cacert, --cert, --key, --capath, --tlsv1.2
Proxy --proxy-user, --noproxy
Header shortcuts -A / --user-agent, -e / --referer
Output control -i / --include, -I / --head, -D / --dump-header, -w / --write-out
Other --resolve, --interface, --url, --json, -4 / -6, --http1.1, -S / --show-error, -f / --fail

This chapter is JQuick-Curl Syntax Demos. Every section demonstrates exactly one syntax element — chained annotations, command parameters and request construction — so you can look up a specific syntax point quickly. All examples use declarative @JCurlCommand; the core idea is: the annotation value is a complete curl command, and each method is one callable API.

The @JCurlCommand value is a complete curl command that must start with curl and declare the method explicitly with -X. JCurlInvoker.createProxy compiles the interface into a dynamic-proxy client; the method return type decides how the response is parsed, where String means the raw response body text.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface BasicApi {
// Syntax: curl -X <METHOD> <URL>
// -X declares the method; the return type is the parsing target
@JCurlCommand("curl -X GET https://httpbin.org/get")
String get(JQuickCurlReq request);
}
class BasicDemo {
public static void main(String[] args) throws Exception {
// Syntax: interface -> proxy client
BasicApi api = JCurlInvoker.createProxy(BasicApi.class);
// Every method declares at least one JQuickCurlReq parameter
System.out.println(api.get(new JQuickCurlReq()));
}
}

Add request headers with -H 'Name: value' (--header is equivalent). One command may repeat -H several times; headers with the same name are merged rather than overwritten. Headers are the content-type foundation for every body syntax below.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface HeaderApi {
// Syntax: -H 'Name: value', repeatable
@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()));
}
}

Query parameters are written inline in the URL as ?key=value&key2=value2. Both names and values may use ${var} placeholders, replaced as plain text from the method parameters before parsing; substituted values are not URL-encoded automatically, so encode special characters on the Java side first.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface QueryApi {
// Syntax: ?k=v&k2=v2 in the URL, fixed values
@JCurlCommand("curl -X GET 'https://httpbin.org/anything?category=book&page=1'")
String fixed(JQuickCurlReq request);
// Syntax: ?k=${var}, replaced from method parameters
@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));
}
}

Declare path variables with ${name} inside URL path segments; the matching method parameter replaces them at call time. The mechanism relies on parameter names being kept at compile time, so a plain Maven project must enable -parameters for maven-compiler-plugin.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface PathApi {
// Syntax: ${id} in the path is replaced by the same-named parameter
@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"));
}
}

Send a raw text body with -d / --data / --data-raw, and declare the content type with -H 'Content-Type: text/plain'. -d only carries the payload; it performs no serialization.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface RawApi {
// Syntax: -H 'Content-Type: text/plain' + -d 'raw text'
@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()));
}
}

The JSON body syntax is -H 'Content-Type: application/json' combined with -d '{...}'. The JSON literal is wrapped in single quotes with double quotes inside; the method return type may be a custom POJO and is deserialized by the result converter.

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 {
// Syntax: -H 'Content-Type: application/json' + -d '{json}'
// The POJO return type is deserialized automatically
@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()));
}
}

Declare multipart fields with -F: -F 'field=value' is a plain form field, -F 'field=@path' is a file field. Both kinds may be mixed in one command; prefer forward slashes in file paths, and repeat -F for multiple files.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface MultipartApi {
// Syntax: -F 'field=value' plain field + -F 'file=@path' file field
@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()));
}
}

Timeouts are configured through chainable methods on the global singleton JQuickCurlConfig, all with the signature (long timeout, TimeUnit unit), covering connect, read and write. JQuickCurlConfig is a global singleton, so configure it once at application start-up; per-method overrides use @JTimeout, whose unit is always milliseconds.

import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class TimeoutSyntaxDemo {
// Syntax: JQuickCurlConfig.getInstance().<xxx>Timeout(long, TimeUnit)
public static void init() {
JQuickCurlConfig.getInstance()
.connectTimeout(3, TimeUnit.SECONDS) // connect timeout
.readTimeout(10, TimeUnit.SECONDS) // read timeout
.writeTimeout(10, TimeUnit.SECONDS); // write timeout
}
}
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.anno.JTimeout;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface TimeoutApi {
// Syntax: @JTimeout overrides global timeouts; all three values are milliseconds
@JTimeout(connect = 3000, read = 10000, write = 10000)
@JCurlCommand("curl -X GET https://httpbin.org/delay/1")
String call(JQuickCurlReq request);
}

Declare a proxy with -x or --proxy as host:port, without an http:// scheme prefix. The 2.6.0 proxy model only carries host, port and type and does not support authentication options such as --proxy-user; for proxy authentication, add the Basic credentials manually with -H 'Proxy-Authorization: ...'.

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 {
// Syntax: -x host:port (no scheme prefix) + Proxy-Authorization header
@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));
}
}

JQuick-Curl exposes no dedicated HTTP/2 switch. Its transport is OkHttp, which over HTTPS negotiates HTTP/2 automatically through ALPN whenever the server supports it, so no explicit option is needed in the curl command; curl’s native --http2 / --http1.1 options are not implemented in the 2.6.0 grammar. At the syntax level you simply issue a normal HTTPS request.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface Http2Api {
// Syntax: a normal HTTPS request is enough; OkHttp negotiates HTTP/2 via ALPN
// No --http2 needed; forcing a protocol list requires extending 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()));
}
}

Skip certificate validation with -k or --insecure; the framework then uses an X509TrustManager that trusts every certificate — development debugging only, never in production. For a custom trust store or mutual-TLS client certificate, 2.6.0 offers no in-app programming API; the standard approach is to point the JVM system properties at the trust store and key store.

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface SslApi {
// Syntax: -k / --insecure skips chain validation (development only)
@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()));
}
}

For mutual TLS, pass the key store and trust store through JVM arguments (-Djavax.net.ssl.keyStore, -Djavax.net.ssl.trustStore and their passwords); the curl command stays unchanged.

A synchronous response is shaped by the method return type: returning String yields the body text, while returning JQuickCurlResponseBody additionally exposes headers and the byte stream. The usual readers are asString(), header(name), headers(name) and asInputStream(). The call blocks until the response has been fully read.

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 {
// Syntax: returning String yields the body text
@JCurlCommand("curl -X GET https://httpbin.org/get")
String raw(JQuickCurlReq request);
// Syntax: returning JQuickCurlResponseBody exposes headers and the stream
@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"); // first matching header
List<String> all = response.headers("Set-Cookie"); // all matching headers
String body = response.asString(); // body text
System.out.println(first);
System.out.println(all);
System.out.println(body);
}
}

JQuick-Curl proxy calls are synchronous and blocking; 2.6.0 provides no built-in async API or callback interface (JFunction is a Function wrapper used for method-reference resolution, not an async callback). For async behaviour, wrap the synchronous call with CompletableFuture on the caller side to gain concurrency and callback-style composition.

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);
// Syntax: wrap the synchronous proxy call with CompletableFuture
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
try {
return api.get(new JQuickCurlReq());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// Callback composition
future.thenAccept(System.out::println).join();
}
}

The connection pool is configured through the global singleton JQuickCurlConfig via connectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit unit) — the maximum idle connections, the idle keep-alive duration and its time unit. It shares its origin with the timeout config, so set it once at start-up.

import com.github.paohaijiao.config.JQuickCurlConfig;
import java.util.concurrent.TimeUnit;
public class PoolSyntaxDemo {
public static void init() {
// Syntax: connectionPool(max idle connections, keep-alive duration, unit)
JQuickCurlConfig.getInstance()
.connectionPool(50, 5, TimeUnit.MINUTES)
.maxRetryCount(2) // retry count
.followRedirects(true); // follow redirects automatically
}
}
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-curl</artifactId>
<version>2.6.0</version>
</dependency>

Every snippet above is a minimal, syntax-level fragment. Full runnable examples live in the GitHub test source tree: https://github.com/dromara/jquick-curl/tree/main/src/test/java/com/github/paohaijiao , and the repository home is https://github.com/dromara/jquick-curl .

Comparison with RestTemplate / OkHttp / WebClient

Section titled “Comparison with RestTemplate / OkHttp / WebClient”
Dimension JQuick-Curl RestTemplate OkHttp (native) WebClient
Positioning A curl-command-driven HTTP client Spring’s traditional synchronous client A general-purpose HTTP client library Spring’s reactive client
Call declaration Write the curl command directly in an annotation / XML Hand-write getForObject / exchange Hand-write Request.Builder Chain webClient.get().uri(...)
Boilerplate Very low (one line of command) Medium High Medium
Learning cost Low (knowing curl is enough) Low Medium (need to know the Builder system) Medium-high (need to understand Reactor)
Command reuse Copy and use from a browser / Postman / Apifox Must be translated by hand Must be translated by hand Must be translated by hand
Dependency footprint OkHttp + grammar runtime + commons-pool2 etc. Spring Web (heavier) OkHttp only Spring WebFlux (heavier)
Blocking model Synchronous blocking Synchronous blocking Synchronous (async via enqueue) Asynchronous non-blocking
Connection pool Configure via JQuickCurlConfig Depends on the underlying implementation Native ConnectionPool Depends on Reactor Netty
Interceptors Supports OkHttp interceptors ClientHttpRequestInterceptor Native support ExchangeFilterFunction
Automatic retries Proxy mode only, fixed count Must be implemented yourself Must be implemented yourself Supports retry()
SSL customisation Custom certificates not supported yet Set via HttpClient Native support Native support
Proxy Supports HTTP proxies Supported Supported Supported
Return value conversion Built-in String / byte[] / POJO Built-in several HttpMessageConverters Must be parsed yourself Built-in codecs
Spring integration No Starter, register beans manually Native Manual Native
Runtime overhead Every command needs grammar parsing Low Lowest Low
  • When you already have a pile of curl commands to land (integration testing, packet captures, migrating automation scripts): JQuick-Curl removes the most tedious translation work, and its value is most obvious here.
  • When you need asynchronous, high-concurrency streaming: choose WebClient or OkHttp + Reactor; JQuick-Curl uses a synchronous blocking model.
  • When you need highly customised HTTP behaviour (mutual authentication, mTLS, custom DNS, HTTP/2 priorities): use OkHttp directly; this module does not expose those capabilities yet.
  • When you are already in the Spring ecosystem with stable APIs: RestTemplate / WebClient are easier, with a more complete ecosystem and observability story.
  • When commands need runtime hot updates (curl configuration served dynamically from a database or a configuration centre): annotation-style commands are compile-time constants and XML-style commands come from classpath resources, so neither suits dynamic delivery — for such cases it is better to store the commands in a database and execute them programmatically with JQuickCurlExecutor.execute(...).

The parser insists that the method be declared explicitly; the assertion message in the source is The specified httpMethod must be displayed eg -X POST|GET|DELETE|PUT etc...... This is intentional: in a curl command the method can appear anywhere, and declaring it explicitly removes ambiguity. Always include -X in every command.

Can commands copied from Postman / Apifox be used directly?

Section titled “Can commands copied from Postman / Apifox be used directly?”

Mostly yes, but note the following:

  • copied commands often start with curl --location; remember that both --location and -L must be written as separate arguments;
  • options such as --compressed, --insecure and -A get carried along automatically and must be removed (--insecure parses but has no effect, while --compressed causes a parse failure);
  • Apifox sometimes emits --data-raw mixed with --data-urlencode; keep only one -d;
  • multi-line commands use \ continuations, which must be turned into string concatenation inside Java strings.

The call succeeds but returns null—how do I troubleshoot?

Section titled “The call succeeds but returns null—how do I troubleshoot?”

Check three things in order:

  1. whether Parse Fail: ... appears in the console — the command did not pass grammar parsing;
  2. whether a java.net.*Exception stack appears in the console — it is a network-layer failure;
  3. whether the HTTP status code is non-2xx — non-2xx does not throw, the response body is returned as usual, and the log only contains a single line Cannot request the resource.

Preferably set the global timeout at application start-up through JQuickCurlConfig.getInstance(); use the @JTimeout annotation when a single API needs to override it. Note that @JTimeout writes its values back into the global singleton configuration, so several APIs using different timeout values will overwrite one another.

No. Internally it uses OkHttp’s synchronous execute() and never exposes enqueue(). Wrap the calls in a thread pool at the business layer when you need concurrency.

Not entirely. Feign provides a whole set of governance capabilities—interface declarations, load balancing, service discovery, circuit breaking—whereas this module only solves the transformation from a curl command to an HTTP request. In a Spring Cloud setting it is better suited to calling third-party open APIs than to internal service-to-service calls.

Every command prints a banner; how do I turn it off?

Section titled “Every command prints a banner; how do I turn it off?”

The banner is printed only on the first request; afterwards a single line Banner has already been printed, skipping... is emitted. Choose one of two ways to disable it:

  1. put jquick-banner.properties in the classpath root with the content jquick.banner.enabled=false;
  2. run the following before the first request:
package com.example.curl;
import com.github.paohaijiao.banner.config.JQuickBannerConfig;
import com.github.paohaijiao.banner.impl.JQuickBannerImpl;
public class BannerOffDemo {
public static void main(String[] args) {
JQuickBannerConfig config = new JQuickBannerConfig();
config.setEnabled(false);
JQuickBannerImpl.getInstance().setConfig(config);
}
}

Does parsing have a performance cost? Is it cached?

Section titled “Does parsing have a performance cost? Is it cached?”

Every command goes through lexical analysis, grammar analysis and visitor assembly again on every call; there is no parse-result cache. For high-frequency APIs this cost is a stable constant. If you are extremely latency-sensitive, call OkHttp directly instead.

How do I print full request and response logs?

Section titled “How do I print full request and response logs?”

The built-in JLoggingInterceptor only prints the duration. For full logs, register an application interceptor of your own that prints request.url(), request.headers(), response.code() and response.peekBody(...) (note that reading the response body must use peekBody, so as not to consume the real response stream).

  • Requires JDK 8 or later; the published artifact is Java 8 bytecode.
  • The transport layer is OkHttp 4.x (a Kotlin implementation), so okhttp and kotlin-stdlib are pulled onto the classpath.
  • The parsing layer is the built-in grammar runtime; for commands whose literals contain ${ / }, wrap the -d value in double quotes so that it is not treated as a placeholder.
  • No Starter is provided, and there is no auto-configuration at all. Every bean must be registered manually in a @Configuration.
  • The module itself does not read application.yml / application.properties; it only provides the loadFromProperties(Properties) and loadFromClasspathResource(String) entry points, and it is up to you where to put the configuration file.
  • JQuickCurlConfig is a global singleton: every setting (timeouts, connection pool, interceptors, redirect policy) is process-wide and cannot be isolated per API. Take extra care in multi-tenant scenarios or where different configurations must coexist.
  • Every request creates a new OkHttpClient and ConnectionPool instance, so the benefit of reusing long-lived connections across requests is limited.
  • The default timeout is 1000000000 (milliseconds, about 11.6 days) and must be set explicitly, otherwise timeout protection is effectively absent.
  • Proxies support the HTTP type only; --socks5-hostname degrades to an HTTP proxy, and proxy credentials are not supported.
  • Certificate-related capabilities are all unsupported: -k has no effect, custom CAs cannot be loaded, and mutual authentication is impossible.
  • Retries have two layers: OkHttp’s retryOnConnectionFailure (connection-failure retries) and maxRetryCount (business-layer retries). Business-layer retries only take effect in dynamic proxy mode, the actual count is maxRetryCount + 1, and there is no back-off.
  • Non-2xx responses do not throw; assert status codes yourself through an interceptor.
  • -v / -s are no-ops; --max-redirs causes a parse failure; --http2 and --data-urlencode have incomplete behaviour.
  • Declaring several URLs or several independent requests in a single command is not supported (with multiple URLs the last one wins).
  • Parsing failures and network failures both surface as a null return, so callers must perform null checks.
  • The expectedStatus, expectedBusinessStatus, validationScript and execute attributes of @JCurlCommand currently have no effect.
  • Return types support void, String, byte[], InputStream, Reader, BufferedReader, ByteString, JQuickCurlResponseBody, okhttp3.ResponseBody, POJOs and generic collections; the HTTP status code is not available, so register an interceptor yourself when you need it.
  • When POJO conversion fails (JSON shape mismatch) no exception is thrown; the stack is printed and null is returned.
  • Dynamic proxies reuse the commons-pool2 object pool of JCurlCommandProcessor, with fixed pool parameters maxTotal = 8, maxIdle = 4 and minIdle = 1; it is not configurable and will become a bottleneck at high concurrency.
  • Application interceptors get registered more than once (see the “Interceptors” section above), so interceptor implementations must be idempotent.
  • The configuration structure can only grow, never shrink; long-running processes should watch out for configuration accumulation in tests or plugin scenarios.

This documentation corresponds to version: 2.6.0

Item Value
GroupId / ArtifactId io.github.paohaijiao / jquick-curl
Version 2.6.0
Parent POM io.github.paohaijiao:javelin:2.7.0
Licence Apache License 2.0
Compilation level Inherits the parent POM configuration; the artifact is Java 8 bytecode

Main transitive dependencies:

Dependency Version Purpose
com.squareup.okhttp3:okhttp 4.10.0 HTTP transport layer
org.antlr:antlr4-runtime 4.13.2 curl command parsing runtime (internal grammar engine)
io.github.paohaijiao:javelin-core 2.7.0 Context, assertions, type conversion
io.github.paohaijiao:jquick-json 1.3.0 JSON handling
io.github.paohaijiao:jquick-xmlProxy 1.6.0 Proxy generation from XML descriptions
io.github.paohaijiao:jquick-banner 1.3.0 Start-up banner
com.google.code.gson:gson 2.8.0 Generic result conversion
org.apache.commons:commons-pool2 2.12.0 JCurlCommandProcessor object pool
org.slf4j:slf4j-api 2.0.9 Logging facade
org.projectlombok:lombok 1.18.30 Compile-time tooling (provided, not transitive)
<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'
Resource Address
Source repository github.com/dromara/jquick-curl
Issue tracker github.com/dromara/jquick-curl/issues
Example code src/test/java
Official documentation www.jquick.org
Maven Central io.github.paohaijiao:jquick-curl

This project is released under the Apache License 2.0.

Copyright 2025-2099 Martin ([email protected])
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Full licence text: https://www.apache.org/licenses/LICENSE-2.0