文章目录
- 1、需求背景
- 2、接口+抽象类+具体实现类
- 3、疑问
- 4、存在的问题
- 5、通过反射加载SDK并完成调用
- 5、补充:关于业务网关
- 7、补充:关于SDK的开发
关键点:
- 接口+抽象类(半抽象半实现)+具体实现类
- 业务网关
- 反射加载SDK,完成统一调用
半路接手一个需求,需要从自己系统出发,经过业务网关的统一校验和转发,来请求第三方供应商系统的接口,整理下看同事代码学到的一点思路。
1、需求背景
第三方供应商需要上架自己的产品到公司的交易平台,但用户使用产品时,最后一步请求的自然是供应商自己的服务器资源和API。关于这个需求的实现思路,大致是在交易平台需要做接口有效性校验、服务实例有效性校验等,以及消费数据记录落库,最后转发到供应商接口去请求资源(既然是请求别人的系统,那就涉及到怎么通过人家的鉴权系统)。
@PostMapping("/data/{platform}/{apiId}")
public Object redirect(@PathVariable String platform, @PathVariable String apiId,
@RequestBody Map<String, Object> parameterMap, HttpServletRequest request) {
//直接把API的ID放进请求参数里,后面用完了,再调三方API时去掉就行
parameterMap.put("apiId", apiId);
return redirectHandler.redirect(platform, ServletUtils.getHeaders(request), parameterMap);
}
这里给访问所有三方系统接口一个统一的入口,做为业务网关(后面展开说),接口传参中:
- paltform:确定是哪个第三方系统
- apiID:用来标识想请求第三方系统哪个的API接口,通过这个ID,可以在库里查到API的路径、三方系统的host、密钥、以及后面会提到的SDK的存储路径、SDK里的核心方法名等信息
- parameterMap:用户传入的请求参数
- request:Http请求对象
其中用工具类获取下HTTP请求的全部请求头信息存入Map。
public class ServletUtils{
/**
* 获取Http请求的请求头信息
*/
public static Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
}
return map;
}
}
2、接口+抽象类+具体实现类
既然需要对接很多第三方供应商系统,去调用第三方系统的API,那就考虑定义一个接口,里面抽象出一个做鉴权、转发的方法,对接不同的供应商系统时,去实现这个接口,然后走不同的实现。
public interface ApiRedirectHandler {
/**
* @param headerMap 请求头参数Map
* @param paramMap 对第三方接口的请求参数
* @return 返回第三方接用调用的结果
*/
Object redirect(Map<String, String> headerMap, Map<String, Object> paramMap);
}
前面提到,在交易平台要做一些校验和消费记录落库的操作,这些是对接所有三方系统的公共步骤,而后面请求第三方系统接口肯定要做的鉴权认证以及转发或者调用,则属于各个三方系统的定制化行为(因为一个系统有一个系统的认证方式,A系统用APP密钥、B系统可能用sign验签)。因此,考虑在接口下面垫一个抽象类,抽象类中,实现接口中的转发方法,里面做校验、记录落库等操作,同时调用本抽象类自己的抽象request方法(这个方法里做第三方系统的定制化的认证和转发或调用)。这样,对接不同的三方系统,只需就继承这个抽象类,实现里面的request方法,做自己的认证和转发即可。
总结
:全抽象的接口,过渡到半抽象的抽象类,抽象类中实现接口的抽象方法时,方法体中写一部分公共逻辑 + 调用本抽象类自己的一个抽象方法B,这个抽象方法B就给以后的普通类去继承和重写。
@Slf4j
public abstract class AbstractRedirectHandler implements ApiRedirectHandler {
//抽象类中实现接口的方法
@Override
public Object redirect(Map<String, String> headerMap, Map<String, Object> paramMap) {
//todo: 1.请求有效性验证
//从请求参数paramMap中拿到你要调用APIId,然后查到的三方系统接口的路径、host等信息
ApiInfo apiDetailVo = queryApiInfo(paramMap);
//API的ID用完了,它不是三方系统接口需要的请求参数,移除
paramMap.remove("apiId");
//todo: 2.服务实例有效性验证
//request中去写不同三方系统的鉴权、转发或调用逻辑
val responseData = request(headerMap, paramMap, apiDetailVo);
//todo: 3.记录消费记录
//返回第三方接口的响应结果
return responseData;
}
/**
* API转发请求,对接时,针对不同的三方系统去定制化实现
*
* @param headerMap 头信息
* @param paramMap 请求参数
* @Param apiDetailVo 接口信息,如接口路径、服务器host
* @return 返回第三方接用调用的结果
*/
protected abstract Object request(Map<String, String> headerMap, Map<String, Object> paramMap, ApiDetailVo apiDetailVo);
}
比如现在对接001号系统:按它们系统支持的方式做认证,比如header中添加APP密钥,然后组装请求URL成一个HttpRequest对象,发送Http请求即可完成对三方系统API的调用。
public class System001RedirectHandler extends AbstractRedirectHandler {
@Override
public Object request(Map<String, String> headerParam, Map<String, Object> paramMap, ApiDetailVo apiDetailVo) {
//拿到三方系统的服务器HOST以及接口路径
String url = apiDetailVo.getHost() + apiDetailVo.getPath();
//拿到三方系统接口的请求方式,POST还是GET...
val method = Method.valueOf(apiDetailVo.getRequestMethod());
//使用Hutool工具类的HTTP请求对象,方便后面调用现成的方法来发送HTTP请求
HttpRequest request = null;
//如果是GET
if (method.equals(Method.GET)) {
String headerBody = "";
StringBuffer body = new StringBuffer();
StringBuffer param = new StringBuffer();
for (String key : paramMap.keySet()) {
body.append(key).append("=").append(paramMap.get(key)).append("&");
param.append(key).append("=").append(URLEncoder.encode((String) paramMap.get(key), StandardCharsets.UTF_8)).append("&");
}
//拼接出一个GET请求的完整路径
if (param.length() > 0) {
headerBody = body.substring(0, body.length() - 1);
url = url + "?" + param.substring(0, param.length() - 1);
}
//创建request请求对象
request = HttpUtil.createGet(url);
//请求头中加入001系统的认证秘钥,以便通过认证
request.addHeaders(getHeader(headerBody, apiDetailVo.getAppSecret()));
} else {
//POST请求
request = HttpUtil.createPost(url).contentType("application/json");
String body = JSON.toJSONString(paramMap);
//组装请求头和请求体
request.addHeaders(getHeader(body, apiDetailVo.getAppSecret())).body(body);
}
//库里存的API有要求超时时间
if (apiDetailVo.getTimeout() > 0) {
request.timeout(apiDetailVo.getTimeout());
}
//发送HTTP请求,拿到响应
val httpResponse = request.execute();
return JSON.parseObject(httpResponse.body());
}
}
3、疑问
给所有三方系统接口的调用一个统一的请求入口,怎么实现根据传入的第三方系统类型platformType,来选择不同的实现类对象:考虑把转发接口ApiRedirectHandler的所有实现类放进一个List,遍历去匹配传入的platformType,匹配,则找到了三方系统对应的处理器实现类。找不到,就给个默认的处理器实现类。
@RequiredArgsConstructor
public class CompositeRedirectHandler {
private ArrayList<ApiRedirectHandler> handlers = new ArrayList<>();
public CompositeRedirectHandler(ArrayList<ApiRedirectHandler> redirectHandlerList) {
handlers = redirectHandlerList;
}
public Object redirect(String platform, Map<String, String> headerMap, Map<String, Object> paramMap) {
//给一个默认的通用执行器实现类对象
ApiRedirectHandler execHandler = handlers.get(0);
//根据平台信息匹配到ApiRedirectHandler接口的三方系统的实现类
for (ApiRedirectHandler handler : handlers) {
if (handler.isMatched(platform)) {
execHandler = handler;
break;
}
}
//用实现类去调用转发方法 ==> 抽象类(包含抽象方法request) ==> 各个三方系统对抽象类的实现 ==> 完成三方系统API的请求
return execHandler.redirect(headerMap, paramMap);
}
}
上面的接口中注入这个CompositeRedirectHandler对象,调用它的redirect方法,即可全部串起来。
private final CompositeRedirectHandler redirectHandler;
@PostMapping("/data/{platform}/{apiId}")
public Object redirect(@PathVariable String platform, @PathVariable String apiId,
@RequestBody Map<String, Object> parameterMap, HttpServletRequest request) {
parameterMap.put("apiId", apiId);
return redirectHandler.redirect(platform, ServletUtils.getHeaders(request), parameterMap);
}
4、存在的问题
如此,以后每对接一个三方系统,就得开发一个新的实现类,去按照他们系统支持的认证方式来做认证以及转发或调用。相当繁琐,现在考虑把这个认证的事交给三方系统自己去完成,比如让他们开发一个SDK,SDK里他们按照自己系统支持的认证方式,做能通过鉴权的操作(到底是header里放密钥还是做验签,我就不再关心了),以及组装HTTP请求,而我们只需要load这个SDK里的内容,传入请求参数和路径,做一个调用即可。
@Slf4j
public class CommonRedirectHandler extends AbstractRedirectHandler {
@Override
public Object request(Map<String, String> headerMap, Map<String, Object> paramMap, ApiDetailVo apiDetailVo) {
//根据apiDetailInfo加载对应的SDK,完成调用
//....
}
}
如此,我就只需要一个通用的实现类CommonRedirectHandler就可以实现对所有三方系统的对接,这个通用类中也实现了上面的抽象类的request方法,request方法中只需load SDK里三方系统开发者写的方法,传入请求路径和请求参数即可完成三方系统接口的调用。
5、通过反射加载SDK并完成调用
现在问题成了如何加载SDK,完成调用。 ⇒ 通过反射拿到核心类的对象,以及负责认证和转发请求的核心方法,最后完成调用即可。这里的反射直接用hutool这个强大的第三方依赖库。
<!--引入hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
加载SDK的示意代码:
@Slf4j
public class CommonRedirectHandler extends AbstractRedirectHandler {
/**
* 动态加载sdk,调用里面已经完成鉴权和转发的方法,以实现转发请求
*
* @param headerMap 头信息
* @param paramMap 请求参数
* @return 三方系统接口的返回数据
*/
@Override
public Object request(Map<String, String> headerMap, Map<String, Object> paramMap, ApiDetailVo apiDetailVo) {
//SDK的路径、类名、核心方法名
val jarFilePath = apiDetailVo.getSdkJarFilePath();
val classFullName = apiDetailVo.getClassFullName();
val invokeMethodName = apiDetailVo.getInvokeMethodName();
val httpMethod = apiDetailVo.getRequestMethod().toUpperCase();
//拼接完整的三方系统接口的URL
String apiUrl = apiDetailVo.getHost() + apiDetailVo.getPath();
log.info("file = {}", new File(jarFilePath));
//hutool工具类加载SDK成class对象
Class<?> clazz = ClassLoaderUtil.loadClass(new File(jarFilePath), classFullName);
//反射拿到构造方法对象
final val constructors = ReflectUtil.getConstructor(clazz);
Object instance = null;
try {
//SDK核心类的对象
instance = constructors.newInstance();
final val requestMethod = ReflectUtil.getMethodByName(clazz, invokeMethodName);
//调用
return requestMethod.invoke(instance, apiUrl, httpMethod, headerMap, paramMap);
} catch (InstantiationException e) {
log.error(e.getMessage());
throw new ServiceException(ExceptionCodeEnum.API_GATEWAY_REQUEST_API_ERROR);
} catch (IllegalAccessException e) {
log.error(e.getMessage());
throw new ServiceException(ExceptionCodeEnum.API_GATEWAY_REQUEST_API_ERROR);
} catch (InvocationTargetException e) {
log.error(e.getCause().getMessage());
throw new ServiceException(ExceptionCodeEnum.API_GATEWAY_REQUEST_API_ERROR);
}
}
}
5、补充:关于业务网关
本需求里,给请求第三方系统接口资源提供了一个统一的API入口,比如:
@POSTMapping(/data/{platformType}/{API_ID})
public Object redirect(@PathVariable String platformType, @PathVariable String API_ID,
@RequestBody Map<String, Object> requestParam, HttpServletRequset requset){
//.....
}
有了这个统一入口,请求三方系统资源就都从这个接口过,前面说的各种合法性、有效性校验、记录落库、转发等就可以在这里完成了,由此可见,其虽然不比常规的Gateway服务,比如SpringCloudGateway,但干的活儿是类似的,即校验和转发(路由),因此,称业务网关。
思路:给所有三方系统的api调用提供一个统一的入口(Api)
7、补充:关于SDK的开发
SDK,Software Development Kit,即软件开发工具包。简单说就是造轮子,实现一个小功能,别人引入,就能使用。往大了说,如Java开发工具包JDK,使用import引入相关的包:
import java.util.*;
往小了说,如文件上传的SDK,其他系统引入后就可用。关于SDK的开发,需要注意:
- 易用性:提供统一调用,用户不用关心内部实现的细节,只需知道调谁、传什么、能返回什么即可
//常见方式1.直接调用
FileManage.upload(String filePath);
//常见方式2.需要new对象
FileManage fileManage = new FileManage();
fileManage.upload(String filePath);
- 轻量依赖:尽量减少SDK本身对其他类库的依赖,以减少用户项目中的已有依赖和SDK依赖的冲突
- 结构清晰:如maven项目下,service包下编写业务逻辑、constant包下存放常量、utils包下放工具类
- 见名知意:不用看说明文档也知道这个方法是干啥用的
- 可扩展:提供接口或者抽象类对外,支持用户自己继承和按需写实现类,如密码相关SDK,做加密解密,起名PasswordHandler,其加密方法encode需要传入一个密码,一个加密器,这个加密器就可以提供成接口,用户可通过实现这个接口来自定义加密方式。
//加密器对象:按照非对称算法加密
Encoder encoder = new SignEncoder();
String password = PasswordHandler.encode("daihao9527", encoder);
//用户自己实现加密器接口
public MyEncoder implements Encoder {
@Override
void doEncode(){
//...用户自己写,如采用时间戳、或自定义的MD5工具类
Calendar calendar = Calendar.getInstance();
Long timestamp = calendar.getTime.getTime();
String sign = MD5Utils.getMD5Str(timestamp + secretKey);
//..
}
}
//此时,用户可以自己指定加密器
String password = PasswordHandler.encode("daihao9527", new MyEncoder());
SDK中的内容一般包括:
- 功能模块:实现功能
- API:SDK的门面,调用和使用功能的入口
- 文档:附相关使用说明和指引
- Demo:使用示例,运行Demo,直观体验SDK功能