实现思路
最近做的项目中,并没有使用分布式RPC框架,各个服务之间的通信走HTTP,通过nginx配置的负载均衡分发到各个实例以达到分布式通信的目的。
各个服务之间的调度走http造成了代码中存在大量的RestTemplate调用,为了避免后续开发过程中难以管理,于是我基于JDK的动态代理简单实现了一个RestInterface
,通过类似于SpringMVC的方式来管理所有的HTTP请求。
步骤1:创建几个注解
GET
与POST
,一看就很像是springmvc的PostMapping
与GetMapping
。
package com.tydic.config.restconfig;
import java.lang.annotation.*;
/**
* @author Gmw
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface GET {
String path() default "";
}
package com.tydic.config.restconfig;
import java.lang.annotation.*;
/**
* @author Gmw
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface POST {
String path() default "";
}
这两个注解负责标注在RestInterface
接口方法上,表示该方法使用POST方法还是GET方法来调用。
然后是请求体注解,偷懒所以我直接使用了RequestBody
注解,没啥大区别,其实所有注解都可以用spring的。
然后是GET请求的参数注解,这回没有偷懒,自己创建了一个PARAM
:
package com.tydic.config.restconfig;
import java.lang.annotation.*;
/**
* RequestParam
* @author Gmw
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Inherited
@Documented
public @interface PARAM {
String value();
}
步骤2:HTTP调用接口RestInterface
/**
* 统一管理代码中的HTTP请求
* <p>
* <b>注意:</b>
* GET请求只能携带路径参数;POST请求只能携带请求体(v1.0)
* </p>
*
* @author Gmw
*/
public interface RestInterface {
/**
* 通过工单ID获取用户信息
*
* @param parameter 请求体
* @return 返回体
*/
@POST(path = "/wm/api/sysmgr/users/info")
JSONObject queryUserInfo(@RequestBody JSONObject parameter);
/**
* 获取订单更新
*
* @param parameter 请求体
* @return 返回体
*/
@POST(path = "/wm/api/worksheets/receipts/order/updates")
JSONObject queryOrderUpdates(@RequestBody JSONObject parameter);
/**
* 提交订单
*
* @param parameter 请求体
* @return 返回体
*/
@POST(path = "/wm/api/orders/submit")
JSONObject orderSubmit(@RequestBody JSONObject parameter);
}
接口很简单,通过注解的标注,以及大量的注释,就可以很清晰地知道每个方法调用的作用。
步骤3:实现动态代理类
动态代理的实现很简单,实现InvocationHandler
接口即可:
private static class RestInterfaceImpl implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ...
}
}
其中三个参数主要使用到method
与args
,代表当前代理的接口方法以及方法入参实例。
所有的与RestInterface
接口类似的HTTP接口都可以使用这个RestInterfaceImpl
作为代理实现,因为每个Rest接口的代理逻辑都是一样的,解析注解、组装请求参数、发送http请求并返回。
接下来通过Spring的配置类来提供这个代理类的实现,并封装为Spring组件:
@Configuration
public class RestInterfaceConfig {
private final RestInterfaceImpl proxyImpl;
public RestInterfaceConfig(
@Value("${nginx.protocol}") String protocol,
@Value("${nginx.host}") String host,
@Value("${nginx.port}") String port,
LogServiceImpl logService) {
restInterface = new RestInterfaceImpl(protocol, host, port, logService);
}
@Bean(name = "restInterface")
public RestInterface createRestInterface() {
return (RestInterface) Proxy.newProxyInstance(
// 用于加载代理类的加载器,用接口的就好
RestInterface.class.getClassLoader(),
// 定义该代理类要实现几个接口
new Class[]{RestInterface.class},
// 代理类实例
proxyImpl);
}
}
如果需要实现多个Rest接口的Bean提供,那么只需要:
@Bean(name = "anotherRestInterface")
public RestInterface createAnotherRestInterface() {
return (AnotherRestInterface) Proxy.newProxyInstance(
// 用于加载代理类的加载器,用接口的就好
RestInterface.class.getClassLoader(),
// 定义该代理类要实现几个接口
new Class[]{AnotherRestInterface.class},
// 代理类实例
proxyImpl);
}
/// ...
RestInterfaceImpl接口是线程安全的(不保存状态),所以多个Bean都可以共用这个代理实现。
完整代码
以下是RestInterfaceConfig
完整代码:
package com.tydic.config.restconfig;
import com.alibaba.fastjson.JSONObject;
import com.tydic.business.RestInterface;
import com.tydic.constant.Constants;
import com.tydic.log.LogServiceImpl;
import com.tydic.utils.BaseUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Gmw
*/
@Slf4j
@Configuration
public class RestInterfaceConfig {
private final RestInterfaceImpl restInterface;
public RestInterfaceConfig(
@Value("${nginx.protocol}") String protocol,
@Value("${nginx.host}") String host,
@Value("${nginx.port}") String port,
LogServiceImpl logService) {
restInterface = new RestInterfaceImpl(protocol, host, port, logService);
}
@Bean(name = "restInterface")
public RestInterface createRestInterface() {
return (RestInterface) Proxy.newProxyInstance(
// 用于加载代理类的加载器,用接口的就好
RestInterface.class.getClassLoader(),
// 定义该代理类要实现几个接口
new Class[]{RestInterface.class},
// 代理类实例
restInterface);
}
private static class RestInterfaceImpl implements InvocationHandler {
private final String restPrefix;
private final LogServiceImpl logService;
public RestInterfaceImpl(String protocol, String host, String port, LogServiceImpl logService) {
this.logService = logService;
restPrefix = String.format("%s://%s:%s", protocol, host, port);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
RequestMetadata requestMetadata = getRequestMedataFromMethod(method, restPrefix);
ParamWrapper paramWrapper = getParamFromMethodAndArgs(method.getParameters(), args);
urlComplete(requestMetadata, paramWrapper.reqParams);
RestTemplate restTemplate = BaseUtils.newRestTemplateInstance();
ResponseEntity<String> response = restInvoke(restTemplate, requestMetadata, paramWrapper);
return JSONObject.parseObject(response.getBody(), method.getReturnType());
} catch (Exception e) {
logService.writeErrorLog(e, method.getDeclaringClass(), method, args, Constants.KeyName.restInterface);
throw e;
}
}
private void urlComplete(RequestMetadata requestMetadata, Map<String, String> reqParams) {
if (reqParams != null && !reqParams.isEmpty()) {
String url = requestMetadata.getUrl() + ("?" + reqParams
.keySet().stream().map(key -> String.format("%s={%s}", key, key)).collect(Collectors.joining("&")));
requestMetadata.setUrl(url);
}
}
private ResponseEntity<String> restInvoke(RestTemplate rest,
RequestMetadata request,
ParamWrapper params) {
log.debug("即将发送请求: ");
log.debug("request: {}", request);
log.debug("params: {}", params);
if ("get".equals(request.getMethod())) {
return rest.getForEntity(request.url, String.class, params.reqParams);
}
HttpEntity<Object> requestEntity = new HttpEntity<>(params.requestBody, request.getHeaders());
return rest.exchange(request.url, HttpMethod.POST, requestEntity, String.class);
}
private ParamWrapper getParamFromMethodAndArgs(Parameter[] parameters, Object[] args) {
ParamWrapper wrapper = new ParamWrapper();
for (int index = 0; index < parameters.length; index++) {
Parameter parameter = parameters[index];
Object parameterValue = args[index];
if (parameter.isAnnotationPresent(RequestBody.class)) {
wrapper.setRequestBodyType(parameter.getType());
if (parameter.getAnnotation(RequestBody.class).required() && parameterValue == null) {
// 请求体不能为空
throw new IllegalArgumentException("请求体不能为空");
}
wrapper.setRequestBody(parameterValue);
} else if (parameter.isAnnotationPresent(PARAM.class)) {
PARAM paramAnno = parameter.getAnnotation(PARAM.class);
if (paramAnno.required() && parameterValue == null) {
// 请求参数不能为空
throw new IllegalArgumentException("请求参数[" + paramAnno.value() + "]不能为空");
}
wrapper.setReqParams(paramAnno.value(), parameterValue);
}
}
return wrapper;
}
private RequestMetadata getRequestMedataFromMethod(Method method, String prefix) {
RequestMetadata metadata = new RequestMetadata();
List<Annotation> methodAnnotations = Arrays.asList(method.getAnnotations());
metadata.setUrl(findUrl(methodAnnotations, prefix));
metadata.setMethod(findMethod(methodAnnotations));
metadata.setHeaders(generateCommonHttpHeaders());
return metadata;
}
private String findMethod(List<Annotation> methodAnnotations) {
Annotation reqPathAnno = methodAnnotations
.stream().filter(anno -> anno instanceof POST || anno instanceof GET)
.findFirst().orElseThrow(() -> new RuntimeException("未找到HTTP方法定义"));
return reqPathAnno instanceof POST ? "post" : "get";
}
private String findUrl(List<Annotation> methodAnnotations, String prefix) {
Annotation reqPathAnno = methodAnnotations
.stream().filter(anno -> anno instanceof POST || anno instanceof GET)
.findFirst().orElseThrow(() -> new RuntimeException("未找到HTTP路径定义"));
String path;
if (reqPathAnno instanceof POST) {
path = ((POST) reqPathAnno).path();
} else {
path = ((GET) reqPathAnno).path();
}
return prefix + (path.startsWith("/") ? path : "/" + path);
}
private HttpHeaders generateCommonHttpHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(requestAttributes -> ServletRequestAttributes.class.isAssignableFrom(requestAttributes.getClass()))
.map(requestAttributes -> ((ServletRequestAttributes) requestAttributes).getRequest())
.ifPresent(req -> {
headers.set(Constants.CustomHeader.WM_TENANT_ID, req.getHeader(Constants.CustomHeader.WM_TENANT_ID));
headers.set(Constants.CustomHeader.WM_APP_ID, req.getHeader(Constants.CustomHeader.WM_APP_ID));
headers.set(Constants.CustomHeader.WM_TOUCH_POINT, req.getHeader(Constants.CustomHeader.WM_TOUCH_POINT));
});
return headers;
}
private static class RequestMetadata {
private String url;
private String method;
private HttpHeaders headers;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public HttpHeaders getHeaders() {
return headers;
}
public void setHeaders(HttpHeaders headers) {
this.headers = headers;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("url", url)
.append("method", method)
.append("headers", headers)
.toString();
}
}
private static class ParamWrapper {
private Map<String, String> reqParams = new HashMap<>();
private Class<?> requestBodyType;
private Object requestBody;
public Map<String, String> getReqParams() {
return reqParams;
}
public void setReqParams(Map<String, String> reqParams) {
this.reqParams = reqParams;
}
public Class<?> getRequestBodyType() {
return requestBodyType;
}
public void setRequestBodyType(Class<?> requestBodyType) {
this.requestBodyType = requestBodyType;
}
public Object getRequestBody() {
return requestBody;
}
public void setRequestBody(Object requestBody) {
this.requestBody = requestBody;
}
public void setReqParams(String paramName, Object paramValue) {
if (paramValue != null) {
reqParams.put(paramName, paramValue.toString());
}
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("reqParams", reqParams)
.append("requestBodyType", requestBodyType)
.append("requestBody", requestBody)
.toString();
}
}
}
}
总结
如此便实现了统一的http管理,初版只支持JSON序列化,很多都是写死的比如头部的获取等等,而且请求方法也只支持GET、POST,且只有POST能携带请求体,只有GET能携带请求参数。后续找时间扩展这个玩意。