背景
由于项目上经常要对接第三方的api,各类http工具类编码繁琐,定义接口不明确,所以就基于spring的RestTemplate封装了一个可读性高的工具类,方便接口管理
特性
- 采用函数+链式方式直观展现接口定义
- 支持一键开启日志,formdata请求转换
- 内置 javabean 到 queryparam 和 path占位符 的自动映射
- 支持定制请求客户端
代码:
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.lang.NonNull;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StopWatch;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author Alone
*/
public interface Api<IN, OUT> extends Function<IN, OUT> {
/**
* Api构建器
*
* @param reqClass 请求
* @param respClass 响应
* @param <IN> reqClass
* @param <OUT> respClass
* @return Api构建器
*/
static <IN, OUT> Builder<IN, OUT> builder(Class<? extends IN> reqClass, Class<? extends OUT> respClass) {
return new Builder<>(reqClass, respClass);
}
/**
* Api构建器, 没有动态入参,写死的请求
*
* @param respClass 响应
* @param <OUT> respClass
* @return Api构建器
*/
static <OUT> Builder<Void, OUT> builder(Class<? extends OUT> respClass) {
return new Builder<>(Void.class, respClass);
}
/**
* Api构建器, 没有动态入参,写死的请求
*
* @return Api构建器
*/
static Builder<Void, String> builder() {
return new Builder<>(Void.class, String.class);
}
/**
* 执行请求
*
* @param in 入参
* @return OUT
*/
default OUT exec(IN in) {
return apply(in);
}
/**
* 空入参
*
* @return OUT
*/
default OUT exec() {
return apply(null);
}
/**
* 返回当前备份的构建器以创建其他副本
*
* @return Api.BuilderEditor<IN, OUT>
*/
Api.BuilderEditor<IN, OUT> customize();
class Builder<IN, OUT> {
public static final RestTemplate DEFAULT_CLIENT;
private static final Logger LOG = LoggerFactory.getLogger(Builder.class);
// init client
static {
try {
// 可以自己定制下
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(null, null, null);
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
HttpClientBuilder
.create()
.setSSLContext(context)
.build()
);
factory.setConnectTimeout(50000);
DEFAULT_CLIENT = new RestTemplate(factory);
// 处理500的请求直接抛错的问题
DEFAULT_CLIENT.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(@NonNull ClientHttpResponse resp) throws IOException {
System.out.println(resp.getStatusCode());
System.out.println(resp.getHeaders());
System.out.println(resp.getBody());
return false;
}
@Override
public void handleError(@NonNull ClientHttpResponse resp) {
}
});
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new RuntimeException(e);
}
}
}
// base data
private final Class<? extends IN> reqClass;
private final Class<? extends OUT> respClass;
private String url;
private HttpMethod method = HttpMethod.GET;
private MultiValueMap<String, Function<IN, String>> header;
private List<Function<IN, Map<String, String>>> headers;
private Map<String, Function<IN, Object>> param;
private Map<String, Function<IN, Object>> path;
private Set<String> pathValPlaceholder;
private Function<IN, ?> body;
private RestOperations client = DEFAULT_CLIENT;
private Function<Object, OUT> resultMapper;
// feature
/**
* 以application/x-www-form-urlencoded方式提交body
* 默认为application/json格式
*/
private boolean formData = false;
/**
* 请求信息打印
*/
private boolean log = false;
/**
* 自动从param中取path需要到参数
* 不需要通过方法 {@link #path(String, Function)} {@link #path(String, Object)} 设置值,不过优先级是这两个方法高
*/
private boolean autoMapParamToPath = true;
public Builder(Class<? extends IN> reqClass, Class<? extends OUT> respClass) {
this.reqClass = reqClass;
this.respClass = respClass;
}
/**
* 设置请求客户端
*/
public Builder<IN, OUT> client(RestOperations client) {
this.client = client;
return this;
}
/**
* 将 body 以 form data 表单提交
*/
public Builder<IN, OUT> enableFormData() {
this.formData = true;
return this;
}
/**
* 开启日志
*/
public Builder<IN, OUT> enableLog() {
this.log = true;
return this;
}
public Builder<IN, OUT> disableAutoMapParamToPath() {
this.autoMapParamToPath = false;
return this;
}
/**
* 将入参完全映射到param
*/
public Builder<IN, OUT> mapToParam() {
PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(reqClass);
if (this.param == null) {
this.param = new HashMap<>(propertyDescriptors.length);
}
mapTo(param, propertyDescriptors);
return this;
}
/**
* 将入参完全映射到path
*/
public Builder<IN, OUT> mapToPath() {
PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(reqClass);
if (this.path == null) {
this.path = new HashMap<>(propertyDescriptors.length);
}
mapTo(path, propertyDescriptors);
return this;
}
/**
* 将入参完全映射到body
*/
public Builder<IN, OUT> mapToBody() {
this.body = in -> in;
return this;
}
private void mapTo(Map<String, Function<IN, Object>> container, PropertyDescriptor[] data) {
for (PropertyDescriptor propertyDescriptor : data) {
// 忽略getClass()方法
if ("class".equals(propertyDescriptor.getName())) {
continue;
}
container.put(propertyDescriptor.getName(), in -> {
if (Objects.isNull(in)) {
return null;
}
try {
Method readMethod = propertyDescriptor.getReadMethod();
readMethod.setAccessible(true);
return readMethod.invoke(in);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
});
}
}
/**
* 设置请求地址
*/
public Builder<IN, OUT> url(String url) {
this.url = url;
return this;
}
/**
* 设置请求方式
*/
public Builder<IN, OUT> method(HttpMethod method) {
this.method = method;
return this;
}
/**
* 设置请求头
*/
public Builder<IN, OUT> header(String key, String value) {
if (this.header == null) {
this.header = new LinkedMultiValueMap<>();
}
this.header.add(key, in -> value);
return this;
}
/**
* 设置请求头
*/
public Builder<IN, OUT> header(String key, Function<IN, String> valueFunc) {
if (this.header == null) {
this.header = new LinkedMultiValueMap<>();
}
this.header.add(key, valueFunc);
return this;
}
public Builder<IN, OUT> headers(Function<IN, Map<String, String>> mapFunc) {
if (this.headers == null) {
this.headers = new ArrayList<>();
}
headers.add(mapFunc);
return this;
}
/**
* 设置请求参数
*/
public Builder<IN, OUT> param(String key, Object value) {
if (this.param == null) {
this.param = new HashMap<>(2);
}
this.param.put(key, in -> value);
return this;
}
/**
* 设置请求参数
*/
public Builder<IN, OUT> param(String key, Function<IN, Object> valueFunc) {
if (this.param == null) {
this.param = new HashMap<>(2);
}
this.param.put(key, valueFunc);
return this;
}
/**
* 设置请求路径值
*/
public Builder<IN, OUT> path(String key, Object value) {
if (this.path == null) {
this.path = new HashMap<>(2);
}
this.path.put(key, in -> value);
return this;
}
/**
* 设置请求路径值
*/
public Builder<IN, OUT> path(String key, Function<IN, Object> valueFunc) {
if (this.path == null) {
this.path = new HashMap<>(2);
}
this.path.put(key, valueFunc);
return this;
}
/**
* 设置请求体
*/
public Builder<IN, OUT> body(Object body) {
this.body = in -> body;
return this;
}
/**
* 设置请求体
*/
public Builder<IN, OUT> body(Function<IN, Object> body) {
this.body = body;
return this;
}
/**
* 设置请求体
*/
public Builder<IN, OUT> body(Consumer<Map<String, Object>> body) {
Map<String, Object> bodyMap = new HashMap<>(1);
body.accept(bodyMap);
this.body = in -> bodyMap;
return this;
}
/**
* 设置请求体
*/
public Builder<IN, OUT> multiValBody(Consumer<MultiValueMap<String, Object>> body) {
MultiValueMap<String, Object> bodyMap = new LinkedMultiValueMap<>();
body.accept(bodyMap);
this.body = in -> bodyMap;
return this;
}
public Builder<IN, OUT> resultMapper(Function<Object, OUT> func) {
this.resultMapper = func;
return this;
}
public Api<IN, OUT> build() {
check();
handlePathValue();
Builder<IN, OUT> ref = this;
return new Api<IN, OUT>() {
private BuilderEditor<IN, OUT> editor = null;
@Override
public BuilderEditor<IN, OUT> customize() {
// lazy load
if (editor == null) {
editor = BuilderEditor.clone(ref);
}
return editor;
}
@Override
public OUT apply(IN in) {
return execution(in);
}
};
}
private OUT execution(IN in) {
MultiValueMap<String, String> mapHeader = apply(in, header);
if (Objects.nonNull(headers)) {
if (mapHeader == null) {
mapHeader = new LinkedMultiValueMap<>();
}
for (Function<IN, Map<String, String>> func : headers) {
Map<String, String> apply = func.apply(in);
for (String key : apply.keySet()) {
mapHeader.add(key, apply.get(key));
}
}
}
Object mapBody = Objects.nonNull(body) ? body.apply(in) : null;
// adapt, format body data to the MultiValueMap
if (formData) {
mapHeader = null == mapHeader ? new LinkedMultiValueMap<>(1) : mapHeader;
mapHeader.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
mapBody = bodyToMultiValueMap(mapBody);
}
HttpEntity<Object> entity;
if (empty(mapHeader) && null == mapBody) {
entity = null;
} else {
entity = new HttpEntity<>(mapBody, mapHeader);
}
// extract param and path data
Map<String, Object> mapParam = apply(in, param);
Map<String, Object> mapPath = apply(in, path);
// log around weave start
StopWatch stopWatch = null;
if (log) {
stopWatch = new StopWatch();
stopWatch.start();
}
OUT res;
if (Objects.isNull(resultMapper)) {
res = client.exchange(
splicingUriValues(url, mapParam, mapPath),
method, entity,
respClass).getBody();
} else {
Object data = client.exchange(
splicingUriValues(url, mapParam, mapPath),
method, entity,
Object.class).getBody();
res = resultMapper.apply(data);
}
// log around weave end
if (log) {
assert stopWatch != null;
stopWatch.stop();
String result = null != res ? res.toString() : "";
LOG.info("\n╔═════════════════请求接口═════════════════╗\n" +
"║请求地址:{}\n" +
"║请求方式:{}\n" +
"║请求头:{}\n" +
"║请求参数:{}\n" +
"║ {}\n" +
"║请求体:{}\n" +
"║返回数据:{}\n" +
"║请求耗时:{}\n" +
"╚═════════════════════════════════════════╝\n",
url,
method,
mapHeader,
mapParam,
mapPath,
mapBody,
(result.length() >= 100 ? result.substring(0, 100) + " ..." : result).replace("\n", ""),
stopWatch.getTotalTimeMillis() + "ms"
);
}
return res;
}
private static final Pattern PATH_VAL_PATTERN = Pattern.compile("\\{(.*?)}");
private void handlePathValue() {
Matcher matcher = PATH_VAL_PATTERN.matcher(url);
Set<String> set = new HashSet<>();
while (matcher.find()) {
set.add(matcher.group(1));
}
this.pathValPlaceholder = set;
}
private void check() {
if (null == url || url.isEmpty()) {
throw new IllegalArgumentException("url不能为空");
}
if (null == method) {
throw new IllegalArgumentException("method不能为空");
}
}
/**
* adapt form data request
* 将body转换成 MultiValueMap
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private MultiValueMap<String, Object> bodyToMultiValueMap(Object body) {
if (Objects.isNull(body)) {
return null;
}
if (body instanceof MultiValueMap) {
return (MultiValueMap<String, Object>) body;
} else if (body instanceof Map) {
Map<String, Object> bodyMap = (Map) body;
MultiValueMap<String, Object> res = new LinkedMultiValueMap<>(bodyMap.size());
bodyMap.forEach((k, v) -> {
if (v instanceof List) {
res.put(k, (List) v);
} else if (v.getClass().isArray()) {
res.put(k, Arrays.asList((Object[]) v));
} else if (v instanceof Collection) {
res.put(k, new ArrayList<>((Collection) v));
} else if (v instanceof Iterable) {
Iterable it = (Iterable) v;
for (Object o : it) {
res.add(k, o);
}
} else {
res.add(k, v);
}
});
return res;
} else {
PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(body.getClass());
MultiValueMap<String, Object> res = new LinkedMultiValueMap<>(propertyDescriptors.length);
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
// 忽略getClass()方法
if ("class".equals(propertyDescriptor.getName())) {
continue;
}
Method readMethod = propertyDescriptor.getReadMethod();
readMethod.setAccessible(true);
try {
res.add(propertyDescriptor.getName(), readMethod.invoke(body));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
return res;
}
}
private String splicingUriValues(String url,
Map<String, Object> requestParams,
Map<String, Object> requestPaths) {
return splicingUriValues(url, requestParams, requestPaths, pathValPlaceholder, autoMapParamToPath);
}
public static String splicingUriValues(String url,
Map<String, Object> requestParams) {
return splicingUriValues(url, requestParams, null, Collections.emptySet(), false);
}
/**
* 填充 path 和 param
*/
public static String splicingUriValues(String url,
Map<String, Object> requestParams,
Map<String, Object> requestPaths,
Set<String> pathValPlaceholder,
boolean autoMapParamToPath) {
boolean emptyParams = empty(requestParams);
boolean emptyPaths = empty(requestPaths);
if (emptyParams && emptyPaths) {
if (empty(pathValPlaceholder)) {
return url;
} else {
throw new IllegalStateException("path value : " + pathValPlaceholder + " required");
}
}
StringBuilder sb = new StringBuilder();
String res = url;
// handle path
if (!emptyPaths) {
for (String key : requestPaths.keySet()) {
if (pathValPlaceholder.contains(key)) {
Object val = requestPaths.get(key);
if (Objects.isNull(val) && !autoMapParamToPath) {
throw new IllegalStateException("path value : {" + key + "} required");
} else {
res = res.replace("{" + key + "}", toParamStr(val));
// consume placeholder
pathValPlaceholder.remove(key);
}
}
}
}
sb.append("?");
// handle param
if (!emptyParams) {
for (String key : requestParams.keySet()) {
// handle surplus path if function open
if (autoMapParamToPath) {
if (pathValPlaceholder.contains(key)) {
Object val = requestParams.get(key);
if (Objects.isNull(val)) {
throw new IllegalArgumentException("path value : {" + key + "} required");
} else {
res = res.replace("{" + key + "}", toParamStr(val));
// consume placeholder
pathValPlaceholder.remove(key);
}
continue;
}
}
// do handle param
Object val = requestParams.get(key);
if (Objects.isNull(val)) {
continue;
}
sb.append(key).append("=").append(toParamStr(val)).append("&");
}
}
// check path has fill complete
if (!pathValPlaceholder.isEmpty()) {
throw new IllegalStateException("path value : " + pathValPlaceholder + " required");
}
int andIndex = sb.lastIndexOf("&");
if (andIndex != -1) {
sb.deleteCharAt(andIndex);
}
return res + sb;
}
/**
* 如果是迭代器或者数组 就用逗号拼接
* 其他就toString
*/
@SuppressWarnings("rawtypes")
private static String toParamStr(Object val) {
String value;
if (val instanceof Iterable) {
Iterable it = (Iterable) val;
StringBuilder valSb = new StringBuilder();
for (Object o : it) {
valSb.append(o.toString()).append(",");
}
value = valSb.deleteCharAt(valSb.lastIndexOf(",")).toString();
} else if (val.getClass().isArray()) {
value = Arrays.stream((Object[]) val)
.map(Object::toString)
.collect(Collectors.joining(","));
} else {
value = val.toString();
}
return value;
}
private <T> MultiValueMap<String, T> apply(IN in, MultiValueMap<String, Function<IN, T>> target) {
if (empty(target)) {
return null;
}
MultiValueMap<String, T> res = new LinkedMultiValueMap<>(target.size());
target.forEach((key, value) -> {
List<T> values = value.stream()
.map(func -> func.apply(in))
.collect(Collectors.toList());
res.addAll(key, values);
});
return res;
}
/**
* extract raw data to the map container
*
* @param in raw data
* @param funcMap map function
* @param <T> T
* @return map
*/
private <T> Map<String, T> apply(IN in, Map<String, Function<IN, T>> funcMap) {
if (empty(funcMap)) {
return null;
}
Map<String, T> res = new HashMap<>(funcMap.size());
funcMap.forEach((key, value) -> res.put(key, value.apply(in)));
return res;
}
private static boolean empty(Map<String, ?> target) {
return null == target || target.isEmpty();
}
private static boolean empty(Set<?> target) {
return null == target || target.isEmpty();
}
}
class BuilderEditor<IN, OUT> extends Builder<IN, OUT> {
private static <IN, OUT> BuilderEditor<IN, OUT> clone(Builder<IN, OUT> ref) {
return new BuilderEditor<>(ref);
}
private BuilderEditor(Builder<IN, OUT> ref) {
super(ref.reqClass, ref.respClass);
// copy column
super.url = ref.url;
super.method = ref.method;
super.path = ref.path != null ? new HashMap<>(ref.path) : null;
super.param = ref.param != null ? new HashMap<>(ref.param) : null;
super.header = ref.header != null ? new LinkedMultiValueMap<>(ref.header) : null;
super.body = ref.body;
super.pathValPlaceholder = ref.pathValPlaceholder != null ? new HashSet<>(ref.pathValPlaceholder) : null;
super.client = ref.client != null ? ref.client : DEFAULT_CLIENT;
super.formData = ref.formData;
super.log = ref.log;
super.autoMapParamToPath = ref.autoMapParamToPath;
}
public BuilderEditor<IN, OUT> disableLog() {
super.log = false;
return this;
}
public BuilderEditor<IN, OUT> disableFormData() {
super.formData = false;
return this;
}
public BuilderEditor<IN, OUT> enableAutoMapParamToPath() {
super.autoMapParamToPath = true;
return this;
}
public BuilderEditor<IN, OUT> removeHeader(String key) {
super.header.remove(key);
return this;
}
public BuilderEditor<IN, OUT> removeParam(String key) {
super.param.remove(key);
return this;
}
public BuilderEditor<IN, OUT> removePath(String key) {
super.path.remove(key);
return this;
}
}
}
demo
public static void main(String[] args) {
Api<String, String> test = Api.builder(String.class, String.class)
.method(HttpMethod.GET)
.url("https://www.baidu.com/{path}")
.path("path", in -> in)
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.param("timestamp", System.currentTimeMillis())
.enableLog()
.client(Api.Builder.DEFAULT_CLIENT)
.build();
test.apply("测试");
}