Skip to content

声明式

概述

声明式调用是面向接口,可以通过接口方法声明绑定相应的注解达到方法调用即是HTTP调用的效果。见下面这个简单的例子:

java
// HttpBinApi API接口定义
public interface HttpBinApi {

    @Get(path = "/get")
    String getString(@Query("foo") String fooVal);
}

ApiEnhancer apiEnhancer = Solpic.newApiEnhancerBuilder()
        .httpClient(Solpic.newHttpClientBuilder().type(HttpClientType.JDK_HTTPCLIENT).build())
        .baseUrl("https://httpbin.org")
        .build();
HttpBinApi httpBinApi = apiEnhancer.enhance(HttpBinApi.class);
String response = httpBinApi.getString("bar");
System.out.println(response);

上面的声明式调用效果和下面这个命令式调用的例子是一致的:

java
HttpClient httpClient = Solpic.newHttpClientBuilder().type(HttpClientType.JDK_HTTPCLIENT).build();
HttpRequest httpRequest = HttpRequest.newBuilder()
        .uri(URI.create("https://httpbin.org/get"))
        .method(HttpMethod.GET)
        .query("foo", "bar")
        .build();
String content = httpClient.sendSimple(httpRequest, PayloadSubscribers.X.ofString());
System.out.println(content);

注解支持

Solpic提供大量注解用于支持API接口方法和实际的HTTP调用参数。

@Request

@Request对于指定请求方法、请求的(相对)路径和请求的绝对路径。方法参数如下:

  • method():用于指定HTTP请求方法,例如HttpMethod.GET
  • path():用于指定HTTP请求的路径,默认值为空字符串
  • url():用于指定HTTP请求的绝对URL,此值有较高优先级并且会覆盖path()的值,默认值为空字符串

path()url()都为空字符串的时候,@Request注解所在的方法名称会被作为HTTP请求的路径使用。@Request的同类型特化注解有@Get@Post@Put@Delete@Options@Head@Patch@Trace。举个例子:

java
public interface HttpBinApi {

    @Get(path = "/get")
    String getString1(@Query("foo") String fooVal);

    @Request(path = "/get", method = HttpMethod.GET)
    String getString2(@Query("foo") String fooVal);
}

@Query

@Query用于指定单个HTTP请求的URL参数,其中encoded()属性用于指定参数的名称和值是否需要进行URL编码,默认为false

@Queries

@Queries用于指定多个HTTP请求的URL参数,其中encoded()属性用于指定参数的名称和值是否需要进行URL编码,默认为false。此注解所在参数的类型可以是Map或者Iterable<Pair>。例如:

java
public interface HttpBinApi {

    @Get(path = "/get")
    String getString1(@Queries Map<String, String> qs);

    @Get(path = "/get")
    String getString2(@Queries List<Pair> qs);
}

这里的Pair的全类名为cn.vlts.solpic.core.util.Pair

@Consume

@Consume用于指定HTTP请求的Content-Type,优先级低于请求头参数。例如:

java
@Consume("application/json")
public interface HttpBinApi {

}

@Produce

@Produce用于指定HTTP响应的Content-Type,目前只用于JSON的反序列化策略匹配和判断。

@Header用于指定单个请求头K-V参数。例如:

java
public interface HttpBinApi {

    @Get(path = "/get")
    String getString(@Query("foo") String fooVal, @Header("k") String headerVal);
}

@Headers

@Headers用于指定多个请求头K-V参数,此注解所在参数的类型可以是Map或者Iterable<HttpHeader>。例如:

java
public interface HttpBinApi {

    @Get(path = "/get")
    String getString1(@Query("foo") String fooVal, @Headers Map<String, String> headers);

    @Get(path = "/get")
    String getString2(@Query("foo") String fooVal, @Headers List<HttpHeader> headers);
}

这里的HttpHeader的全类名为cn.vlts.solpic.core.http.HttpHeader

@Form

@Form对应于请求Content-Typeapplication/x-www-form-urlencoded的请求有效载荷的特化场景处理,此注解所在参数的类型可以是Map或者UrlEncodedForm。例如:

java
public interface HttpBinApi {

    @Post(path = "/post")
    String form1(@Form Map<String, String> form);

    @Post(path = "/post")
    String form2(@Form UrlEncodedForm form);
}

@Multipart

@Multipart对应于请求Content-Typemultipart/form-data的请求有效载荷的特化场景处理,此注解所在参数的类型必须是MultipartData。例如:

java
public interface HttpBinApi {

    @Post(path = "/post")
    String multipart(@Multipart MultipartData m);
}

@Opt

@Opt用于指定HTTP请求的选项配置,只支持REQUEST级别的选项。例如:

java
public interface HttpBinApi {

    @Get(path = "/get")
    @Opt(key = "HTTP_ENABLE_EXECUTE_TRACING", value = "true")
    @Opt(key = "HTTP_ENABLE_LOGGING", value = "true")
    String getString(@Query("foo") String fooVal);
}

@Var

@Var用于配置某些特殊请求类型所需的参数,目前支持三种参数delaylistenerpromise,下文遇到具体使用场景时候再详细说明。

@Payload

@Payload用于指定请求有效载荷(请求体),也就是所有非特化场景类型的请求有效载荷都是用此注解处理对应的参数。例如:

java
public interface HttpBinApi {

    @Post(path = "/post")
    @Consume("text/plain")
    String post(@Payload String content);
}

更详细的说明见下面的"参数转换"小节。

创建服务

首先要初始化ApiEnhancer实例:

java
ApiEnhancer apiEnhancer = Solpic.newApiEnhancerBuilder()
        .httpClient(Solpic.newHttpClientBuilder().type(HttpClientType.JDK_HTTPCLIENT).build()) // 指定HttpClient
        .baseUrl("https://httpbin.org") // 指定基础请求URL
        .delay(1000L) // 默认的延迟调度发送时间,单位为毫秒 - 用于scheduledSend场景
        .promise(CompletableFuture::new) // 默认的Promise提供者 - 用于scheduledSend场景
        .listener(() -> (f) -> {}) // 默认的FutureListener提供者  - 用于enqueue场景
        .loadEagerly()  // 是否提前加载解析所有方法
        .codec(new JacksonCodec<>())  // 指定编码解码器
        .addConverterFactory(new ConverterFactory<>() {})  // 添加转换工厂
        .build();

得到ApiEnhancer实例后,基于它和指定API接口类型即可创建API接口实例:

java
public interface HttpBinApi {

    @Get(path = "/get")
    String getString(@Query("foo") String fooVal);
}

HttpBinApi httpBinApi = apiEnhancer.enhance(HttpBinApi.class);

接着可以直接调用API接口实例的方法实现HTTP调用。

参数转换

ApiEnhancer增强的API接口代理实例很多时候需要进行入参和返回值转换,此功能由转换工厂ConverterFactory完成。自定义参数转换实现只需要继承ConverterFactory编写转换逻辑,然后把转换工厂实例添加到ApiEnhancer中。这里举个请求参数和响应结果都是嵌套泛型的例子:

java
@Data
public class CommonRequest<T> {

    private String requestId;
    private Long timestamp;
    private T data;
}

@Data
public class CommonResponse<T> {

    private String requestId;
    private Long code;
    private String message;
    private T data;
}

@Data
public class CommonData {

    private String name;
    private Long amount;
}

public interface CommonApi {

    @Post(path = "/invoke")
    CommonResponse<CommonData> invoke(@Payload CommonRequest<CommonData> request);
}

编写CommonApi的参数转换工厂CommonApiConverterFactory如下:

java
class CommonApiConverterFactory extends ConverterFactory<CommonRequest, CommonResponse> {

    private static final String INVOKE_METHOD = "invoke";

    private final Codec<CommonRequest, CommonResponse> codec = new JacksonCodec<>();

    @Override
    public boolean supportRequestConverter(ApiParameterMetadata metadata) {
        Class<?> rawType = ReflectionUtils.X.getRawType(metadata.getParameterType());
        return CommonRequest.class.isAssignableFrom(rawType);
    }

    @Override
    public Converter<CommonRequest, RequestPayloadSupport> newRequestConverter(ApiParameterMetadata metadata) {
        if (Objects.equals(INVOKE_METHOD, metadata.getMethodName())) {
            return codec::createPayloadPublisher;
        }
        return super.newRequestConverter(metadata);
    }

    @Override
    public boolean supportResponseSupplier(ApiParameterMetadata metadata) {
        Class<?> rawType = ReflectionUtils.X.getRawType(metadata.getReturnType());
        return CommonResponse.class.isAssignableFrom(rawType);
    }

    @Override
    public Supplier<ResponsePayloadSupport<CommonResponse>> newResponseSupplier(ApiParameterMetadata metadata) {
        if (Objects.equals(INVOKE_METHOD, metadata.getMethodName())) {
            return () -> codec.createPayloadSubscriber(metadata.getReturnType());
        }
        return super.newResponseSupplier(metadata);
    }
}

最后通过ApiEnhancerBuilder加载CommonApiConverterFactory使用即可:

java
CommonApi commonApi = Solpic.newApiEnhancerBuilder()
        .addConverterFactory(new CommonApiConverterFactory())
        .build()
        .enhance(CommonApi.class);

通过ApiEnhancer进行代理增强的API接口,不仅仅支持同步发送HTTP请求,还支持异步发送、队列发送和调度发送,并且方法返回结果可以使用复杂的泛型嵌套类型。这里提供一个例子,列举目前Solpic支持的所有复杂声明式API定义:

java
public interface HttpBinApi {

    @Get(path = "/get")
    HttpResponse<?> p1(@Query(value = "foo") String bar);

    @Get(path = "/get")
    HttpResponse<Map<String, Object>> p2(@Query(value = "foo") String bar);

    @Get(path = "/get")
    String p3(@Query(value = "foo") String bar);

    @Get(path = "/get")
    HttpBinResult p4(@Query(value = "foo") String bar);

    @Get(path = "/get")
    HttpResponse<HttpBinResult> p5(@Query(value = "foo") String bar);

    @Get(path = "/get")
    CompletableFuture<HttpBinResult> p6(@Query(value = "foo") String bar);

    @Get(path = "/get")
    CompletableFuture<HttpResponse<HttpBinResult>> p7(@Query(value = "foo") String bar);

    @Get(path = "/get")
    ListenableFuture<?> p8(@Query(value = "foo") String bar,
                           @Var("listener") FutureListener<?> listener);

    @Get(path = "/get")
    ListenableFuture<HttpBinResult> p9(@Query(value = "foo") String bar,
                                       @Var("listener") FutureListener<HttpBinResult> listener);

    @Get(path = "/get")
    ListenableFuture<HttpResponse<HttpBinResult>> p10(@Query(value = "foo") String bar,
                                                      @Var("listener") FutureListener<HttpResponse<HttpBinResult>> listener);

    @Get(path = "/get")
    ScheduledFuture<?> p11(@Query(value = "foo") String bar,
                           @Var("delay") Long delay,
                           @Var("promise") CompletableFuture<?> promise);

    @Get(path = "/get")
    ScheduledFuture<HttpBinResult> p12(@Query(value = "foo") String bar,
                                       @Var("delay") Long delay,
                                       @Var("promise") CompletableFuture<HttpBinResult> promise);

    @Get(path = "/get")
    ScheduledFuture<HttpResponse<HttpBinResult>> p13(@Query(value = "foo") String bar,
                                                     @Var("delay") Long delay,
                                                     @Var("promise") CompletableFuture<HttpResponse<HttpBinResult>> promise);
}

其中:

  • p1 ~ p5为同步发送模式,对应于HttpClient#send()HttpClient#sendSimple()方法
  • p6 ~ p7为异步发送模式,对应于HttpClient#sendAsync()HttpClient#sendAsyncSimple()方法
  • p8 ~ p10为队列发送模式,对应于HttpClient#enqueue()HttpClient#enqueueSimple()方法
  • p11 ~ p13为调度发送模式,对应于HttpClient#scheduledSend()HttpClient#scheduledSendSimple()方法

对于队列发送模式,需要配置结果监听器FutureListener实例,添加结果监听器必须使用@Var("listener")注解声明。对于调度发送模式,需要配置延迟发送时间(单位为毫秒)和原始类型为CompletableFuture的泛型Promise,前者必须使用@Var("delay")注解声明,后者必须使用@Var("promise")注解声明。ApiEnhancer在处理这四种发送模式的时候已经基于返回值类型做了不同的调用分支,但是鉴于提取泛型参数的逻辑比较复杂,返回值泛型嵌套类型最里面的真实类型需要在转换器工厂中指定,以上面的HttpBinApi为例:

java
@SuppressWarnings("rawtypes")
public class HttpBinConverterFactory extends ConverterFactory {

    final Codec codec = new JacksonCodec();

    @Override
    public boolean supportResponseSupplier(ApiParameterMetadata metadata) {
        return true;
    }

    @Override
    public Supplier<ResponsePayloadSupport> newResponseSupplier(ApiParameterMetadata metadata) {
        ReflectionUtils.ParameterizedTypeInfo pti = ReflectionUtils.X.getParameterizedTypeInfo(metadata.getReturnType());
        // 提取最里层参数位置为0的参数类型,也就是HttpBinResult.class或者NULL
        Class<?> rawType = pti.getRawClass(pti.getMaxDepth(), 0);
        if (HttpBinResult.class.isAssignableFrom(rawType)) {
            return () -> codec.createPayloadSubscriber(rawType);
        }
        return super.newResponseSupplier(metadata);
    }
}

对于使用@Payload注解声明的方法入参,可以使用参数转换工厂的ConverterFactory#supportRequestConverter()ConverterFactory#newRequestConverter()判断是否需要生成转换器和创建转换器实例。例如:

java
@SuppressWarnings({"rawtypes", "unchecked"})
class CustomConverterFactory extends ConverterFactory {

    private static final String M = "someMethod";

    private final Codec codec = new JacksonCodec<>();

    @Override
    public boolean supportRequestConverter(ApiParameterMetadata metadata) {
        Class<?> rawType = ReflectionUtils.X.getRawType(metadata.getParameterType());
        if (Map.class.isAssignableFrom(rawType) ||
                HttpBinResult.class.isAssignableFrom(rawType) ||
                Objects.equals(M, metadata.getMethodName())) {
            return true;
        }
        return false;
    }

    @Override
    public Converter<?, RequestPayloadSupport> newRequestConverter(ApiParameterMetadata metadata) {
        return codec::createPayloadPublisher;
    }
}

编码解码器Codec通常能处理绝大部分请求有效载荷为JSON对象的场景。

贡献者

页面历史

Released under the MIT License.