JQuick-Curl
Module overview
Section titled “Module overview”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:
- Declarative annotation-based calls — write
@JCurlCommandon an interface method;JCurlInvoker.createProxygenerates a dynamic proxy, and calling the method issues the request. - Centralized XML configuration — request templates are maintained centrally in
apis.xml, decoupled from Java code, with support for the<if>/<foreach>/<choose>dynamic tags. - Programmatic calls — construct an executor directly or invoke through method references; suited to scripts, tests and no-interface scenarios.
Features
Section titled “Features”- 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:
@JCurlCommandgoes on the interface method, andJCurlInvoker.createProxy(XxxApi.class)gives you an injectable client in one line. - Dynamic proxy + connection pool: the dynamic proxy reuses handler instances managed by
GenericObjectPool, whileJQuickCurlConfigmanages 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
--datafamily,-Fmultipart 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
JQuickCurlConfigsingleton, configured once with chained calls and overridable per method with@JTimeout. - Global and network interceptors: attach OkHttp interceptors directly with
addInterceptor/addNetworkInterceptorfor 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.
When to use it
Section titled “When to use it”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.
Quick start
Section titled “Quick start”Installation
Section titled “Installation”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>implementation 'io.github.paohaijiao:jquick-curl:2.6.0'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 |
Using it with Spring Boot (minimal demo)
Section titled “Using it with Spring Boot (minimal demo)”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. */@Configurationpublic 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;
@RestControllerpublic 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=3000quick.curl.read.timeout=10000quick.curl.write.timeout=10000quick.curl.pool.max.idle=50quick.curl.pool.keep.alive=300000quick.curl.max.retry.count=2Load 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;
@Configurationpublic class CurlPropertiesConfiguration {
@Bean public JQuickCurlConfig jquickCurlConfig() throws IOException { return JQuickCurlConfig.getInstance().loadFromClasspathResource("quick-curl.properties"); }}Plain Java project (no Spring)
Section titled “Plain Java project (no Spring)”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); }}Declarative calls with @JCurlCommand
Section titled “Declarative calls with @JCurlCommand”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.
Define the API interface
Section titled “Define the API interface”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);Invoke the interface
Section titled “Invoke the interface”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);}Method-level timeouts: @JTimeout
Section titled “Method-level timeouts: @JTimeout”@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);}Multi-method interface example
Section titled “Multi-method interface example”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; }}XML-based calls
Section titled “XML-based calls”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.
XML definition file
Section titled “XML definition 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.
Create an interface proxy via the factory
Section titled “Create an interface proxy via the factory”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); }}Dynamic tags: if / foreach / choose
Section titled “Dynamic tags: if / foreach / choose”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="&"> -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)); }}Programmatic calls (JCurlInvoker)
Section titled “Programmatic calls (JCurlInvoker)”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.
Basic programmatic invocation
Section titled “Basic programmatic invocation”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()); }}Supported return types
Section titled “Supported return types”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. |
Placeholder variables ${} syntax
Section titled “Placeholder variables ${} syntax”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.
Basic variable usage
Section titled “Basic variable usage”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)); }}Variables in the URL path
Section titled “Variables in the URL path”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);Variables in query parameters
Section titled “Variables in query parameters”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);Variables in headers
Section titled “Variables in headers”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 JSON request body
Section titled “Variables in the JSON request body”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 sideJQuickCurlReq request = new JQuickCurlReq();
// Option 2: a single field variableJQuickCurlReq fieldRequest = new JQuickCurlReq();fieldRequest.put("name", "Ada");JQuickCurlReq input parameters in detail
Section titled “JQuickCurlReq input parameters in detail”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 oneJQuickCurlReq request = new JQuickCurlReq();request.put("id", 1001);request.put("tenant", "acme");
// Put entries in bulkjava.util.Map<String, Object> values = new java.util.HashMap<>();values.put("page", 1);values.put("size", 20);request.putAll(values);
// Read and checkObject id = request.get("id");boolean hasTenant = request.containsKey("tenant");Request body examples
Section titled “Request body examples”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-Typedefaults toapplication/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 request (with query parameters)
Section titled “GET request (with query parameters)”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)); }}POST JSON
Section titled “POST JSON”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")); }}POST Multipart file form -F
Section titled “POST Multipart file form -F”-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. |
File upload and download
Section titled “File upload and download”File upload (local files)
Section titled “File upload (local files)”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(); }}File download, saved to local disk
Section titled “File download, saved to local disk”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); }}Download into an in-memory byte array
Section titled “Download into an in-memory byte array”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); }}Authentication
Section titled “Authentication”Basic Auth -u
Section titled “Basic Auth -u”-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 token (header)
Section titled “Bearer token (header)”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); }}Custom authentication headers
Section titled “Custom authentication headers”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); } }}SSL / HTTPS configuration
Section titled “SSL / HTTPS configuration”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())); }}Loading custom certificates
Section titled “Loading custom certificates”HTTP proxy configuration
Section titled “HTTP proxy configuration”HTTP/HTTPS proxy
Section titled “HTTP/HTTPS proxy”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. |
Proxy username and password
Section titled “Proxy username and password”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); }}Timeouts, redirects and retries
Section titled “Timeouts, redirects and retries”Connect timeout and read timeout
Section titled “Connect timeout and read timeout”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. |
Following redirects -L
Section titled “Following redirects -L”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())); }}Request retry configuration
Section titled “Request retry configuration”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())); }}Global connection pool
Section titled “Global connection pool”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: 300000package 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;
@Configurationpublic 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"); }}Interceptors
Section titled “Interceptors”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.
Registration entry points
Section titled “Registration entry points”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.
Built-in logging interceptor
Section titled “Built-in logging interceptor”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.
Full custom interceptor example
Section titled “Full custom interceptor example”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; } }}An implementation detail worth noting
Section titled “An implementation detail worth noting”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 |
Exception model
Section titled “Exception model”The module does not define its own exception types; exceptions come from three places: parameter assertions, the grammar parsing layer, and the underlying OkHttp.
Overview of exception types
Section titled “Overview of exception types”| 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(...)catchesJAntlrExecutionException, printsParse Fail: ...to the standard error stream, and thenreturn null. - Network failures: at the request execution site
catch (IOException e) { e.printStackTrace(); }runs, and thenreturn 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 apartUser 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 resourceand then returns the response body as usual; validateStatusCode(...)is a private method that is never called;expectedBusinessStatus,validationScriptandexecuteare 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()); }}Custom parsers (JQuickParseHandler)
Section titled “Custom parsers (JQuickParseHandler)”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 |
Supported curl options
Section titled “Supported curl options”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. |
Native curl options not supported yet
Section titled “Native curl options not supported yet”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.
1. Basic request syntax
Section titled “1. Basic request syntax”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())); }}2. Header syntax
Section titled “2. Header syntax”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())); }}3. Query parameter syntax
Section titled “3. Query parameter syntax”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)); }}4. Path variable syntax
Section titled “4. Path variable syntax”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")); }}5. Raw text body syntax
Section titled “5. Raw text body syntax”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())); }}6. JSON body syntax
Section titled “6. JSON body syntax”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())); }}7. Multipart form syntax
Section titled “7. Multipart form syntax”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())); }}8. Timeout config syntax
Section titled “8. Timeout config syntax”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);}9. Proxy syntax
Section titled “9. Proxy syntax”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)); }}10. HTTP/2 enable syntax
Section titled “10. HTTP/2 enable syntax”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())); }}11. SSL / mutual TLS syntax
Section titled “11. SSL / mutual TLS syntax”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.
12. Response read syntax
Section titled “12. Response read syntax”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); }}13. Async callback syntax
Section titled “13. Async callback syntax”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(); }}14. Connection pool config syntax
Section titled “14. Connection pool config syntax”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 }}Maven Dependency
Section titled “Maven Dependency”<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 |
Choosing between them
Section titled “Choosing between them”- 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(...).
Why does omitting -X fail immediately?
Section titled “Why does omitting -X fail immediately?”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--locationand-Lmust be written as separate arguments; - options such as
--compressed,--insecureand-Aget carried along automatically and must be removed (--insecureparses but has no effect, while--compressedcauses a parse failure); - Apifox sometimes emits
--data-rawmixed 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:
- whether
Parse Fail: ...appears in the console — the command did not pass grammar parsing; - whether a
java.net.*Exceptionstack appears in the console — it is a network-layer failure; - 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.
How do I set timeouts centrally?
Section titled “How do I set timeouts centrally?”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.
Is asynchronous calling supported?
Section titled “Is asynchronous calling supported?”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.
Can it replace Feign?
Section titled “Can it replace Feign?”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:
- put
jquick-banner.propertiesin the classpath root with the contentjquick.banner.enabled=false; - 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).
Notes and known limitations
Section titled “Notes and known limitations”Environment and dependencies
Section titled “Environment and dependencies”- Requires JDK 8 or later; the published artifact is Java 8 bytecode.
- The transport layer is OkHttp 4.x (a Kotlin implementation), so
okhttpandkotlin-stdlibare pulled onto the classpath. - The parsing layer is the built-in grammar runtime; for commands whose literals contain
${/}, wrap the-dvalue in double quotes so that it is not treated as a placeholder.
Relationship with Spring Boot
Section titled “Relationship with Spring Boot”- 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 theloadFromProperties(Properties)andloadFromClasspathResource(String)entry points, and it is up to you where to put the configuration file.
HTTP behaviour
Section titled “HTTP behaviour”JQuickCurlConfigis 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
OkHttpClientandConnectionPoolinstance, 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-hostnamedegrades to an HTTP proxy, and proxy credentials are not supported. - Certificate-related capabilities are all unsupported:
-khas no effect, custom CAs cannot be loaded, and mutual authentication is impossible. - Retries have two layers: OkHttp’s
retryOnConnectionFailure(connection-failure retries) andmaxRetryCount(business-layer retries). Business-layer retries only take effect in dynamic proxy mode, the actual count ismaxRetryCount + 1, and there is no back-off. - Non-2xx responses do not throw; assert status codes yourself through an interceptor.
Parsing and results
Section titled “Parsing and results”-v/-sare no-ops;--max-redirscauses a parse failure;--http2and--data-urlencodehave 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
nullreturn, so callers must perform null checks. - The
expectedStatus,expectedBusinessStatus,validationScriptandexecuteattributes of@JCurlCommandcurrently 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
nullis returned.
Concurrency and lifecycle
Section titled “Concurrency and lifecycle”- Dynamic proxies reuse the commons-pool2 object pool of
JCurlCommandProcessor, with fixed pool parametersmaxTotal = 8,maxIdle = 4andminIdle = 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.
Version notes
Section titled “Version notes”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) |
Repository and licence
Section titled “Repository and licence”Maven coordinates
Section titled “Maven coordinates”<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'Related links
Section titled “Related links”| 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 |
License
Section titled “License”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, softwaredistributed 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 andlimitations under the License.Full licence text: https://www.apache.org/licenses/LICENSE-2.0