思考:
- 回调代码中如何实现基于设计模式的重构?——采用模板方法模式
- 你在项目中哪里用到过多线程?——使用多线程异步回收日志,写入数据库中
- 异步回调中网络异常或者延迟,异步回调重复执行怎么保证幂等性?——手动补偿,根据支付ID查日志即可判断是否支付过
- 你在项目中哪里用到过RabbitMQ?——使用MQ解决分布式事务
一、理论基础
1.1、什么是模板方法模式
抽象模板(Abstract Template)角色有如下责任:
(1)定义了一个或多个抽象操作(先定义一个抽象类),以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤;
(2)定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中
具体模板(Concrete Template)角色又如下责任:
(1)实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤(组成部分);
(2)每一个抽象模板角色都可以有任意多个具体模板角色与之对应(跟抽象模板是多对一关系);
(3)而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
示例1:去银行办业务,银行给我们提供了一个模板就是:先取号,排对,办理业务(核心部分我们子类完成),给客服人员评分,完毕。
示例2:去餐厅吃饭,餐厅给提供的一套模板就是:先点餐,等待,吃饭(核心部分我们子类完成),买单。这里吃饭是属于子类来完成的,其他的点餐,买单则是餐厅提供给我们客户的一个模板。 这里办理业务是属于子类来完成的,其他的取号,排队,评分则是一个模板。
注意:模板方法不是接口
参考:https://blog.csdn.net/RuiKe1400360107/article/details/103297723
1.2、骨架图
二、代码实现
2.1、利用模板模式实现支付服务
(1)创建回调接口service
@RestController
public class PayAsynCallbackService {
private static final String UNIONPAYCALLBACK_TEMPLATE = "unionPayCallbackTemplate";
//支付宝的回调——略
/**
* 银联异步回调接口执行代码
*
* @param req
* @param resp
* @return
*/
@RequestMapping("/unionPayAsynCallback")
public String unionPayAsynCallback(HttpServletRequest req, HttpServletResponse resp) {
AbstractPayCallbackTemplate abstractPayCallbackTemplate = TemplateFactory
.getPayCallbackTemplate(UNIONPAYCALLBACK_TEMPLATE);//获取具体实现的模版工厂方案
return abstractPayCallbackTemplate.asyncCallBack(req, resp);//调用异步回调方法
}
/**
* 支付宝的回调接口执行代码——略
*/
}
(2)获取回调具体实现(支付宝、银联、微信)的模版工厂方案
简单工厂模式:相当于一个工厂中有各种产品创建在一个类中,客户无需知道具体产品的名称,只需要知道具体产品参数即可((只有一个工厂)):
public class TemplateFactory {
public static AbstractPayCallbackTemplate getPayCallbackTemplate(String beanId) {
//SpringContextUtil是对Spring容器进行各种上下文操作的工具类,该工具类必须声明为Spring 容器里的一个Bean对象,
//否则无法自动注入ApplicationContext对象,可使用@Component注解实例化
return (AbstractPayCallbackTemplate) SpringContextUtil.getBean(beanId);//通过name获取 Bean.
}
}
(3)创建抽象模版
使用模版方法重构异步回调代码
asyncCallBack:异步回调方法,是整体的异步回调方法,不同的异步实现全部交给子类实现
asyncCallBack步骤:
a、验证报文参数
- 获取所有请求参数封装成map集合,进行验证;因为银联和支付宝的验证都不同,所以verifySignature方法可以直接封装成抽象模板,给每个子类模板进行实现;
b、将日志根据支付ID存放在数据库中;(多线程方式)
c、执行异步回调业务逻辑——具体业务逻辑各个具体模板实现
抽象模板具体代码:
@Slf4j
@Component
public abstract class AbstractPayCallbackTemplate {
@Autowired
private PaymentTransactionLogMapper paymentTransactionLogMapper;
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
/**
* 获取所有请求的参数,封装成Map集合 并且验证是否被篡改
*/
public abstract Map<String, String> verifySignature(HttpServletRequest req, HttpServletResponse resp);
/**
* 异步回调执行业务逻辑
*/
@Transactional
public abstract String asyncService(Map<String, String> verifySignature);
public abstract String failResult();
public abstract String successResult();
/**
* *1. 将报文数据存放到es <br>
* 1. 验证报文参数<br>
* 2. 将日志根据支付id存放到数据库中<br>
* 3. 执行的异步回调业务逻辑<br>
*/
@Transactional
public String asyncCallBack(HttpServletRequest req, HttpServletResponse resp) {
// 1. 验证报文参数 相同点 获取所有的请求参数封装成为map集合 并且进行参数验证
Map<String, String> verifySignature = verifySignature(req, resp);
// 2.将日志根据支付id存放到数据库中
String paymentId = verifySignature.get("paymentId");
if (StringUtils.isEmpty(paymentId)) {
return failResult();
}
// log.info(">>>>>asyncCallBack service 01");
// 3.采用异步形式写入日志到数据库中
threadPoolTaskExecutor.execute(new PayLogThread(paymentId, verifySignature));
// log.info(">>>>>asyncCallBack service 04");
String result = verifySignature.get(PayConstant.RESULT_NAME);
// 4.201报文验证签名失败
if (result.equals(PayConstant.RESULT_PAYCODE_201)) {
return failResult();
}
// 5.执行的异步回调业务逻辑
return asyncService(verifySignature);
}
/**
* 采用多线程技术或者MQ形式进行存放到数据库中
*/
private void payLog(String paymentId, Map<String, String> verifySignature) {
// log.info(">>paymentId:{paymentId},verifySignature:{}", verifySignature);
PaymentTransactionLogEntity paymentTransactionLog = new PaymentTransactionLogEntity();
paymentTransactionLog.setTransactionId(paymentId);
paymentTransactionLog.setAsyncLog(verifySignature.toString());
paymentTransactionLogMapper.insertTransactionLog(paymentTransactionLog);
}
// A 1423 B 1234
/**
* 使用多线程写入日志目的:加快响应 提高程序效率 使用线程池维护线程
*/
class PayLogThread implements Runnable {
private String paymentId;
private Map<String, String> verifySignature;
public PayLogThread(String paymentId, Map<String, String> verifySignature) {
this.paymentId = paymentId;
this.verifySignature = verifySignature;
}
@Override
public void run() {
// log.info(">>>>>asyncCallBack service 02");
payLog(paymentId, verifySignature);
// log.info(">>>>>asyncCallBack service 03");
}
}
}
注意:上面代码这里引入了线程池
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
在异步回调的抽象模板方法中调用了线程池,执行多线程写入日志:
// 3.采用异步形式写入日志到数据库中
threadPoolTaskExecutor.execute(new PayLogThread(paymentId, verifySignature));
使用多线程异步回收日志:
为什么是异步而不是同步?——为了加快相应,提高程序效率
多线程相关配置:
配置信息:
###多线程配置
threadPool:
###核心线程数
corePoolSize: 10
###最大线程数
maxPoolSize: 20
## 队列容量
queueCapacity: 16
配置线程池时线程数设置多少好?:
- CPU密集型时:大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
- IO密集型时:大部分线程都阻塞,故需要多配置线程数,2*cpu核数
Spring线程池:ThredPoolTaskExcutor的处理流程:
ThreadPoolTaskExecutor和ThreadPoolExecutor有何区别?:https://cloud.tencent.com/developer/article/1408125
(1)当池子大小小于corePoolSize,就新建线程,并处理请求
(2)当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去workQueue中取任务并处理
(3)当workQueue放不下任务时,就新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理
(4)当池子的线程数大于corePoolSize时,多余的线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁
线程池的配置代码(通过@Bean加入了Spring的Ioc容器):
@Configuration
@EnableAsync
@Slf4j
public class AsyncTaskConfig implements AsyncConfigurer {
/**
* 最小线程数(核心线程数)
*/
@Value("${threadPool.corePoolSize}")
private int corePoolSize;
/**
* 最大线程数
*/
@Value("${threadPool.maxPoolSize}")
private int maxPoolSize;
/**
* 等待队列(队列最大长度)
*/
@Value("${threadPool.queueCapacity}")
private int queueCapacity;
/**
* ThredPoolTaskExcutor的处理流程 当池子大小小于corePoolSize,就新建线程,并处理请求
* 当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去workQueue中取任务并处理
* 当workQueue放不下任务时,就新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize,
* 就用RejectedExecutionHandler来做拒绝处理
* 当池子的线程数大于corePoolSize时,多余的线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁
*/
@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 最小线程数(核心线程数)
taskExecutor.setCorePoolSize(corePoolSize);
// 最大线程数
taskExecutor.setMaxPoolSize(maxPoolSize);
// 等待队列(队列最大长度)
taskExecutor.setQueueCapacity(queueCapacity);
taskExecutor.initialize();
return taskExecutor;
}
/**
* 异步异常处理
*
* @return
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SpringAsyncExceptionHandler();
}
class SpringAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
log.error("Exception occurs in async method", throwable.getMessage());
}
}
}
(2)具体模板——银联支付回调模版实现
具体模板实现调用银联或者支付宝支付官网拉下来的对接代码,这里以银联支付为例,如下图为银联支付的代码:
银联支付具体模板实现步骤:
A、获取所有请求的参数,封装成Map集合 并且验证是否被篡改(该验证是封装成了抽象方法);
B、执行的异步回调业务逻辑(异步回调骨架也封装成了抽象方法),步骤:
(1)进行手动补偿判断。根据订单ID,去数据库中查询之前在抽象模板那用多线程写入数据库的日志是否已经支付过,如果支付过则直接返回————解决了可能因为网络延迟或者崩溃,导致异步回调重复执行,可能导致的幂等性问题。
(2)将状态改为已支付成功;
(3)调用积分服务增加积分(如果调用积分服务增加积分后,报错导致事务回滚,支付状态为待支付,但是积分却增加了)——这里要使用分布式事务保证幂等性,请看下一节:使用MQ解决分布式事务问题
@Component
public class UnionPayCallbackTemplate extends AbstractPayCallbackTemplate {
@Autowired
private PaymentTransactionMapper paymentTransactionMapper;
@Autowired
private IntegralProducer integralProducer;
@Override
public Map<String, String> verifySignature(HttpServletRequest req, HttpServletResponse resp) {
LogUtil.writeLog("BackRcvResponse接收后台通知开始");
String encoding = req.getParameter(SDKConstants.param_encoding);
// 获取银联通知服务器发送的后台通知参数
Map<String, String> reqParam = getAllRequestParam(req);
LogUtil.printRequestLog(reqParam);
// 重要!验证签名前不要修改reqParam中的键值对的内容,否则会验签不过
if (!AcpService.validate(reqParam, encoding)) {
LogUtil.writeLog("验证签名结果[失败].");
reqParam.put(PayConstant.RESULT_NAME, PayConstant.RESULT_PAYCODE_201);
} else {
LogUtil.writeLog("验证签名结果[成功].");
// 【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态
String orderId = reqParam.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取
reqParam.put("paymentId", orderId);
reqParam.put(PayConstant.RESULT_NAME, PayConstant.RESULT_PAYCODE_200);
}
LogUtil.writeLog("BackRcvResponse接收后台通知结束");
return reqParam;
}
// 异步回调中网络尝试延迟,导致异步回调重复执行 可能存在幂等性问题
//
@Override
@Transactional
public String asyncService(Map<String, String> verifySignature) {
String orderId = verifySignature.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取
String respCode = verifySignature.get("respCode");
// 判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。
System.out.println("orderId:" + orderId + ",respCode:" + respCode);
// 1.判断respCode是否为已经支付成功断respCode=00、A6后,
if (!(respCode.equals("00") || respCode.equals("A6"))) {
return failResult();
}
// 根据日志 手动补偿 使用支付id调用第三方支付接口查询
PaymentTransactionEntity paymentTransaction = paymentTransactionMapper.selectByPaymentId(orderId);
if (paymentTransaction.getPaymentStatus().equals(PayConstant.PAY_STATUS_SUCCESS)) {
// 网络重试中,之前已经支付过
return successResult();
}
// 2.将状态改为已经支付成功
paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_STATUS_SUCCESS + "", orderId, "yinlian_pay");
// 3.调用积分服务接口增加积分(处理幂等性问题) MQ
addMQIntegral(paymentTransaction); // 使用MQ
int i = 1 / 0; // 支付状态还是为待支付状态但是 积分缺增加
return successResult();
}
/**
* 基于MQ增加积分
*/
@Async
private void addMQIntegral(PaymentTransactionEntity paymentTransaction) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("paymentId", paymentTransaction.getPaymentId());
jsonObject.put("userId", paymentTransaction.getUserId());
jsonObject.put("integral", 100);
integralProducer.send(jsonObject);
}
@Override
public String failResult() {
return PayConstant.YINLIAN_RESULT_FAIL;
}
@Override
public String successResult() {
return PayConstant.YINLIAN_RESULT_SUCCESS;
}
/**
* 获取请求参数中所有的信息 当商户上送frontUrl或backUrl地址中带有参数信息的时候,
* 这种方式会将url地址中的参数读到map中,会导多出来这些信息从而致验签失败,
* 这个时候可以自行修改过滤掉url中的参数或者使用getAllRequestParamStream方法。
*
* @param request
* @return
*/
public static Map<String, String> getAllRequestParam(final HttpServletRequest request) {
Map<String, String> res = new HashMap<String, String>();
Enumeration<?> temp = request.getParameterNames();
if (null != temp) {
while (temp.hasMoreElements()) {
String en = (String) temp.nextElement();
String value = request.getParameter(en);
res.put(en, value);
// 在报文上送时,如果字段的值为空,则不上送<下面的处理为在获取所有参数数据时,判断若值为空,则删除这个字段>
if (res.get(en) == null || "".equals(res.get(en))) {
// System.out.println("======为空的字段名===="+en);
res.remove(en);
}
}
}
return res;
}
/**
* 获取请求参数中所有的信息。
* 非struts可以改用此方法获取,好处是可以过滤掉request.getParameter方法过滤不掉的url中的参数。
* struts可能对某些content-type会提前读取参数导致从inputstream读不到信息,所以可能用不了这个方法。
* 理论应该可以调整struts配置使不影响,但请自己去研究。
* 调用本方法之前不能调用req.getParameter("key");这种方法,否则会导致request取不到输入流。
*
* @param request
* @return
*/
public static Map<String, String> getAllRequestParamStream(final HttpServletRequest request) {
Map<String, String> res = new HashMap<String, String>();
try {
String notifyStr = new String(IOUtils.toByteArray(request.getInputStream()), UnionPayBase.encoding);
LogUtil.writeLog("收到通知报文:" + notifyStr);
String[] kvs = notifyStr.split("&");
for (String kv : kvs) {
String[] tmp = kv.split("=");
if (tmp.length >= 2) {
String key = tmp[0];
String value = URLDecoder.decode(tmp[1], UnionPayBase.encoding);
res.put(key, value);
}
}
} catch (UnsupportedEncodingException e) {
LogUtil.writeLog("getAllRequestParamStream.UnsupportedEncodingException error: " + e.getClass() + ":"
+ e.getMessage());
} catch (IOException e) {
LogUtil.writeLog("getAllRequestParamStream.IOException error: " + e.getClass() + ":" + e.getMessage());
}
return res;
}
}
上一篇:支付功能-策略+工厂+反射+Token实现类型选择和表单提交
若对你有帮助,欢迎关注!!点赞!!评论!!