设计模式之责任链讲解
责任链模式适用于需要将请求和处理解耦的场景,同时又需要动态地组织处理逻辑的场景。
通过使用责任链模式,可以实现请求的动态处理、灵活的扩展和简化的代码编写,提高系统的可维护性和可扩展性。
一、责任链入门
以下这是GPT生成的责任链代码:
1.定义抽象处理器接口
// 定义抽象处理者接口
interface Handler {void handleRequest(int request);void setNextHandler(Handler handler);
}
2. 具体处理者类1
class ConcreteHandler1 implements Handler {// 定义第二个处理器private Handler nextHandler;@Overridepublic void handleRequest(int request) {if (request >= 0 && request < 10) {System.out.println("ConcreteHandler1 处理请求:" + request);} else if (nextHandler != null) {nextHandler.handleRequest(request);}}@Overridepublic void setNextHandler(Handler handler) {this.nextHandler = handler;}
}
3. 具体处理者类2
class ConcreteHandler2 implements Handler {private Handler nextHandler;@Overridepublic void handleRequest(int request) {if (request >= 10 && request < 20) {System.out.println("ConcreteHandler2 处理请求:" + request);} else if (nextHandler != null) {nextHandler.handleRequest(request);}}@Overridepublic void setNextHandler(Handler handler) {this.nextHandler = handler;}
}
4. 客户端类
public class Client {public static void main(String[] args) {// 创建具体处理者对象Handler handler1 = new ConcreteHandler1();Handler handler2 = new ConcreteHandler2();// 设置处理者之间的关系handler1.setNextHandler(handler2);// 创建请求并发送给责任链的第一个处理者handler1.handleRequest(5);handler1.handleRequest(15);handler1.handleRequest(25);}
}
在这个示例中,Handler 是抽象处理者接口,定义了处理请求的方法 handleRequest 和设置下一个处理者的方法 setNextHandler。ConcreteHandler1 和 ConcreteHandler2 是具体处理者类,分别处理不同范围的请求。Client 是客户端类,创建具体处理者对象并设置它们之间的关系,然后发送请求给责任链的第一个处理者。当请求发送到责任链后,责任链中的处理者按顺序尝试处理请求,直到有一个处理者处理了请求为止。
handler1.handleRequest(15);
表示将请求值为15的请求发送给责任链的第一个处理者 handler1。在当前设计的责任链模式中,一旦有一个处理者处理了请求,处理链就会终止,不再将请求传递给下一个处理者。因此,每次发送请求后,只有第一个能够处理该请求的处理者会对其进行处理。因为具体的实现类中是通过if...else
进行处理器的整合的,当然也可以修改为只要满足特定的处理条件,处理器就会继续往下走。
责任链作为一种设计模式,目的是为了让代码更加优雅,复用性更高。
二、上下文责任链
1. 定义订单处理器模板接口
public interface OrderProcessor {// 自定义void processOrder(Order order, OrderContext context);// 公共方法void preRequestHandler(Order order);
}
2. 定义订单处理器抽象类模板
实现公共方法
public abstract class AbstractOrderProcessor implements OrderProcessor {@Overridepublic void preRequestHandler(Order order) {System.out.println("采购员对订单统一处理");}
}
3. 订单处理器实现
3.1 开始处理器
@Component
@HandlerOrder(order = 1)
public class PendingOrderProcessor extends AbstractOrderProcessor {@Overridepublic void processOrder(Order order, OrderContext context) {// 处理待处理的订单逻辑System.out.println("开始订单处理: " + order.getId());// 可以通过上下文对象访问共享信息System.out.println("Context: " + context.getInfo());}
}
3.2 批准处理器
@Component
@HandlerOrder(order = 2)
public class ApprovedOrderProcessor extends AbstractOrderProcessor{@Overridepublic void processOrder(Order order, OrderContext context) {// 处理已批准的订单逻辑System.out.println("批准订单: " + order.getId());// 可以通过上下文对象访问共享信息System.out.println("Context: " + context.getInfo());}
}
3.3 发货处理器
@Component
@HandlerOrder(order = 3)
public class ShippedOrderProcessor extends AbstractOrderProcessor{@Overridepublic void processOrder(Order order, OrderContext context) {// 处理已发货的订单逻辑System.out.println("订单发货: " + order.getId());// 可以通过上下文对象访问共享信息System.out.println("Context: " + context.getInfo());}
}
4. 订单责任链
@Component
public class OrderProcessorChain {private final ApplicationContext applicationContext;Collection<OrderProcessor> processors = new ArrayList<>();// 注入 ApplicationContextpublic OrderProcessorChain(ApplicationContext applicationContext) {this.applicationContext = applicationContext;// 获取所有订单处理器this.processors = applicationContext.getBeansOfType(OrderProcessor.class).values();}// 根据注解值进行排序protected List<OrderProcessor> getProcessors() {return this.processors.stream().sorted((o1, o2) -> {HandlerOrder order1 = o1.getClass().getAnnotation(HandlerOrder.class);HandlerOrder order2 = o2.getClass().getAnnotation(HandlerOrder.class);if (order1 == null && order2 == null) {return 0; // 如果两者都没有注解,则认为它们相等} else if (order1 == null) {return 1; // 如果 o1 没有注解但 o2 有注解,则 o1 排在 o2 前面} else if (order2 == null) {return -1; // 如果 o1 有注解但 o2 没有注解,则 o1 排在 o2 后面} else {return Integer.compare(order1.order(), order2.order()); // 比较两个注解的 order 属性}}).collect(Collectors.toList());}public void process(Order order, OrderContext context) {List<OrderProcessor> processors = getProcessors();// 依次调用订单处理器处理订单for (OrderProcessor processor : processors) {processor.preRequestHandler(order);processor.processOrder(order, context);}}
}
5. 实体类和注解
@Data
public class Order { private int id;
}@Data
public class OrderContext {private String info; // 上下文信息
}// 指定注解的保留策略
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlerOrder {int order();
}
6. 测试
@SpringBootTest
public class OrderProcessorChainTest {@Autowiredprivate OrderProcessorChain processorChain;@Testpublic void testOrderProcessing() {// 创建订单Order order = new Order();order.setId(123);// 创建订单上下文对象OrderContext context = new OrderContext();context.setInfo("Some info for processing orders");// 处理订单processorChain.process(order, context);}
}
结果:
采购员对订单统一处理
开始订单处理: 123
Context: Some info for processing orders
采购员对订单统一处理
批准订单: 123
Context: Some info for processing orders
采购员对订单统一处理
订单发货: 123
Context: Some info for processing orders
三、Spring过滤器链
责任链在Spring中怎么运用呢?
在Spring框架中,责任链模式常常运用在拦截器(Interceptor)和过滤器(Filter)等场景中,用于处理HTTP请求、消息传递等。
拦截器(Interceptor):在Spring MVC中,拦截器用于在处理请求之前或之后执行一些操作,比如权限检查、日志记录等。你可以定义多个拦截器,它们按照顺序构成一个责任链。当一个请求到达时,会依次执行每个拦截器的预处理方法和后处理方法,类似于责任链模式的行为。
过滤器(Filter):在Spring框架中,你也可以使用Servlet过滤器来对HTTP请求进行预处理或后处理。过滤器链在Servlet容器中会按照配置顺序依次执行,也可以看作是一个责任链模式的应用。
事件监听器(Event Listener):Spring框架提供了事件监听器机制,你可以定义自己的事件和监听器,并通过Spring容器进行管理。当某个事件发生时,监听器会按照注册顺序依次被调用,类似于责任链模式。
下面是一个简单的Spring拦截器示例:
1. 定义拦截器
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 在请求处理之前执行的操作,比如权限检查,未登录则直接拦截if (!checkAuth(request)) {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);return false;}// 放行return true;}// 可以不重写该方法@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 在请求处理之后执行的操作}// 可以不重写该方法@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 在请求完成之后执行的操作}private boolean checkAuth(HttpServletRequest request) {// 检查用户是否有权限// 这里假设简单地检查请求中是否包含合法的身份验证信息String authToken = request.getHeader("Authorization");return authToken != null && authToken.equals("valid_token");}
}
在这个示例中, LoginInterceptor 类实现了 HandlerInterceptor 接口,它是Spring MVC中的拦截器。preHandle 方法用于在请求处理之前执行操作,比如权限检查;postHandle 和 afterCompletion 方法分别用于在请求处理之后执行操作。多个拦截器可以组成一个责任链,按照它们的注册顺序依次执行。
代码中只是提供了拦截器的实现,但是并没有形成责任链,那么多个拦截器是如何形成链路并进行请求的处理的呢?
2. 过滤器链配置
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate EmployeeService employeeService;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/user/login","/employee/login").order(2);// 管理员拦截器,用户可操作的功能都排除registry.addInterceptor(new AdminLoginInterceptor(employeeService)).excludePathPatterns("/user/login","/user/logout").order(3);// token刷新的拦截器,order值越小,越先执行,这个拦截器对所有请求都执行registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(1);}}
在上面的代码中MvcConfig 实现了 WebMvcConfigurer 类,并且通过addInterceptors
方法项过滤器链中增加了两个拦截器LoginInterceptor
和 AdminLoginInterceptor
。这两个拦截器通过order指定了拦截器的顺序。两个拦截器在请求过来时都会触发,拦截器会根据自身配置的拦截路径做过滤处理。
我们可以来看下 WebMvcConfigurer接口的定义:接口定义了很多方法,其中有一个addInterceptors
方法就是将拦截器添加到链路中的。这个接口其实有点像我们在责任链入门中讲到的定义抽象处理器接口中的setNextHandler,通过registry.addInterceptor()
来将处理器进行组织。
public interface WebMvcConfigurer {... default void addInterceptors(InterceptorRegistry registry) {}
}
InterceptorRegistry类中会通过addInterceptor方法将处理器添加到集合中去。
public class InterceptorRegistry {private final List<InterceptorRegistration> registrations = new ArrayList();private static final Comparator<Object> INTERCEPTOR_ORDER_COMPARATOR;public InterceptorRegistry() {}public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) {InterceptorRegistration registration = new InterceptorRegistration(interceptor);this.registrations.add(registration);return registration;}
}
通过addInterceptors方法将拦截器添加到Spring MVC的拦截器链中。当Spring MVC接收到一个请求时,它会通过DispatcherServlet进行处理。DispatcherServlet会调用拦截器链中的每个拦截器来处理请求。
具体来说,以下是请求如何经过拦截器链被拦截器处理的过程:
当一个请求到达DispatcherServlet时,DispatcherServlet会创建一个用于处理该请求的HandlerExecutionChain。
在创建HandlerExecutionChain的过程中,DispatcherServlet会检查是否存在拦截器链。如果存在,DispatcherServlet会将该请求与拦截器链关联起来。
在处理请求之前,DispatcherServlet会依次调用拦截器链中每个拦截器的preHandle方法。拦截器的preHandle方法可以用来进行预处理,比如身份验证、日志记录等操作。
如果拦截器链中的所有拦截器的preHandle方法都返回true,表示请求可以继续处理。此时,DispatcherServlet会调用与请求匹配的处理器(Controller)来处理请求。
处理器处理完请求后,DispatcherServlet会依次调用拦截器链中每个拦截器的postHandle方法。拦截器的postHandle方法可以用来进行后处理,比如修改响应内容等操作。
最后,DispatcherServlet会依次调用拦截器链中每个拦截器的afterCompletion方法。拦截器的afterCompletion方法会在请求完成后被调用,无论请求是否成功处理。
通过这个过程,拦截器能够在请求处理的不同阶段进行干预,实现诸如权限验证、日志记录、异常处理等功能。拦截器链的顺序由拦截器的order方法决定,数值越小的拦截器优先级越高。
四、总结
关于责任链模式,必须要有处理器模板,要有链,链可以是集合,也可以是类似链表。请求就会沿着这条链依次被链上处理器处理。
相关文章:
设计模式之责任链讲解
责任链模式适用于需要将请求和处理解耦的场景,同时又需要动态地组织处理逻辑的场景。 通过使用责任链模式,可以实现请求的动态处理、灵活的扩展和简化的代码编写,提高系统的可维护性和可扩展性。 一、责任链入门 以下这是GPT生成的责任链代…...
K8s: 将一个节点移出集群和相关注意事项
前置步骤 在Kubernetes集群中,要移出一个节点,你需要执行以下步骤: 1 )将节点标记为不可调度 首先,你需要将目标节点标记为不可调度,以确保Kubernetes不会在该节点上调度新的Pod这可以通过执行以下命令实…...
Python学习笔记24 - 学生信息管理系统
1. 需求分析 2. 系统设计 3. 系统开发必备 4. 主函数设计 5. 学生信息维护模块设计 a. 录入学生信息 b. 删除学生信息 c. 修改学生信息 d. 查询学生信息 e. 统计学生总人数 f. 显示所有学生信息 g. 排序模块设计 6. 项目打包...
【物联网应用案例】某制造企业电锅炉检测项目
供暖行业在我国的经济发展中占据着重要的地位,然而,长期以来,该行业存在着自动化水平低、管理效率不高等问题,制约了其持续发展。为了解决这些问题,吉林某电锅炉生产厂家进行了一项创新性的尝试。 该厂家通过集成物联…...
设计模式实践
结合设计模式概念和在java/spring/spring boot中的实战,说明下列设计模式。 一、工厂模式 这里只讲简单工厂模式,详细的可以参考Java工厂模式(随笔)-CSDN博客。工厂类会根据不同的参数或条件来决定创建哪种对象,这样…...
嵌入式学习52-ARM1
知识零散: 1.flash: nor flash 可被寻地址 …...
Java(MySQL基础)
数据库相关概念 MySOL数据库 关系型数据库(RDBMS) 概念: 建立在关系模型基础上,由多张相互连接的二维表组成的数据库。特点: 使用表存储数据,格式统一,便于维护使用SQL语言操作,标准统一,使用方便 SQL SOL通用语法…...
预约系统的使用
预约系统的使用 目录概述需求: 设计思路实现思路分析1.用户年规则 在 预约系统中的使用流程 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy,skip hardness,make a better result,wa…...
酷开科技OTT大屏营销:开启新时代的营销革命
随着互联网技术的不断发展和普及,大屏已经成为越来越多家庭选择的娱乐方式。在这个背景下,酷开科技凭借其强大的技术实力和敏锐的市场洞察力,成功地将大屏转化为一种新的营销渠道,为品牌和企业带来了前所未有的商业机会。 酷开科技…...
网络安全(防火墙,IDS,IPS概述)
问题一:什么是防火墙,IDS,IPS? 防火墙是对IP:port的访问进行限制,对访问端口进行制定的策略去允许开放的访问,将不放开的端口进行拒绝访问,从而达到充当防DDOS的设备。主要是拒绝网络流量,阻断所有不希望出现的流程,禁止数据流量流通,达到安全防护的作用。如将一些恶…...
安装IntelliJ IDEA插件教程
安装IntelliJ IDEA插件:一份详细指南 在提升IntelliJ IDEA开发效率的过程中,插件扮演着不可或缺的角色。它们为IDE提供了额外的功能和工具,以满足开发者在特定编程语言、框架、测试、版本控制等方面的个性化需求。本文将为您详细阐述如何在I…...
大厂基础面试题(之四)
Q1:请先进行自我介绍 Q2:说下你学习前端的详细过程 Q3:一个页面从url输入到显示页面的整个过程 1.URL解析 2.DNS解析 3.建立TCP连接 4.发起HTTP请求 5.服务器处理请求 6.返回HTTP响应 7.下载页面资源 8.解析和渲染页面 9.JavaScript执行 10…...
为什么我们应该切换到Rust
What is RUST? 什么是Rust? Rust is a programming language focused on safety, particularly safe concurrency, supporting functional and imperative-procedural paradigms. Rust is syntactically similar to C, but it provides memory safety without usi…...
基于Linux定时任务实现的MySQL周期性备份
1、创建备份目录 sudo mkdir -p /var/backups/mysql/database_name2、创建备份脚本 sudo touch /var/backups/mysql/mysqldump.sh# 用VIM编辑脚本文件,写入备份命令 sudo vim /var/backups/mysql/mysqldump.sh# 内如如下 #!/bin/bash mysqldump -uroot --single-…...
【Altium Designer 20 笔记】隐藏PCB上的信号线(连接线)
使用网络类隐藏特定类型的信号线 如果你想要隐藏特定类型的信号线(例如电源类),你可以首先创建一个网络类。使用快捷键DC调出对象类浏览器,在Net Classes中右击添加类,并重命名(例如为“Power”࿰…...
【Git教程】(九)版本标签 —— 创建、查看标签,标签的散列值,将标签添加到日志输出中,判断标签是否包含特定的提交 ~
Git教程 版本标签(tag) 1️⃣ 创建标签2️⃣ 查看存在的标签3️⃣ 标签的散列值4️⃣ 将标签添加到日志输出中5️⃣ 判断tag是否包含特定的提交🌾 总结 大多数项目都是用 1.7.3.2和 “ gingerbread” 这样的数字或名称来标识软件版本的。在 …...
MemberPress配置和使用会员登录页面
目录 隐藏 创建会员登录页面 编辑登录页面 设计您的登录页面 链接到您的登录页面 创建会员登录页面 要创建MemberPress会员登录页面,您需要做的就是导航到 MemberPress > 设置 > 页面选项卡,然后在页面顶部附近的“MemberPress 登录页面”…...
分享一个预测模型web APP的功能模块和界面的设计
一个临床预测模型web APP功能模块与界面设计 随着医疗技术的不断进步,web APP是临床预测模型在医学领域的应用的重要形式。这里分享一个web APP的设计,手里有医学预测模型的可以尝试将其构建成webAPP,进而在临床实践中体验预测模型带来的便利…...
智慧公厕是智慧城市建设中不可或缺的一部分
智慧城市的数字化转型正在取得显著成效,各项基础设施的建设也在迅速发展,其中智慧公厕成为了智慧城市体系中不可或缺的一部分。作为社会生活中必要的设施,公共厕所的信息化、数字化、智慧化升级转型能够实现全区域公共厕所管理的横向打通和纵…...
leetcode热题100.爬楼梯(从二进制到快速幂)
Problem: 70. 爬楼梯 文章目录 题目思路Code复杂度 题目 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 示例 1: 输入:n 2 输出:2 解释:有两种方…...
使用Docker定时备份数据
文章目录 一、Docker镜像制作二、MySQL数据备份三、Minio数据备份四、数据跨服务器传输五、Nginx日志分割六、Docker启动七、Docker备份日志 一、Docker镜像制作 镜像制作目录 mc下载地址 - rsyncd.conf # https://download.samba.org/pub/rsync/rsyncd.conf.5port 873 uid …...
conda搭建与管理python环境
conda搭建与管理python环境.md Anaconda下载地址Miniconda下载地址Linux下安装1.执行安装2.查看可安装的python版本3.创建环境4.激活环境5.安装python的工具包5.退出环境6.删除指定的环境7.设置默认的环境 Window下安装1.执行安装2.配置环境变量3.检查是否安装成功4.通过conda配…...
获取当前的年、月、日、时、分、秒,并将这些信息用作保存 Excel 文件的前缀
要获取当前的年、月、日、时、分、秒,并将这些信息用作保存 Excel 文件的前缀,你可以使用 Python 的 datetime 模块来获取当前时间,并格式化时间字符串,然后使用 pandas 库将数据保存为 Excel 文件。示例代码: from d…...
Gitlab全量迁移
Gitlab全量迁移 一、背景1.前提条件 一、背景 公司研发使用的Gitlab由于服务器下架需要迁移到新的Gitlab服务器上。Gitlab官方推荐了先备份然后再恢复的方法。个人采用官方的另外一种方法,就写这篇文章给需要的小伙伴参考。 源Gitlab: http://old.mygitlab.com #地…...
Golang ProtoBuf 初学者完整教程:语法
一、编码规范推荐 1、文件名使用小写下划线的命名风格,例如 lower_snake_case.proto 2、使用 2 个空格缩进 3、包名应该和目录结构对应 4、消息名使用首字母大写驼峰风格(CamelCase),例如message StudentRequest { ... } 5、字段名使用小写下划线的风格…...
使用.cc域名的优势
域名注册越来越难了,很多人选择结尾加123、56、365等等数字,总感觉怪怪的。那么能不能选择其他后缀的域名呢?我感觉可以,大部分用户都不会去看域名,只有做技术的会去关注。 使用.cc域名的优势 很多好域名,…...
存储器管理单元MMU概述
在ARM系统中,存储器管理单元MMU主要完成以下工作: ● 虚拟存储空间到物理存储空间的映射。在ARM中采用了页式虚拟存储管理。它把虚拟地址空间分成一个个固定大小的块,每一块称为一页,把物理内存的地址空间也分成同样大小的页。页…...
了解监控易(25):网络拓扑管理,可视化监控网络,快速定位问题
在复杂的网络环境中,快速准确地定位问题、确保网络的稳定运行是至关重要的。监控易的网络拓扑管理功能,正是为了解决这一问题而设计的。该功能通过可视化监控网络,帮助用户迅速把握网络整体状况,快速定位并解决问题。 监控易的网络…...
C#学习笔记10:winform上位机与西门子PLC网口通信_中篇_winform的窗口操作设计、日志的添加使用
今日继续我的C#winform上位机学习之路 这系列笔记的目标是尝试编写一个能够与西门子PLC进行以太网口通信的上位机软件。 文章提供完整代码解释、设计点解释、测试效果图、完整工程下载 本章主要学习:Winform多个窗体的一些操作 、无边框窗体的创建、Combox组件插…...
第14章 大数据与数据科学知识点梳理
第14章 大数据与数据科学知识点梳理(附带页码) ◼ 原则:组织应仔细管理与大数据源相关的元数据,以便对数据文件及其来源和价值进行准确的清单管理。P386 ◼ 大数据:数据量大(Volume)、数据更新…...
wordpress升级流程/手机优化游戏性能的软件
文章目录分层思想OSI七层模型TCP/IP五层协议簇数据的封装与解封装过程设备与层之间的对应关系各层之间通信协议与层之间的对应关系TCP/IP四层模型分层思想 通信需求---->定义协议(规则)标准 完成一件事需要的协议太多!就需要进行分层&…...
关于动物自己做的网站/seo优化方案模板
1、字符型输入框: (1)字符型输入框:英文全角、英文半角、数字、空或者空格、特殊字符,特别要注意单引号和&符号。不要直接输入特殊字符时,使用“粘贴、拷贝”功能尝试输入。 (2࿰…...
彩票网站怎么做赚钱/百度做网站推广的费用
描述: 最近XX公司举办了一个奇怪的比赛:鸡蛋硬度之王争霸赛。参赛者是来自世 界各地的母鸡,比赛的内容是看谁下的蛋最硬,更奇怪的是XX公司并不使用什么精密仪器来测量蛋的硬度,他们采用了一种最老土的办法--从高度扔鸡…...
毕设做购物网站容易吗/seo一键优化
泛型:为了让集合能够记住集合内元素各个类型,且能够达到只要编译时不出现问题,运行时就不会出现类型异常的解决方案。 泛型又从称为参数化类型,是一种编译时类型安全检测机制,类型参数的魅力在于使得程序具有可读性和…...
做vi的图有网站吗/seo一个月赚多少钱
介绍 牛云资源变现专为内容/资源下载场景设计,一站式解决内容带货、流量、资源变现难题,uniCloud云开发无需购买服务器和域名,助力每一位内容创业者。 传统小程序开通流量主要求小程序累计独立访客(UV)1000以上,本项…...
网站域名申请费用/站内优化怎么做
如何制作动态图片在线制作动态图片不完全教程,由动态图片基地【ASQQL.com】原创制作,如有疑问,请通过QQ方式与我们联系!联系方式见网站底部!动态图片制作步骤分为以下几步:第一步:上传视频或者寻…...