公司需要新开发一个项目,功能在这里不需要Care,主要是基于我们之前的会员网关以及资金网关。这两个服务都是支持 GET/POST
的 https
请求的调用。因为我们这个项目是第三方支付,所以安全方面比较关注。请求方式使用的是 https
方式,然后数据在传输过程过程中需要把请求的数据进行加签,对于敏感数据(比如手机,实名,银行卡等)还需要加密。其中请求参数包括基本参数与业务参数,而对于响应参数的格式是 JSON
的数据
- 基本参数就是所有请求都需要传递的参数:主要包括接口名称,接口版本,编码字符集,签名,签名方式等等参数
- 业务参数就是针对不同业务所需要传递的业务参数
- 响应参数包括响应码与响应信息等参数
针对于以上场景可以得出:
- 调用服务都是基于 http 的
GET/POST
方式 - 调用服务需要对请求进行统一的处理:加密,加签
所以我想可以参照 Mybatis 的接口调用实现的方式,在服务之间我也可以使用 动态代理来实现调用。使用 AOP 主要有以下几点好处:
- 调用过程都是以上的公用处理逻辑
- 解耦,使用方不需要关心远程调用的细节、不直接依赖实现
- 灵活,客户端直接使用接口,可以灵活封装和修饰
- 可以更加方式的测试与
Mock
下面就是我的实现代码由于会员网关的调用逻辑与资金网关的调用逻辑类似,所以在分享的时候就只需要分享会员网关的伪代码:
1、会员网关调用类
该接口用于调用会员网关相关的接口,使用接口而不需要具体实现。具体的调用逻辑实现于相关的代理类中。针对于需要添加会员网关的接口调用,只需要在此处添加一个方法就可以了。
public interface MgsService {
/**
* 创建并激活会员
* @param request
* @return
*/
CreateMemberAccountResponse createMemberAccount(CreateActivateMemberRequest request);
}
2、网关代理抽象类
因为会员网关与资金网关的调用方式类似,所以抽象出了网关代理抽象类。它的作用就是代理网关接口,把网关接口的请求使用统一的方式进行处理,然后把请求参数通过 HTTP 把请求发送到网关,以及处理请求的响应值。处理过程如下所示:
- 处理请求参数
- 调用网关接口
- 返回请求参数
public abstract class AbstractGatewayProxyHandler implements InvocationHandler {
private static Logger LOGGER = LoggerFactory.getLogger(AbstractGatewayProxyHandler.class);
/**
* 网关调用 HTTP 处理类
*/
private GateWayInvokeService gateWayInvokeService;
/**
* 接口方法与Service的映射
*/
private Map<String, String> services;
protected AbstractGatewayProxyHandler(GateWayInvokeService gateWayInvokeService, Map<String, String> services){
this.gateWayInvokeService = gateWayInvokeService;
this.services = services;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String invokeSeqUuid = UuidUtil.getRandomUuid();
Class<?> clazz = method.getDeclaringClass();
if (clazz == Object.class) {
return method.invoke(proxy, args);
}
String methodName = method.getName();
// 1、获取service名称
String serviceName = getServiceName(methodName);
LOGGER.info("gateway service req, serialNo: {}, method: {}, args: {}", invokeSeqUuid, methodName, GatewayJson.toMaskJsonString(args));
// 2、解析请求参数
Object parameterInput = parseParameterInput(args);
// 3、装载网关请求参数
processGateWayRequestData(parameterInput, serviceName);
// 4、调用网关
LOGGER.info("send to gateway: serialNo: {}, serviceName: {}, parameterInput: {}", invokeSeqUuid, serviceName, GatewayJson.toMaskJsonString(parameterInput));
String responseData = gateWayInvokeService.invoke(parameterInput);
String response = URLDecoder.decode(responseData, "utf-8");
LOGGER.info("return by gateway : serialNo: {}, serviceName: {}, gateway response: {}", invokeSeqUuid, serviceName, response);
// 5、check response
ResponseHolder holder = ResponseHolder.builder().response(response).build();
checkResponseValue(holder);
// 6、解析返回值
Class<?> returnClass = method.getReturnType();
Type returnType = method.getGenericReturnType();
if (returnClass == Void.class || returnType == Void.TYPE) {
return null;
}
Object result = JSON.parseObject(holder.getResponse(), returnType, Feature.IgnoreNotMatch);
return result;
}
/**
* 解析请求参数
* @param args
* @return
*/
private Object parseParameterInput(Object[] args) {
Object parameterInput;
if (args == null || args.length == 0) {
throw new RuntimeException("非法的调用,入参不合法");
} else if (args.length > 1) {
parameterInput = args[0];
} else {
parameterInput = args[0];
}
return parameterInput;
}
/**
* 获取服务名称
* @param methodName
* @return
*/
private String getServiceName(String methodName) {
if(!services.containsKey(methodName)) {
LOGGER.info("非法的请求, 不支持的请求方法, 方法名称为 : " + methodName);
throw new InnerException(ReturnCode.CONFIG_ERROR.getCode(), ReturnCode.CONFIG_ERROR.getMessage());
}
return services.get(methodName);
}
/**
* 处理网关请求数据(添加默认参数,数据加密,数据加签)
* @param parameterInput
* @param serviceName
*/
public abstract void processGateWayRequestData(Object parameterInput, String serviceName);
/**
* 检查网关调用返回信息
* @param holder
*/
public abstract void checkResponseValue(ResponseHolder holder);
}
3、会员网关代理类
会员网关具体实现类,主要是对于请求参数合法性的检查以及检查响应参数以及创建代理对象。
public class MgsServiceProxyHandler extends AbstractGatewayProxyHandler {
private static Logger LOGGER = LoggerFactory.getLogger(MgsServiceProxyHandler.class);
private GatewayRequestProcessor mgsRequestProcessor;
public MgsServiceProxyHandler(GateWayInvokeService sinaMgsService, GatewayRequestProcessor mgsRequestProcessor, Map<String, String> services) {
super(sinaMgsService, services);
this.mgsRequestProcessor = mgsRequestProcessor;
}
public void processGateWayRequestData(Object request, String serviceName){
if(request == null) {
return;
}
if(!MgsBaseRequest.class.isAssignableFrom(request.getClass())) {
LOGGER.info("非法的请求参数类型,请求参数类型为 : " + request.getClass().getName());
throw new InnerException(ReturnCode.CONFIG_ERROR.getCode(), ReturnCode.CONFIG_ERROR.getMessage());
}
mgsRequestProcessor.populateGatewayDefaultValues(request, serviceName);
}
@Override
public void checkResponseValue(ResponseHolder holder) {
// do something
}
@SuppressWarnings("unchecked")
public static <T> T create(Class<T> clazz, GateWayInvokeService gateWayInvokeService, GatewayRequestProcessor mgsRequestProcessor, Map<String, String> service) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, new MgsServiceProxyHandler(gateWayInvokeService, mgsRequestProcessor, service));
}
}
4、网关请求参数接口
主要是对请求参数的处理,包含加密,加签的处理等。
/**
* <p>网关请求对象处理类</p>
*/
public interface GatewayRequestProcessor<T> {
void populateGatewayDefaultValues(T requestData, String serviceName);
}
5、加密字段注解
对于请求类中如果某个字段需要加密,只需要在这个字段上添加这个注解在网关代理类中就会统一处理。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EncryptField {
}
6、网关请求参数处理抽象类
因为会员网关与资金网关对于签名以及加密的处理逻辑一样只是设置默认参数的这个逻辑不一样所以提出了一个抽象类。这个接口的主要功能如下:
- 装载默认值
- 数据加密
- 数据签名
public abstract class AbstractGatewayRequestProcessor implements GatewayRequestProcessor<SignEncryptParam> {
private static final Map<Class<?>, List<Field>> encryptFieldCache = new ConcurrentHashMap<Class<?>, List<Field>>(64);
private final ISecurityService iSecurityService;
protected AbstractGatewayRequestProcessor(ISecurityService iSecurityService) {
this.iSecurityService = iSecurityService;
}
@Override
public void populateGatewayDefaultValues(SignEncryptParam requestData, String serviceName) {
// 1、装载默认值
populateDefaultValue(requestData, serviceName);
// 2、数据加密
encryptDataIfNecessary(requestData);
// 3、签名
SignData signData = SignDataUtil.getReqSignData(requestData);
String sign = signData(signData);
requestData.setSign(sign);
}
/**
* 加密数据
* @param requestData
*/
public void encryptDataIfNecessary(SignEncryptParam requestData) {
if(requestData == null) {
return;
}
Class targetClass = requestData.getClass();
List<Field> encryptFields = findEncryptFields(targetClass);
if(CollectionUtils.isEmpty(encryptFields)) {
return;
}
for (Field encryptField : encryptFields) {
if(encryptField.getType() != String.class) {
throw new IllegalArgumentException("非法的字段类型");
}
ReflectionUtils.makeAccessible(encryptField);
String originalValue = (String) ReflectionUtils.getField(encryptField, requestData);
String encryptData = genCipherTextData(requestData, originalValue);
ReflectionUtils.setField(encryptField, requestData, encryptData);
}
}
/**
* 数据签名
* @param signData
* @return
*/
protected String signData(SignData signData) {
// do something
}
/**
* 加密数据
* @param signData
* @return
*/
private String genCipherTextData(SignEncryptParam request, String originalValue) {
// do something
}
private List<Field> findEncryptFields(Class targetClass) {
List<Field> fields = encryptFieldCache.get(targetClass);
if(!CollectionUtils.isEmpty(fields)) {
return fields;
}
List<Field> currElements = new LinkedList<Field>();
for (Field field : targetClass.getDeclaredFields()) {
EncryptField annotation = AnnotationUtils.getAnnotation(field, EncryptField.class);
if(annotation != null) {
currElements.add(field);
}
}
encryptFieldCache.put(targetClass, currElements);
return currElements;
}
public abstract void populateDefaultValue(SignEncryptParam requestData, String serviceName);
}
7、会员网关请求处理类
会员网关与资金网关的具体请求处理类主要是体现在对于默认基本参数的添加上。
@Component("mgsRequestProcessor")
public class MgsRequestProcessor extends AbstractGatewayRequestProcessor {
private static Logger LOGGER = LoggerFactory.getLogger(MgsRequestProcessor.class);
@Autowired
public MgsRequestProcessor(ISecurityService iSecurityService){
super(iSecurityService);
}
@Override
public void populateDefaultValue(SignEncryptParam requestData, String serviceName) {
if(requestData == null) {
return;
}
if(!(requestData instanceof MgsBaseRequest)) {
LOGGER.error("非法的请求对象, requestData {}", requestData.getClass().getName());
throw new InnerException("非法的请求对象");
}
MgsBaseRequest request = MgsBaseRequest.class.cast(requestData);
// 添加默认参数
}
}
8、HTTP处理类
主要就对于 HTTP 请求的封装,在代理类只是对于 HTTP 请求的代理,这里才是最终请求的调用。
@Data
public class GateWayInvokeService {
private static Logger LOGGER = LoggerFactory.getLogger(GateWayInvokeService.class);
private String gatewayUrl;
public String invoke(Object obj) {
if(StringUtils.isEmpty(gatewayUrl)){
throw new InnerException(ReturnCode.SYSTEM_ERROR.getCode(), "未网关URL");
}
try {
Map<String, String> formData = PropertyConverter.object2StringMap(obj, null);
String responseMsg = CallServiceUtil.post(formData, gatewayUrl, formData.get("_input_charset"), true);
if(StringUtils.isBlank(responseMsg)) {
throw new InnerException("网关失败");
}
return URLDecoder.decode(responseMsg, formData.get("_input_charset"));
} catch (Exception ex) {
LOGGER.error("invoke gateway api failed", ex);
throw new InnerException(ReturnCode.SYSTEM_ERROR.getCode(), ex.getMessage());
}
}
}
9、Spring 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- MGS(会员网关) Service-->
<bean id="mgsGatewayInvokeService" class="com.xxx.GateWayInvokeService">
<property name="gatewayUrl" value="${mgs.url}"/>
</bean>
<bean id="mgsServiceProxy" class="com.xxx.MgsServiceProxyHandler" factory-method="create">
<constructor-arg index="0" value="com.xxx.gateway.mgs.MgsService"/>
<constructor-arg index="1" ref="mgsGatewayInvokeService" />
<constructor-arg index="2" ref="mgsRequestProcessor" />
<constructor-arg index="3" >
<map>
<entry key="createMemberAccount" value="create_activate_member" />
</map>
</constructor-arg>
</bean>
使用接口进行方法调用主要是借鉴了 Mybatis 里面的接口调用思想。在 Mybatis 在已有的接口需要添加功能只需要:
- 在接口中添加处理方法
- 在对应
mapper.xml
文件当中添加对应的 SQL 片段
这样就能够完成新功能的添加了,而上面代理的添加逻辑类似:
- 在接口中添加处理方法
- 在 Spring 配置文件中添加
service(网关接口)
与对应 方法名的映射
比如以上只是创建并激活会员功能,如果大家需要添加功能只需要在会员调用网关添加一个接口就行了,是不是很方便?