文章目录
- 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 {//抽象类中实现接口的方法@Overridepublic 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 {@Overridepublic 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;//如果是GETif (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 {@Overridepublic 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 三方系统接口的返回数据*/@Overridepublic 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();//拼接完整的三方系统接口的URLString 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 {@Overridevoid 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功能