前言
HM新出springboot入门项目《苍穹外卖》,笔者打算写一个系列学习笔记,“苍穹外卖项目解读”,内容主要从HM课程,自己实践,以及踩坑填坑出发,以技术,经验为主,记录学习,也希望能给在学想学的小伙伴一个参考。
注:本文章是直接拿到项目的最终代码,然后从代码出发,快速逆向学习技术经验! 可能需要一些前置知识
觉得文章有用可以关注点赞收藏期待更新^^,期待您的评论留言
苍穹外卖项目解读(一) 完整代码本地部署运行
苍穹外卖项目解读(二) 管理端JWT令牌、AOP注解开发、分页
苍穹外卖项目解读(三) redis、cache缓存解读
苍穹外卖项目解读(四) 微信小程序支付、定时任务、WebSocket
微信小程序支付
微信小程序开发
1、微信小程序开发需要到微信小程序服务平台注册,分为个人、企业、媒体、政府等,提供需要的注册文件即可。不同的注册主体所获得的开发权限有所不同:个人版就无法使用微信支付功能
2、注册完成之后,对于后端开发来说,我们需要在开发管理中获取所注册小程序ID:AppID、小程序秘钥:AppSecret。对应Java配置文件中:
wechat:
appid: wxffb3637a228223b8
secret: 84311df9199ecacdf4f12d27b6b9522d
3、微信开发者工具,前端交互开发工具,请大家自行探索^^
4、小程序发布上线,在微信开发者工具中,上传按钮到微信服务器(此时为开发版本),在小程序网页管理端,找到上传的版本小程序,提交审核(审核版本),审核痛过之后,即可发布(线上版本)
微信支付
1、已经进行了企业小程序的注册,在微信支付的商户平台接入微信支付,提交资料、签署协议、绑定场景(小程序支付,网页扫码支付,app调用支付等)。(一般开发人员不接触,都是企业注册完成之后拿到后续开发所需要的信息)
微信小程序的支付流程笔者在这里结合日常举例(现金)
我去水果店买东西,挑了一个大西瓜,给老板称重算价格(下单,图例123)。我告诉管着钱的女友买的啥,在哪买的,多大的西瓜,总共花了多少钱并向她要钱(申请微信下单接口,图例456)。女友看了看西瓜跟老板议论这瓜保熟吗xxx(密文,图例78)。我说买了吧,大夏天好热吃个瓜爽歪歪(确定支付 图例9)。女友给老板钱,老板给我西瓜我提着(支付结果,图例10 11 12)。女友走时跟老板说“这瓜保熟,瓜甜的话还来买”(回调,图例13 14)
其中我就是用户,老板水果店是商户,管钱的女友是微信后台
在程序中需要注意的点:
1、向她要钱(申请微信下单接口):准备好参数,去主动请求微信后台(生成预支付交易单)
2、跟老板议论这瓜保熟吗xxx(密文):告诉用户加密好了一些内容,供用户去确定支付
3、吃个瓜爽歪歪(确定支付):真正给钱,微信后台支付
4、回调:指明回调地址,得到结果信息
5调用微信下单接口 请求图
9用户确定支付请求图(参数来自7、8封装)
更多开发细节可关注文档中心
------配置项解析------
wechat:
appid: wxffb3637a228223b8 小程序id
secret: 84311df9199ecacdf4f12d27b6b9522d 小程序秘钥
mchid : 1561414331 商户号
mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606 构造请求客户端build使用 WechatPayHttpClientBuilder
privateKeyFilePath: D:\pay\apiclient_key.pem 商户私钥文件
apiV3Key: CZBK51236435wxpay435434323FFDuv3 解密回调内容的key
weChatPayCertFilePath: D:\pay\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem 平台证书文件
notifyUrl: https://58869fb.r2.cpolar.top/notify/paySuccess 回调地址公网ip
refundNotifyUrl: https://58869fb.r2.cpolar.top/notify/refundSuccess 回调地址
定时任务
spring对定时调度的开发又很友好的开发方式,即启动类上@EnableScheduling,在定时任务上使用@Scheduled,并搭配cron表达式。下面从源码解析spring是如何进行定时调度的。
@EnableScheduling
1、ScheduledAnnotationBeanPostProcessor
其中postProcessAfterInitialization方法中,主要对标注@Scheduled和聚合注解@Schedules的类成员方法进行处理,主要分为2步:
1)识别标注@Scheduled和聚合注解@Schedules的方法;
2)对注解方法调用processScheduled方法进行处理;
2、processScheduled处理过程如下
1)将调用目标方法的过程包装为ScheduledMethodRunnable类
2)构造CronTask并进行调度
3)构造FixedDelayTask并进行调度
4)构造FixedRateTask并进行调度
3、调度框架支持的Task类型
Spring调度框架中重要支持3种调度任务类型(继承结构如上图),具体说明如下:
1)CronTask:cron表达式调度的任务
2)FixedDelayTask:固定延迟时间执行的任务
3)FixedRateTask:固定速率执行的任务
4、3种的调度执行实现近似,以常用的cron为例
@Nullable
public ScheduledTask scheduleCronTask(CronTask task) {
ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
boolean newTask = false;
if (scheduledTask == null) {
scheduledTask = new ScheduledTask(task);
newTask = true;
}
if (this.taskScheduler != null) {
scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
}
else {
addCronTask(task);
this.unresolvedTasks.put(task, scheduledTask);
}
return (newTask ? scheduledTask : null);
}
1)将调度任务包装为ScheduledTask类型,其中封装了执行结果ScheduledFuture
2)存在任务调度器(taskScheduler)时,直接进行调度执行.
3)不存在任务调度器(taskScheduler)时,将任务暂存到addCronTask中,待调用afterPropertiesSet方法时再进行调度执行
5、任务调度器支持自定义,当无自定义调度器时,调度框架提供了默认的任务调度器;
自定义任务调度器的处理逻辑在方法finishRegistration中
上述获取任务调度器的优先级顺序为:
1)当Bean后处理器中定义了任务调度器时,优先取Bean后处理器的任务调度器
2)在BeanFactory中获取Bean类型为SchedulingConfigurer的实例,在其方法configureTasks中可以自定义任务调度器
3)获取BeanFactory中TaskScheduler类型的bean(如有)
4)获取BeanFactory中ScheduledExecutorService类型的bean(如有)
5)当上述方式获取的任务调度器都不存在时,会使用框架中默认的任务调度器,如下:
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
6、框架内提供的任务调度器
框架内提供的任务调度器主要包括:
1)ConcurrentTaskScheduler
2)ThreadPoolTaskScheduler
继承结构如下:
以上述框架默认的ConcurrentTaskScheduler进行说明,在调用调度器方法scheduleWithFixedDelay执行时,具体执行逻辑为:
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
try {
if (this.enterpriseConcurrentScheduler) {//默认false
return new EnterpriseConcurrentTriggerScheduler().schedule(decorateTask(task, true), trigger);
}
else {
ErrorHandler errorHandler =
(this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
return new ReschedulingRunnable(task, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule();
}
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
}
}
这里主要包含2部分:
1)首先把task任务包装为DelegatingErrorHandlingRunnable类型(支持嵌入错误处理器逻辑),具体是在方法decorateTask中实现
2)调用线程池方法ReschedulingRunnable().schedule()进行调度执行this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
@Scheduled
fixedDealy
在上一次调用结束和下一次调用开始之间的固定时间内执行注释方法。时间单位默认为毫秒,但可以通过 timeUnit 重载。
fixedRate
以固定的调用间隔执行注释方法。时间单位默认为毫秒,但可以通过 timeUnit.Me 方法重载。
cron表达式
包括秒、分、时、月、月日和星期的触发器。
例如,"0 * * * MON-FRI “表示在工作日每分钟触发一次(在分钟的顶部,即第 0 秒)。
从左到右读取的字段解释如下。
秒 分钟 小时 日 月 星期
特殊值”-"表示禁用 cron 触发器,主要用于由 ${…} 占位符解析的外部指定值。
WebSocket
WebSocket 是一种支持双向通讯网络通信协议。
意思就是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息
属于服务器推送技术的一种.
特点:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据(blob对象或Arraybuffer对象)
(5)收到的数据类型 可以使用binaryType 指定, 显式指定收到的二进制数据类型
(6)没有同源限制,客户端可以与任意服务器通信。
(7)协议标识符是ws(握手http)(如果加密,则为wss(tcp +TLS)),服务器网址就是 URL。
WebSocket对象
WebSocket对象提供了用于创建和管理WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
使用 WebSocket() 构造函数来构造一个 WebSocket 。
前后端都需要实现websocket的open close message方法
@Component
@ServerEndpoint("/ws/{sid}") //此处类似controller的方式,由前端访问到
public class WebSocketServer {
//存放会话对象 建立websocket连接的对象,此处的Session为websocke包下
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点 建立服务端连接
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
reference:
https://blog.csdn.net/supzhili/article/details/131324690
https://baijiahao.baidu.com/s?id=1706643919026404240&wfr=spider&for=pc