likes
comments
collection
share

Java http声明式客户端对比

作者站长头像
站长
· 阅读数 7

在Java程序中发起http调用时,一般使用Java自带的httpConnection和apache的httpclient。还可以尝试一下几种声明式 http客户端调用工具

forest国产比较好用的httpclient
openFeginspringcloud组件库中的httpClient
retrofit来自于android中的httpclient
Http Interface底层基于webflux中的webclient实现

openFeign相比如其他框架多了一个ribbon(服务发现功能)。也可以作为普通的http客户端调用工具。

Forest

官方网站

引用

<dependency>
    <groupId>com.dtflys.forest</groupId>
    <artifactId>forest-spring-boot-starter</artifactId>
    <version>1.5.36</version>
</dependency>

同步也支持protobuf

配置

  • 超时时间
forest:
  max-connections: 1000        # 连接池最大连接数
  connect-timeout: 3000        # 连接超时时间,单位为毫秒
  read-timeout: 3000           # 数据读取超时时间,单位为毫秒

拦截器

定义一个拦截器需要实现com.dtflys.forest.interceptor.Interceptor接口

public class SimpleInterceptor<T> implements Interceptor<T> {

    private final static Logger log = LoggerFactory.getLogger(SimpleInterceptor.class);

    /**
     * 该方法在被调用时,并在beforeExecute前被调用 
     * @Param request Forest请求对象
     * @Param args 方法被调用时传入的参数数组 
     */
    @Override
    public void onInvokeMethod(ForestRequest req, ForestMethod method, Object[] args) {
        log.info("on invoke method");
        // req 为Forest请求对象,即 ForestRequest 类实例
        // method 为Forest方法对象,即 ForestMethod 类实例
        // addAttribute作用是添加和Forest请求对象以及该拦截器绑定的属性
        addAttribute(req, "A", "value1");
        addAttribute(req, "B", "value2");
    }

    /**
     * 在请求体数据序列化后,发送请求数据前调用该方法
     * 默认为什么都不做
     * 注: multlipart/data类型的文件上传格式的 Body 数据不会调用该回调函数
     *
     * @param request Forest请求对象
     * @param encoder Forest转换器
     * @param encodedData 序列化后的请求体数据
     */
    public byte[] onBodyEncode(ForestRequest request, ForestEncoder encoder, byte[] encodedData) {
        // request: Forest请求对象
        // encoder: 此次转换请求数据的序列化器
        // encodedData: 序列化后的请求体字节数组
        // 返回的字节数组将替换原有的序列化结果
        // 默认不做任何处理,直接返回参数 encodedData
        return encodedData;
    }


    /**
     * 该方法在请求发送之前被调用, 若返回false则不会继续发送请求
     * @Param request Forest请求对象
     */
    @Override
    public boolean beforeExecute(ForestRequest req) {
        log.info("invoke Simple beforeExecute");
        // 执行在发送请求之前处理的代码
        req.addHeader("accessToken", "11111111");  // 添加Header
        req.addQuery("username", "foo");  // 添加URL的Query参数
        return true;  // 继续执行请求返回true
    }

    /**
     * 该方法在请求成功响应时被调用
     */
    @Override
    public void onSuccess(T data, ForestRequest req, ForestResponse res) {
        log.info("invoke Simple onSuccess");
        // 执行成功接收响应后处理的代码
        int status = res.getStatusCode(); // 获取请求响应状态码
        String content = res.getContent(); // 获取请求的响应内容
        String result = (String)data;  // data参数是方法返回类型对应的返回数据结果,注意需要视情况修改对应的类型否则有可能出现类转型异常
        result = res.getResult(); // getResult()也可以获取返回的数据结果
        response.setResult("修改后的结果: " + result);  // 可以修改请求响应的返回数据结果
        
        // 使用getAttributeAsString取出属性,这里只能取到与该Forest请求对象,以及该拦截器绑定的属性
        String attrValue1 = getAttributeAsString(req, "A1");

    }

    /**
     * 该方法在请求发送失败时被调用
     */
    @Override
    public void onError(ForestRuntimeException ex, ForestRequest req, ForestResponse res) {
        log.info("invoke Simple onError");
        // 执行发送请求失败后处理的代码
        int status = res.getStatusCode(); // 获取请求响应状态码
        String content = res.getContent(); // 获取请求的响应内容
        String result = res.getResult(); // 获取方法返回类型对应的返回数据结果
    }

    /**
     * 该方法在请求发送之后被调用
     */
    @Override
    public void afterExecute(ForestRequest req, ForestResponse res) {
        log.info("invoke Simple afterExecute");
        // 执行在发送请求之后处理的代码
        int status = res.getStatusCode(); // 获取请求响应状态码
        String content = res.getContent(); // 获取请求的响应内容
        String result = res.getResult(); // 获取方法返回类型对应的最终数据结果
    }
}

这里有一个限制,拦截器只能针对同一个返回类型,不同的返回类型需要使用不同的拦截器。或者抽象出一个基类也可以使用同一个拦截器。

OpenFegin

引用

implementation('org.springframework.cloud:spring-cloud-starter-openfeign')

配置

package com.wuhanpe.face.cloud.api

import com.wuhanpe.face.cloud.api.config.AppConfig
import com.wuhanpe.face.cloud.api.request.FaceInfoRequest
import com.wuhanpe.face.cloud.api.request.PushFaceTradeRequest
import com.wuhanpe.face.cloud.api.response.FaceInfoResponse
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody

/**
 *
 * @author:zooooooooy
 * @date: 2024/1/19 - 10:28
 */
@FeignClient(
    name = "app",
    url = "**",
    configuration = arrayOf(AppConfig::class))
interface AppClient {

    @PostMapping("com/whpe/faceCard/pushFaceCardInfo")
    fun pushFaceCardInfo(@RequestBody faceInfoRequest: FaceInfoRequest): FaceInfoResponse


    @PostMapping("com/whpe/faceCard/pushTrade")
    fun pushTrade(@RequestBody pushFaceTradeRequest: PushFaceTradeRequest): FaceInfoResponse

}

和常规的feign调用时一致的,只是需要手动指定url。

拦截器

需要指定config类,并配置encoder。

package com.wuhanpe.face.cloud.api.config

import feign.codec.Encoder
import org.springframework.context.annotation.Bean

/**

 * @author:zooooooooy
 * @date: 2024/1/19 - 10:34
 */
class AppConfig {

    @Bean
    fun appEncoder(): Encoder {
        return AppEncoder()
    }
}
package com.wuhanpe.face.cloud.api.config

import cn.hutool.core.bean.BeanUtil
import cn.hutool.core.codec.Base64
import cn.hutool.crypto.SecureUtil
import cn.hutool.crypto.asymmetric.RSA
import cn.hutool.crypto.asymmetric.SignAlgorithm
import cn.hutool.json.JSONUtil
import com.google.common.base.Joiner
import com.wuhanpe.face.cloud.api.request.FaceBaseRequest
import com.wuhanpe.face.cloud.api.request.FaceInfoRequest
import com.wuhanpe.face.cloud.util.RSACLS
import feign.RequestTemplate
import feign.codec.Encoder
import java.lang.reflect.Type
import java.util.*


/**
 *
 * @author:zooooooooy
 * @date: 2024/1/19 - 17:24
 */
class AppEncoder: Encoder {

    override fun encode(target: Any?, p1: Type?, restTemplate: RequestTemplate?) {

        val paramMap = TreeMap<String, Any>()

        paramMap.putAll(BeanUtil.beanToMap(target, false,  true))

        // 排序
        val join = Joiner.on("&").withKeyValueSeparator("=").join(paramMap)

        val faceBaseRequest = target as FaceBaseRequest

        val privateKey = "***"

        faceBaseRequest.sign = RSACLS.sign(join, privateKey)

        restTemplate?.header("Content-Type", "application/json")

        restTemplate?.body(JSONUtil.toJsonStr(target))

    }

}

不同的feign类需要指定不同的config,需要配置多个config,由openfeign单独进行实例化。

Retrofit

gitee.com/lianjiatech…

引用

<dependency>
  <groupId>com.github.lianjiatech</groupId>
  <artifactId>retrofit-spring-boot-starter</artifactId>
  <version>3.0.3</version>
</dependency>

配置

@RetrofitClient(baseUrl = "${test.baseUrl}")
public interface UserService {

    /**
    * 根据id查询用户姓名
    */
    @POST("getName")
    String getName(@Query("id") Long id);
}
retrofit:
   # 全局转换器工厂
   global-converter-factories:
      - com.github.lianjiatech.retrofit.spring.boot.core.BasicTypeConverterFactory
      - retrofit2.converter.jackson.JacksonConverterFactory
   # 全局调用适配器工厂(组件扩展的调用适配器工厂已经内置,这里请勿重复配置)
   global-call-adapter-factories:

拦截器

全局拦截

@Component
public class MyGlobalInterceptor implements GlobalInterceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());
        // response的Header加上global
        return response.newBuilder().header("global", "true").build();
    }
}

应用拦截

@Component
public class PathMatchInterceptor extends BasePathMatchInterceptor {
    @Override
    protected Response doIntercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());
        // response的Header加上path.match
        return response.newBuilder().header("path.match", "true").build();
    }
}

继承BasePathMatchInterceptor,再在声明式接口进行标记即可。

@RetrofitClient(baseUrl = "${test.baseUrl}")
@Intercept(handler = PathMatchInterceptor.class, include = {"/api/user/**"}, exclude = "/api/user/getUser")
// @Intercept() 如果需要使用多个路径匹配拦截器,继续添加@Intercept即可
public interface InterceptorUserService {

    /**
    * 根据id查询用户姓名
    */
    @POST("getName")
    Response<String> getName(@Query("id") Long id);

    /**
    * 根据id查询用户信息
    */
    @GET("getUser")
    Response<User> getUser(@Query("id") Long id);

}

WebClient(webflux)

docs.spring.io/spring-fram…

引用

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

引入webflux即可。

配置

import com.howtodoinjava.app.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.DeleteExchange;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.service.annotation.PutExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@HttpExchange(url = "/users", accept = "application/json", contentType = "application/json")
public interface UserClient {
  @GetExchange("/")
  Flux<User> getAll();
  @GetExchange("/{id}")
  Mono<User> getById(@PathVariable("id") Long id);
  @PostExchange("/")
  Mono<ResponseEntity<Void>> save(@RequestBody User user);
  @PutExchange("/{id}")
  Mono<ResponseEntity<Void>> update(@PathVariable Long id, @RequestBody User user);
  @DeleteExchange("/{id}")
  Mono<ResponseEntity<Void>> delete(@PathVariable Long id);
}
@Bean
UserClient userApi(WebClient client) {
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
    return factory.createClient(UserClient.class);
}

直接注入即可

拦截器

针对不同url对应的客户端配置不同的拦截器。

RestClient customClient = RestClient.builder()
  .requestFactory(new HttpComponentsClientHttpRequestFactory())
  .messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
  .baseUrl("https://example.com")
  .defaultUriVariables(Map.of("variable", "foo"))
  .defaultHeader("My-Header", "Foo")
  .requestInterceptor(myCustomInterceptor) // 拦截器配置
  .requestInitializer(myCustomInitializer)
  .build();

总结

声明式相对于直接调用具有解耦,代码量小,易于配置等优点。可以根据项目中的组件选择上述4种中的任何一种即可。