状态管理艺术——借助Spring StateMachine驭服复杂应用逻辑
文章目录
- 1. 什么是状态
- 2. 有限状态机概述
- 3. Spring StateMachine
- 4. Spring StateMachine 入门小案例
- 4.1 接口测试
- 5. 总结
1. 什么是状态
在开发中,无时无刻离不开状态的一个概念,任何一条数据都有属于它的状态。
比如一个电商平台,一个订单会有很多状态,比如待付款、待发货、待收货、完成订单。而这其中每一个状态的改变都随着一个个事件的发生。比如将商品下单但未付款,那么订单就是待付款状态,当触发支付事件,那么订单就能从待付款状态转变未待发货状态,以此类推随之对应的事件就是发货、收货。
其二,状态的流动是固定了的。也就是说,待付款状态的下一个状态只能是待发货状态,不能直接转化为待收货状态。这种由待付款直接转变未待收货的状态是非法的,是程序不允许的。
对于这样的一种情况,最简单的解决方案无疑就是if-lese,比如编写一个支付接口,首先根据订单ID从数据库中查询出来订单信息,然后判断一下订单状态是不是待付款状态,如果是待付款状态,则可以继续下面的流程,否则抛出异常告知用户是非法操作。

这种使用硬编码的if-else实现的效果固然没啥问题,但是如果中间状态出现了改变,比如待付款状态出现一个待拼单,那么代码改动幅度未免太大,难以维护。
这时候,学过设计模式的同学,很容易就想到了状态模式。
状态模式将状态改变抽象成了三个角色:
- 环境角色(Context):也称上下文,定义了客户端需要的接口,维护一个当前状态,并将状态的相关操作委托给当前状态对象处理。
- 抽象状态角色(State):定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为。
使用状态模式,可以将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。并且允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
但是状态模式也存在缺点:
- 如果一个实物存在过多状态,会出现类爆炸问题。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
对比两种方案,状态模式是更好的解决方案,而对应到实践,也就是状态机。
2. 有限状态机概述
有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
而要实现状态之间的流转,必须具备以下几个要素。

1. 当前状态:状态流转的起始状态,如上图中的新建状态
2. 触发事件:引发状态与状态之间流转的事件,如上图中的创建订单这个动作
3. 响应函数:触发事件到下一个状态之间的规则
4. 目标状态:状态流转的终止状态,如上图中的待付款状态
简单来说,只有满足当订单是新建状态并且触发创建订单事件,才会执行触发函数,使得状态由新建转化为待付款。
这就是一个状态机的基本要素,但是要实现一个状态机并不简单,好在Spring为我们提供了Spring StateMachine框架。
3. Spring StateMachine
Spring Statemachine是应用程序开发人员在Spring应用程序中使用状态机概念的框架
Spring Statemachine旨在提供以下功能:
- 易于使用的扁平单级状态机,用于简单的使用案例。
- 分层状态机结构,以简化复杂的状态配置。
- 状态机区域提供更复杂的状态配置。
- 使用触发器,转换,警卫和操作。
- 键入安全配置适配器。
- 生成器模式,用于在Spring Application上下文之外使用的简单实例化通常用例的食谱
- 基于Zookeeper的分布式状态机
- 状态机事件监听器。
- UML Eclipse Papyrus建模。
- 将计算机配置存储在永久存储中。
- Spring IOC集成将bean与状态机关联起来。
官网:spring.io/projects/sp…
源码:github.com/spring-proj…
API:docs.spring.io/spring-stat…
状态机是一种用于控制应用程序状态转换的机制。它包含了一组预定义的状态和状态之间的转换规则。在应用程序运行时,通过不同的事件或计时器触发,状态机能够根据事先定义好的规则自动地改变应用程序的状态。这种设计思想使得开发人员能够更加方便地追踪和调试应用程序的行为,因为状态转换的规则是在启动时确定的,而不需要动态地修改或推断。
4. Spring StateMachine 入门小案例
首先,引入Spring StateMachine 的依赖。
<dependency><groupId>org.springframework.statemachine</groupId><artifactId>spring-statemachine-core</artifactId><version>2.1.3.RELEASE</version>
</dependency>
定义订单状态的枚举与触发订单状态改变的事件枚举
/*** @description: 订单状态* @author:lrk* @date: 2023/9/6*/
@AllArgsConstructor
@Getter
public enum OrderState {WAIT_PAYMENT(1, "待支付"),WAIT_DELIVER(2, "待发货"),WAIT_RECEIVE(3, "待收货"),FINISH(4, "已完成");private Integer value;private String desc;
}
/*** @description: 事件枚举类* @author:lrk* @date: 2023/9/6*/
public enum OrderStatusChangeEvent {/*** 支付*/PAYED,/*** 发货*/DELIVERY,/*** 确认收货*/RECEIVED
}
创建一个订单表,这里只是简单演示,所有只有id、用户名称和订单状态
CREATE TABLE `t_order` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '下单用户名称',`status` tinyint NULL DEFAULT NULL COMMENT '订单状态(1:待支付,2:待发货,3:待收货,4:已完成)',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;
接着,编写状态机的配置类。
- 绑定初始状态与解决状态,以及所有的订单状态
- 绑定从一个状态流向下一个状态需要触发的事件
/*** @description: 状态机配置类* @author:lrk* @date: 2023/9/6*/
@Configuration
@EnableStateMachine(name = "orderStateMachine")
@Slf4j
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderState, OrderStatusChangeEvent> {/*** 配置初始状态*/@Overridepublic void configure(StateMachineStateConfigurer<OrderState, OrderStatusChangeEvent> states) throws Exception {states.withStates()// 指定初始化状态.initial(OrderState.WAIT_PAYMENT)// 指定解决状态.end(OrderState.FINISH).states(EnumSet.allOf(OrderState.class));}/*** 配置状态转换事件关系** @param transitions* @throws Exception*/@Overridepublic void configure(StateMachineTransitionConfigurer<OrderState, OrderStatusChangeEvent> transitions) throws Exception {transitions//支付事件:待支付-》待发货.withExternal().source(OrderState.WAIT_PAYMENT).target(OrderState.WAIT_DELIVER).event(OrderStatusChangeEvent.PAYED).and()//发货事件:待发货-》待收货.withExternal().source(OrderState.WAIT_DELIVER).target(OrderState.WAIT_RECEIVE).event(OrderStatusChangeEvent.DELIVERY).and()//收货事件:待收货-》已完成.withExternal().source(OrderState.WAIT_RECEIVE).target(OrderState.FINISH).event(OrderStatusChangeEvent.RECEIVED);}
}
接着,编写状态机监听器。
状态机监听器种指定了状态从某个状态到某个状态的时候会触发哪个方法,执行方法的逻辑。
比如订单状态一开始是WAIT_PAYMENT,需要转化为WAIT_DELIVER
那么就会执行payTransition方法的逻辑,在这个方法中可以编写相应的业务逻辑。
/*** @description: 状态机监听器* @author:lrk* @date: 2023/9/6*/
@WithStateMachine(name = "orderStateMachine")
@Slf4j
@Component("orderStateListener")
public class OrderListener {@Resourceprivate OrderService orderService;@OnTransition(source = "WAIT_PAYMENT", target = "WAIT_DELIVER")public boolean payTransition(Message<OrderStatusChangeEvent> message) {Order order = (Order) message.getHeaders().get("order");order.setStatus(OrderState.WAIT_DELIVER.getValue());log.info("支付,状态机反馈信息:" + message.getHeaders().toString());return orderService.updateById(order);}@OnTransition(source = "WAIT_DELIVER", target = "WAIT_RECEIVE")public boolean deliverTransition(Message<OrderStatusChangeEvent> message) {Order order = (Order) message.getHeaders().get("order");order.setStatus(OrderState.WAIT_RECEIVE.getValue());log.info("发货,状态机反馈信息:" + message.getHeaders().toString());return orderService.updateById(order);}@OnTransition(source = "WAIT_RECEIVE", target = "FINISH")public boolean receiveTransition(Message<OrderStatusChangeEvent> message) {Order order = (Order) message.getHeaders().get("order");order.setStatus(OrderState.FINISH.getValue());log.info("收货,状态机反馈信息:" + message.getHeaders().toString());return orderService.updateById(order);}
}
接着编写接口
/*** @description: 订单接口* @author:lrk* @date: 2023/9/6*/
@RestController
@RequestMapping("order")
public class OrderController {@Resourceprivate OrderService orderService;@GetMapping("create")public BaseResponse<Order> create() {return ResultUtils.success(orderService.create());}@GetMapping("pay")public BaseResponse<Order> pay(@RequestParam Integer id) {return ResultUtils.success(orderService.pay(id));}@GetMapping("deliver")public BaseResponse<Order> deliver(@RequestParam Integer id) {return ResultUtils.success(orderService.deliver(id));}@GetMapping("receive")public BaseResponse<Order> receive(@RequestParam Integer id) {return ResultUtils.success(orderService.receive(id));}@GetMapping("getOrders")public BaseResponse<List<Order>> getOrders() {return ResultUtils.success(orderService.getOrders());}
}
/*** @author lrk* @description 针对表【t_order】的数据库操作Service实现* @createDate 2023-09-06 22:42:22*/
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>implements OrderService {@Resourceprivate StateMachine<OrderState, OrderStatusChangeEvent> orderStateMachine;@Resourceprivate StateMachinePersister<OrderState, OrderStatusChangeEvent, Order> persister;@Overridepublic Order create() {Order order = new Order();order.setName("小明" + UUID.randomUUID());order.setStatus(OrderState.WAIT_PAYMENT.getValue());this.save(order);return order;}@Overridepublic Order pay(int id) {Order order = this.getById(id);log.info("支付:order订单信息:{}", order);if (!sendEvent(OrderStatusChangeEvent.PAYED, order)) {throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");}return this.getById(id);}@Overridepublic Order deliver(int id) {Order order = this.getById(id);log.info("发货:order订单信息:{}", order);if (!sendEvent(OrderStatusChangeEvent.DELIVERY, order)) {throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");}return this.getById(id);}@Overridepublic Order receive(int id) {Order order = this.getById(id);log.info("收货:order订单信息:{}", order);if (!sendEvent(OrderStatusChangeEvent.RECEIVED, order)) {throw new BusinessException(ErrorCode.OPERATION_ERROR, "状态转换异常");}return this.getById(id);}@Overridepublic List<Order> getOrders() {return this.list();}/*** 发送订单状态转换事件* synchronized修饰保证这个方法是线程安全的** @param changeEvent* @param order* @return*/private synchronized boolean sendEvent(OrderStatusChangeEvent changeEvent, Order order) {boolean result = false;try {//启动状态机orderStateMachine.start();//尝试恢复状态机状态persister.restore(orderStateMachine, order);Message message = MessageBuilder.withPayload(changeEvent).setHeader("order", order).build();result = orderStateMachine.sendEvent(message);//持久化状态机状态persister.persist(orderStateMachine, order);} catch (Exception e) {log.error("订单操作失败:{}", e);} finally {orderStateMachine.stop();}return result;}
}
其实到这,还需要思考一个问题,在业务层通过状态机发送的只是订单转变事件只是订单状态改变的事件OrderStatusChangeEvent,那么状态机怎么知道初始状态是什么?因为需要靠初始状态判断是否达到体检可以转变状态。
这就需要配置状态机持久化配置了
/*** 持久化配置* 实际使用中,可以配合redis等,进行持久化操作** @return*/
@Bean
public DefaultStateMachinePersister persister() {return new DefaultStateMachinePersister<>(new StateMachinePersist<Object, Object, Order>() {//这个内存中的示例仅用于演示目的。对于真正的应用程序,你应该使用真正的持久存储实现。private Map<Long, StateMachineContext<Object, Object>> map = new HashMap();@Overridepublic void write(StateMachineContext<Object, Object> context, Order order) throws Exception {map.put(order.getId(), context);}@Overridepublic StateMachineContext<Object, Object> read(Order order) throws Exception {return map.get(order.getId());}});
}
首先状态机会触发read(Order order)方法,在持久化存储中读取相应的状态机上下文。
这样状态机就能获取到的初始状态了。
而write(StateMachineContext<Object, Object> context, Order order)方法,则是将订单ID对应的上下文放到map集合中去。
根据订单的初始状态和触发事件对应的目标状态,执行相对应的状态机监听器事件。
然后将状态机修改后的订单状态的上下文通过write方法,写进map中,以便下一次订单状态流转的时候可以用到。
4.1 接口测试
一开始,创建一个订单,订单状态为1,也就是待付款。

接着调用支付接口,触发支付事件,订单状态流转为2,也就是待发货

如果这时候,不调用发货接口,直接调用收货接口,订单状态会不会改变呢?

很明显不会,状态机会识别到状态流转异常,在sendEvent会返回false表示失败,接着业务层抛出异常。
继续调用发货接口,订单触发发货事件,订单状态转变为3,也就是待收货状态。

最后,收货,整个订单状态流转过程就完美完成了!

5. 总结
Spring StateMachine是Spring旗下的一个状态机框架。所以生态非常丰富,与Spring整合度非常高,非常适合结合Spring框架去使用。
但是,Spring StateMachine定制性难度困难,因为Spring StateMachine是一个复杂的框架,各方面来说难以定制化。
所以如果是直接使用状态机的组件库,可以考虑使用Spring的状态机。
参考
- Squirrel状态机-从原理探究到最佳实践 - 掘金 (juejin.cn)
- 状态机的介绍和使用 | 京东物流技术团队 - 掘金 (juejin.cn)
- Spring之状态机讲解_spring状态机_爱吃牛肉的大老虎的博客-CSDN博客
- Spring StateMachine 文档 | 中文文档 (gitcode.host)
- 【设计模式】软件设计原则以及23种设计模式总结_起名方面没有灵感的博客-CSDN博客
- 使用Spring StateMachine框架实现状态机 (taodudu.cc)
相关文章:
状态管理艺术——借助Spring StateMachine驭服复杂应用逻辑
文章目录 1. 什么是状态2. 有限状态机概述3. Spring StateMachine4. Spring StateMachine 入门小案例4.1 接口测试 5. 总结 1. 什么是状态 在开发中,无时无刻离不开状态的一个概念,任何一条数据都有属于它的状态。 比如一个电商平台,一个订…...
获取和设置小程序和h5的页面栈
获取页面栈: 语法: let pages getCurrentPages(); 设置页面栈: 小程序语法: pages.data H5语法: pages let pages getCurrentPages(); let page pages[pages.length - 2]; if(page.route "pages/conf…...
Mysql基于成本选择索引
本篇文章介绍mysql基于成本选择索引的行为,解释为什么有时候明明可以走索引,但mysql却没有走索引的原因 mysql索引失效的场景大致有几种 不符合最左前缀原则在索引列上使用函数或隐式类型转换使用like查询,如 %xxx回表代价太大索引列区分度过…...
Element-ui container常见布局
1、header\main布局 <template> <div> <el-container> <el-header>Header</el-header> <el-main>Main</el-main> </el-container> </div> </template> <style> .el-header { …...
ssm实现折线统计图
方法1:单张数据表中的数据图表生成 图表统计,查看部门人数统计这里实现的时单张表中的数据实现部门人数折线统计图展示。 <script type"text/javascript">// 利用AjAx来获取后台传入的数据(Responsebody注解传入的&…...
GLSL ES着色器 精度限定字
目录 前言 WebGL支持的三种精度 数据类型的默认精度 float类型没有默认精度 预处理指令 在GLSL ES中常用的三种预处理指令。 预定义的内置宏 前言 GLSL ES新引入了精度限定字,目的是帮助着色器程序提高运行效率,削减内存开支。顾名思义…...
webrtc的FULL ICE和Lite ICE
1、ICE的模式 分为FULL ICE和Lite ICE: FULL ICE:是双方都要进行连通性检查,完成的走一遍流程。 Lite ICE: 在FULL ICE和Lite ICE互通时,只需要FULL ICE一方进行连通性检查, Lite一方只需回应response消息。这种模式对于部署在公网…...
flink的几种常见的执行模式
背景 在运行flink时,我们经常会有几种不同的执行模式,比如在IDE中启动时,通过提交到YARN上,还有通过Kebernates启动时,本文就来记录一下这几种模式 flink的几种执行模式 flink嵌入式模式: 这是一种我们在…...
蓝桥杯备赛Day8——队列
大家好,我是牛哥带你学代码,本专栏详细介绍了蓝桥杯备赛的指南,特别适合迎战python组的小白选手。专栏以天作为单位,定期更新,将会一直更新,直到所有数据结构相关知识及高阶用法全部囊括,欢迎大家订阅本专栏! 队列也属于基础数据结构。 队列概念 队列是一种数据结构,…...
用滑动条做调色板---cv2.getTrackbarPos(),cv2.creatTrackbar()
滑动轨迹栏作调色板 cv.createTrackbar(‘R’, ‘image’, 0, 255, nothing) 参数:哪个滑动轨迹栏,哪个窗口,最小值,最大值,回调函数 cv.getTrackbarPos(‘R’, ‘image’) 参数:轨迹栏名,窗口…...
dubbo 服务注册使用了内网IP,而服务调用需要使用公网IP进行调用
一、问题描述: 使用dubbo时,提供者注册时显示服务地址ip为[内网IP:20880],导致其他消费者在外部连接的情况下时,调用dubbo服务失败 二、解决办法 方法一、修改hosts文件 (1). 先查询一下服务器的hostna…...
外传-Midjourney的局部重绘功能
今天在抄袭。。。啊不,借鉴 midjourney 官网教程的时候,发现多了一个 局部重绘的功能,意外发觉还不错,分享一下用法。 先给大家说一下,我这段时间都在学习 SD,局部重绘是基操,而 MJ 一直是次次…...
Spring Boot 中使用 Poi-tl 渲染数据并生成 Word 文档
本文 Demo 已收录到 demo-for-all-in-java 项目中,欢迎大家 star 支持!后续将持续更新! 前言 产品经理急冲冲地走了过来。「现在需要将按这些数据生成一个 Word 报告文档,你来安排下」 项目中有这么一个需求,需要将用户…...
Java基础(二十一)十点半游戏
十点半游戏 十点半是一种流行的纸牌游戏,可以说是一种变体的二十一点游戏。游戏的规则是,每个玩家根据所拿到的牌点数的总和来决定是否继续要牌。目标是尽量接近但不超过十点半的点数,超过十点半即为爆牌。如果两名玩家都未爆牌,…...
第8节-PhotoShop基础课程-常用快捷键汇总
文章目录 前言1.工具栏1.移动工具 V2.矩形框选工具 M3.套索工具 L4.魔棒工具 W5.裁剪工具 C6.吸管工具 I7.污点修复工具 J8.仿制图章工具 S9.历史记录画笔工具 Y10.橡皮檫工具 E11.油漆桶工具 G12 减淡工具 O13.钢笔工具 P14 横排文字工具 T15.路径选择工具 A16 椭圆工具 U17 抓…...
Redis带你深入学习数据类型set
目录 1、set 2、set相关命令 2.1、添加元素 sadd 2.2、获取元素 smembers 2.3、判断元素是否存在 sismember 2.4、获取set中元素数量 scard 2.5、删除元素spop、srem 2.6、移动元素smove 2.7、集合中相关命令:sinter、sinterstore、sunion、sunionstore、s…...
Json“牵手”易贝商品详情数据方法,易贝商品详情API接口,易贝API申请指南
易贝是一个可让全球民众在网上买卖物品的线上拍卖及购物网站,易贝(EBAY)于1995年9月4日由Pierre Omidyar以Auctionweb的名称创立于加利福尼亚州圣荷塞。人们可以在易贝上通过网络出售商品。2014年2月20日,易贝宣布收购3D虚拟试衣公…...
《AI一键生成抖音商品种草文案》让你秒变带货王!
在这个数字化的时代,我们的生活被各种应用所包围,其中,抖音作为一款短视频分享平台,已经成为了我们生活中不可或缺的一部分。然而,作为一名抖音创作者,你是否曾经遇到过这样的困扰:在创作商品种…...
博客系统(升级(Spring))(二)获取当前用户信息、对密码进行加密、设置统一数据格式、设置未登录拦截、线程池
博客系统(二) 博客系统获取当前用户的信息对密码进行加密和解密的操作设置统一的数据返回格式设置未登录拦截设置线程池 博客系统 博客系统是干什么的? CSDN就是一个典型的博客系统。而我在这里就是通过模拟实现一个博客系统,这是…...
Postman接口测试工具
Postman接口测试工具 Postman简介Postman 发送一个请求postman创建一个集合Postman 快捷键Postman设置postman请求postman历史postman请求排错postman集合简介postman创建和共享集合postman管理集合postman数据导入导出postman测试脚本postman环境变量和全局变量...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...
通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...
关于easyexcel动态下拉选问题处理
前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…...
Python 高效图像帧提取与视频编码:实战指南
Python 高效图像帧提取与视频编码:实战指南 在音视频处理领域,图像帧提取与视频编码是基础但极具挑战性的任务。Python 结合强大的第三方库(如 OpenCV、FFmpeg、PyAV),可以高效处理视频流,实现快速帧提取、压缩编码等关键功能。本文将深入介绍如何优化这些流程,提高处理…...
绕过 Xcode?使用 Appuploader和主流工具实现 iOS 上架自动化
iOS 应用的发布流程一直是开发链路中最“苹果味”的环节:强依赖 Xcode、必须使用 macOS、各种证书和描述文件配置……对很多跨平台开发者来说,这一套流程并不友好。 特别是当你的项目主要在 Windows 或 Linux 下开发(例如 Flutter、React Na…...
