Java http声明式客户端对比
在Java程序中发起http调用时,一般使用Java自带的httpConnection和apache的httpclient。还可以尝试一下几种声明式 http客户端调用工具
forest | 国产比较好用的httpclient |
---|---|
openFegin | springcloud组件库中的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
引用
<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)
引用
<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种中的任何一种即可。
转载自:https://juejin.cn/post/7352075813258264626