设计模式七:责任链模式

文章目录

      • 1、责任链模式
      • 2、spring中的责任链模式
        • Spring Interceptor
        • Servlet Filter
        • Netty

1、责任链模式

责任链模式为请求创建了一个接收者对象的链,在这种模式下,通常每个节点都包含对另一个节点者的引用。每个节点针对请求,处理自己感兴趣的内容,处理完之后可以结束,也可以向下一个节点传递继续处理;

角色

  1. 抽象处理者角色:处理请求的抽象类,定义了处理请求的抽象方法;(抽象类或接口实现);
  2. 具体处理者角色:处理请求的具体实现类;(持有下家对象的引用);

例:请假流程都是先由本部门审核,根据时间长短再进行下一级审批

在这里插入图片描述

//抽象类
public abstract class Handler {/*** 请假天数*/public int maxday;/*** 请假人*/public String name;public Handler(String name, int maxday) {this.maxday = maxday;this.name = name;}private Handler nextHandler;public void setNextHandler(Handler nextHandler) {this.nextHandler = nextHandler;}/*** 处理请假:判断请假天数,超过本部门限定时间则交由上一级部门*/public final void handlingFakeSlips(int day) {if (this.maxday >= day) {this.agree(day);}else {if (nextHandler != null) {System.out.println(name+":天数已超过我的审批权限,已提交我的上级审批");nextHandler.handlingFakeSlips(day);}else {System.out.println("天数时间过长,准备辞职吧!!!");}}}/*** 审批动作:子类来实现* @param day*/abstract void agree(int day);
}//部门实现类
public class RDDepartment extends Handler{public RDDepartment(String name, int maxday) {super(name, maxday);}@Overridevoid agree(int maxday) {System.out.println(name + ":研发部门请假审批通过,请假天数:" + maxday);}
}//主管实现类
public class Supervisor extends Handler{public Supervisor(String name, int maxday) {super(name, maxday);}@Overridevoid agree(int maxday) {System.out.println(name + ":主管请假审批通过,请假天数:" + maxday);}
}//董事实现类
public class Director extends Handler{public Director(String name, int maxday) {super(name, maxday);}@Overridevoid agree(int maxday) {System.out.println(name + ":请假董事审批通过,请假天数:" + maxday);}
}//组装链
public class HandlerChain {private Handler head;private Handler tail;public HandlerContext(){RDDepartment rdDepartment = new RDDepartment("研发部门",5);Supervisor supervisor = new Supervisor("项目主管",30);Director director = new Director("董事",180);rdDepartment.setNextHandler(supervisor);supervisor.setNextHandler(director);head = rdDepartment;tail = director;}public void doHandler(Integer days){head.handlingFakeSlips(days);}
}//请求
public class Request {public static void main(String[] args) {HandlerChain handlerChain = new HandlerChain();handlerChain.doHandler(360);}
}

优点(when,why):

​ 1.发送者与接收者之间的耦合度降低(解耦)

​ 2.可以灵活添加新的责任链中的对象

缺点:

​ 1.不能保证请求一定被接收

​ 2.一定程度上影响性能

这种形式很难进行动态新增和调整处理节点,一种比较复杂的控制节点的形式如Netty中的责任链模式应用,见下一节

2、spring中的责任链模式

Spring Interceptor

回顾springmvc处理请求的流程:DispatcherServlet接收到请求后,执行doDispatcher()方法,流程回顾请求处理流程图

其中通过HandlerMapping获得的是HandlerExecutionChain 对象

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {//......HandlerExecutionChain mappedHandler = null;//......mappedHandler = getHandler(processedRequest);//......     HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());//......
}protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {if (this.handlerMappings != null) {for (HandlerMapping mapping : this.handlerMappings) {HandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;
}protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {if (this.handlerAdapters != null) {for (HandlerAdapter adapter : this.handlerAdapters) {if (adapter.supports(handler)) {return adapter;}}}throw new ServletException("No adapter for handler [" + handler +"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

HandlerExecutionChain 中包含一个handler对象(后面匹配能处理handler的适配器对象执行,详情对应适配器模式中讲解),还有一个拦截器列表List<HandlerInterceptor> interceptorList,所有的实现了HandlerInterceptor接口的类都会被加载进这个集合中,在请求处理前后分别以责任链的形式调用拦截器的preHandlepostHandle

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {HandlerInterceptor[] interceptors = getInterceptors();if (!ObjectUtils.isEmpty(interceptors)) {for (int i = 0; i < interceptors.length; i++) {HandlerInterceptor interceptor = interceptors[i];if (!interceptor.preHandle(request, response, this.handler)) {triggerAfterCompletion(request, response, null);return false;}this.interceptorIndex = i;}}return true;
}void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)throws Exception {HandlerInterceptor[] interceptors = getInterceptors();if (!ObjectUtils.isEmpty(interceptors)) {for (int i = interceptors.length - 1; i >= 0; i--) {HandlerInterceptor interceptor = interceptors[i];interceptor.postHandle(request, response, this.handler, mv);}}
}

这里的链是由集合List维护,使用List有序的特性一次调用每个拦截器,通过方法返回的结果判断是否需要传递到下一个拦截器

Servlet Filter

servletFilter的调用也是通过责任链模式,通过FilterChain作为链条的管理者

//FilterChain接口
public interface FilterChain {public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;
}//FilterChain实现类
public class MockFilterChain implements FilterChain {//......private final List<Filter> filters;//......@Overridepublic void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {Assert.notNull(request, "Request must not be null");Assert.notNull(response, "Response must not be null");Assert.state(this.request == null, "This FilterChain has already been called!");if (this.iterator == null) {this.iterator = this.filters.iterator();}if (this.iterator.hasNext()) {Filter nextFilter = this.iterator.next();nextFilter.doFilter(request, response, this);}this.request = request;this.response = response;}
}

由上可知,FilterChain中管理的 List<Filter> filters即为所有实现了Filter的过滤器,调用过滤器的时候,通过FilterChain进行链条的调用。


//Filter接口
public interface Filter {default public void init(FilterConfig filterConfig) throws ServletException {}public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain)throws IOException, ServletException;default public void destroy() {}
}//Filter实现类
public class AuthFilter extends AuthenticationFilter {//......public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest httpRequest = toLowerCase((HttpServletRequest)request);String tokenString = httpRequest.getParameter("delegation");if (tokenString != null) {filterChain.doFilter(httpRequest, response);} else {super.doFilter(httpRequest, response, filterChain);}}//......
}

FilterdoFilter方法最后调用filterChain.doFilter(httpRequest, response);即传递至下一个Filter进行处理

Netty

Netty中的handler使用了责任链模式,但是其中回调过多,责任链模式的体现不清晰,参考该文章**Spring中如何使用责任链模式**,将责任链抽离出来,完成在spring中的调用


在这里插入图片描述

该模型中,具有多条链,每条链属于不同层级,链中节点为HandlerContextHandlerContext包含相邻接点的引用,还有Handler的引用

Pipeline:为链条的管理者,通过Pipeline来调用责任链

HandlerContext:为链条中节点的上下文,它里面有链条的前一个节点和后一个节点的HandlerContext引用

Handler: 具体的处理程序,与HandlerContext一一对应


我们先仅看Filter事件这一条链,整个结构由Pipeline管理整条链

在这里插入图片描述

//Pipelie接口
public interface Pipeline {Pipeline fireTaskFiltered();
}//Pipeline实现类
Component("pipeline")
@Scope("prototype")
public class DefaultPipeline implements Pipeline, ApplicationContextAware, InitializingBean {// 创建一个默认的handler,将其注入到首尾两个节点的HandlerContext中,其作用只是将链往下传递private static final Handler DEFAULT_HANDLER = new Handler() {};// 将ApplicationContext注入进来的主要原因在于,HandlerContext是prototype类型的,因而需要// 通过ApplicationContext.getBean()方法来获取其实例private ApplicationContext context;// 创建一个头结点和尾节点,这两个节点内部没有做任何处理,只是默认的将每一层级的链往下传递,// 这里头结点和尾节点的主要作用就是用于标志整个链的首尾,所有的业务节点都在这两个节点中间private HandlerContext head;private HandlerContext tail;// 用于业务调用的request对象,其内部封装了业务数据private Request request;// 用于执行任务的task对象private Task task;// 最初始的业务数据需要通过构造函数传入,因为这是驱动整个pipeline所需要的数据,// 一般通过外部调用方的参数进行封装即可public DefaultPipeline(Request request) {this.request = request;}// 这里我们可以看到,每一层级的调用都是通过HandlerContext.invokeXXX(head)的方式进行的,// 也就是说我们每一层级链的入口都是从头结点开始的,当然在某些情况下,我们也需要从尾节点开始链// 的调用,这个时候传入tail即可。// 触发任务过滤的链调用@Overridepublic Pipeline fireTaskFiltered() {HandlerContext.invokeTaskFiltered(head, task);return this;}// 用于往Pipeline中添加节点的方法,读者朋友也可以实现其他的方法用于进行链的维护void addLast(Handler handler) {HandlerContext handlerContext = newContext(handler);tail.prev.next = handlerContext;handlerContext.prev = tail.prev;handlerContext.next = tail;tail.prev = handlerContext;}// 这里通过实现InitializingBean接口来达到初始化Pipeline的目的,可以看到,这里初始的时候// 我们通过ApplicationContext实例化了两个HandlerContext对象,然后将head.next指向tail节点,// 将tail.prev指向head节点。也就是说,初始时,整个链只有头结点和尾节点。@Overridepublic void afterPropertiesSet() throws Exception {head = newContext(DEFAULT_HANDLER);tail = newContext(DEFAULT_HANDLER);head.next = tail;tail.prev = head;}// 使用默认的Handler初始化一个HandlerContextprivate HandlerContext newContext(Handler handler) {HandlerContext context = this.context.getBean(HandlerContext.class);context.handler = handler;return context;}// 注入ApplicationContext对象@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {this.context = applicationContext;}
}

Pipeline的实现类内部除了实现接口的方法,其他方法均为初始化

  • DEFAULT_HANDLER: Pipeline管理一条链表,该链表的每个节点包含HandlerContextHandler两个对象, 而链表的首和尾两个节点由Pipeline自己指定(其他自定义的节点放在首尾两节点之间),DEFAULT_HANDLER用来作为首尾节点的Handler,不起任何作用
  • context: 由于这些类的作用域均不是单例,所以要使用ApplicationContext.getBean()方法获取,所以类实现了ApplicationContextAware接口的setApplicationContext方法,用于注入ApplicationContext对象
  • private HandlerContext head, tail: 为一条链的首尾两个节点,从这儿也可以看出,链条的每个节点都是通过HandlerContext来引用的,HandlerContext再引用一个Handler

@Component
@Scope("prototype")
public class HandlerContext {HandlerContext prev;HandlerContext next;Handler handler;private Task task;public void fireTaskFiltered(Task task) {invokeTaskFiltered(next(), task);}/*** 处理任务过滤事件*/static void invokeTaskFiltered(HandlerContext ctx, Task task) {if (null != ctx) {try {ctx.handler().filterTask(ctx, task);} catch (Throwable e) {ctx.handler().exceptionCaught(ctx, e);}}}private HandlerContext next() {return next;}private Handler handler() {return handler;}
}

HandlerContext作为节点,应有前后两个节点的引用pre next,还有具体处理任务的Handler

fireTaskFiltered方法供Hanndler调用,将请求传递给下一个节点处理(方法实现中区下一个HandlerContext去执行)

invokeTaskFiltered静态方法供Pipeline 和 上一个节点的fireTaskFiltered方法调用,去执行Handler的方法


public interface Handler {/*** 查询到task之后,进行task过滤的逻辑*/default void filterTask(HandlerContext ctx, Task task) {ctx.fireTaskFiltered(task);}
}

Handler定义了感兴趣的事件(暂时只看过滤事件)

Handler的实现类由我们根据自己的需要去编写,实现Handler接口即可

@Component
public class DurationHandler implements Handler {@Overridepublic void filterTask(HandlerContext ctx, Task task) {System.out.println("时效性检验");ctx.fireTaskFiltered(task);}
}@Component
public class RiskHandler implements Handler {@Overridepublic void filterTask(HandlerContext ctx, Task task) {System.out.println("风控拦截");ctx.fireTaskFiltered(task);}
}@Component
public class TimesHandler implements Handler {@Overridepublic void filterTask(HandlerContext ctx, Task task) {System.out.println("次数限制检验");ctx.fireTaskFiltered(task);}
}

这里我们已经实现了PipelineHandlerContextHandler,知道这些bean都是被Spring所管理的bean,那么我们接下来的问题主要在于如何进行整个链的组装。这里的组装方式比较简单,其主要需要解决两个问题:

  • 对于后续写业务代码的人而言,其只需要实现一个Handler接口即可,而无需处理与链相关的所有逻辑,因而我们需要获取到所有实现了Handler接口的bean;
  • 将实现了Handler接口的bean通过HandlerContext进行封装,然后将其添加到Pipeline中。

以上可以由spring完成,通过生命实现接口BeanPostProcessor的类,实现postProcessAfterInitialization方法,可以在初始化完Pipeline后,获取所有实现了Handlerbean,并添加到pipeline中,spring自动调用该方法

@Component
public class HandlerBeanProcessor implements BeanPostProcessor, ApplicationContextAware {private ApplicationContext context;// 该方法会在一个bean初始化完成后调用,这里主要是在Pipeline初始化完成之后获取所有实现了// Handler接口的bean,然后通过调用Pipeline.addLast()方法将其添加到pipeline中@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {if (bean instanceof DefaultPipeline) {DefaultPipeline pipeline = (DefaultPipeline) bean;Map<String, Handler> handlerMap = context.getBeansOfType(Handler.class);handlerMap.forEach((name, handler) -> pipeline.addLast(handler));}return bean;}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {this.context = applicationContext;}
}
  • postProcessAfterInitialization(Object bean, String beanName): 实现了BeanPostProcessor的该方法,在spring启动之后没初始化完成一个Bean之后,都会调用该方法

如此,当初始化完pipeline之后,获取实现了Handler接口的所有实现类,在addLast()方法中为每一个Handler初始化一个HandlerContext,并添加到Pipeline


如此下来,整个过程调用如下

@Service
public class ApplicationService {@Autowiredprivate ApplicationContext context;public void mockedClient(Request request) {Pipeline pipeline = newPipeline(request);pipeline.fireTaskFiltered();}private Pipeline newPipeline(Request request) {return context.getBean(DefaultPipeline.class, request);}
}

1、spring项目启动,加载和初始化Bean,当加载到DefaultPipeline的时候,由于实现了InitializingBean接口,所以会调用初始化方法afterPropertiesSet(),为DefaultPipeline添加两个首尾节点HandlerContext

2、当初始化完成Pipeline之后,调用postProcessAfterInitialization(Object bean, String beanName);

3、加载所有实现了Hnadler接口的Bean,并通过pipeline.addLast(Handler handler方法为每一个handler创建一个HandlerContext,并添加到链条中;

至此形成了一条链

4、service想执行该链的时候,通过有参构造方法,将请求request传递给pipeline,调用pipeline.fireTaskFiltered();

5、pipeline.fireTaskFiltered()中,会调用HandlerContext的静态方法HandlerContext.invokeTaskFiltered(HandlerContext ctx, Task task)将第一个HandlerContext传入去执行,其handlerfilterTask(HandlerContext ctx, Task task)方法执行具体逻辑

6、handlerfilterTask(HandlerContext ctx, Task task)方法最后会调用ctxinvokeTaskFiltered(HandlerContext ctx, Task task)方法,该方法会使用invokeTaskFiltered(next(), task)去执行下一个节点ctx.handler().filterTask(ctx, task)

7、直至最后到节点tail没有下一个节点会停止执行;


至此单条链的责任链模式已完成

在netty中,并不是一条链,每一个handler有很多针对不同的事件的处理

在pipeline中有所有的事件,我们相对某一个事件处理是,实现handler的对应方法的处理逻辑,就会在对应层级的链中加入该handler,netty多层级代码责任链参考:Spring中如何使用责任链模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/267398.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【k8s相关资源api操作】

k8s相关资源api操作 文章目录 k8s相关资源api操作AdmissionregistrationApiextensionApiregistrationAppsV1ApiAutoscalingV1ApiBatchV1ApiCertificatesApi 证书相关CoordinationV1Api 最重要的CoreV1Api !!!CustomObjectsApiDiscoveryV1ApiEventsV1ApiFlowcontrolApiserverApi…

react-组件基础

1.目标 能够使用函数创建组件 能够使用class创建组件 能够给React元素绑定事件 能够使用state和setState() 能够处理事件中的this指向问题 能够使用受控组件方式处理表单 2.目录 React组件介绍 React组件的两种创建方式 React事件处理 有状态组件和无状态组件 组件中的state…

安卓14/aosp14相比安卓13录屏多了MirrorRoot深入剖析解密-千里马android framework实战开发

背景&#xff1a; 在马哥付费课群里技术讨论时候&#xff0c;有一个学员朋友提出一个问题&#xff0c;那就是录屏时候他提到了&#xff0c;aosp14会使用到一个Mirror对应图层的情况&#xff0c;针对录屏需要对录屏这个虚拟的display进行mirror原来的display。 可以用如下图来表…

C#入门:简单数据类型和强制类型转换

本文由 简悦 SimpRead 转码&#xff0c; 原文地址 mp.weixin.qq.com 本期来讲讲 unity 的脚本语言 —C#&#xff0c;C# 的简单数据类型及范围和强制类型转化的方法。这可是 unity 游戏开发必备技能。 1. 简单数据类型 各个类型的范围&#xff1a; byte -> System.Byte (字节…

手动实现一个简单的 HTTP 请求

本文我们通过 Socket&#xff0c;写一个 HTTP 协议&#xff0c;直观的感受一下上篇文章中的请求和响应。 定义 socket server 通过上篇文章&#xff0c;我们知道 HTTP 协议底层是通过 Socket 实现的&#xff0c;所以我们先通过 socket 定义一个 server import socket#初始化 …

Thread多线程(创建,方法,安全,通信,线程池,并发,并行,线程的生命周期)【全详解】

目录 1.多线程概述 2.多线程的创建 3.Thread的常用方法 4.线程安全 5.线程同步 6.线程通信 7.线程池 8.其它细节知识&#xff1a;并发、并行 9.其它细节知识&#xff1a;线程的生命周期 1.多线程概述 线程是什么&#xff1f; 线程(Thread)是一个程序内部的一条执行…

day04_拦截器Apifox角色管理(登录校验,API接口文档,权限管理说明,角色管理,添加角色,修改角色,删除角色)

文章目录 1. 登录校验1.1 需求说明1.2 实现思路1.3 ThreadLocal1.4 AuthContextUtil1.5 拦截器使用1.5.1 拦截器开发1.5.2 拦截器注册 1.6 代码优化1.6.1 配置优化1.6.2 代码优化1.6.3 前端修改 2. API接口文档2.1 Apifox接口管理平台2.1.1 接口管理平台简介2.1.2 Apifox简介2.…

Python读取hbase数据库

1. hbase连接 首先用hbase shell 命令来进入到hbase数据库&#xff0c;然后用list命令来查看hbase下所有表&#xff0c;以其中表“DB_level0”为例&#xff0c;可以看到库名“baotouyiqi”是拼接的&#xff0c;python代码访问时先连接&#xff1a; def hbase_connection(hbase…

mysql 远程不允许连接 1130 -Host ‘‘ is not allowed to connect to this MySQL server

1、docker 进入mysql 命令 sudo docker exec -it 0c58 /bin/bash 2、连接mysql mysql -u root -ppwd 3、 use mysql; 4、更改表所有root用户都可以连接 update user set host ‘%’ where user‘root’; 5、刷新权限 flush privilege&#xff1b; ok解决&#xff1b;

【前端素材】推荐优质后台管理系统网页Hyper平台模板(附源码)

一、需求分析 1、系统定义 后台管理系统是一种用于管理和控制网站、应用程序或系统的管理界面。它通常被设计用来让网站或应用程序的管理员或运营人员管理内容、用户、数据以及其他相关功能。后台管理系统是一种用于管理网站、应用程序或系统的工具&#xff0c;通常由管理员使…

二维码门楼牌管理系统技术服务的深度解析

文章目录 前言一、标准地址名称的定义与重要性二、二维码门楼牌管理系统的核心技术三、标准地址名称在二维码门楼牌管理中的应用四、二维码门楼牌管理系统的优势与挑战五、展望未来 前言 在数字化浪潮中&#xff0c;二维码门楼牌管理系统以其高效、便捷的特性&#xff0c;正逐…

windows 11 前后端项目部署

目录 1.准备环境&#xff1a; 2.安装jdk 测试&#xff1a;winr 输入cmd 3.安装tomcat 4.安装mysql 远程导入数据&#xff1a; 外部后台访问&#xff1a;192.168.232.1:8080/crm/sys/loginAction.action?usernamezs&password123 5.安装nginx 前后端部署&#xff1…

Mysql 的高可用详解

Mysql 高可用 复制 复制是解决系统高可用的常见手段。其思路就是&#xff1a;不要把鸡蛋都放在一个篮子里。 复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。一台主库的数据可以同步到多台备库上&#xff0c;备库本身也可以被配置成另外一台服务器的主库。主…

C++——模版

前言&#xff1a;哈喽小伙伴们好久不见&#xff0c;这是2024年的第一篇博文&#xff0c;我们将继续C的学习&#xff0c;今天这篇文章&#xff0c;我们来习一下——模版。 目录 一.什么是模版 二.模版分类 1.函数模版 2.类模板 总结 一.什么是模版 说起模版&#xff0c;我们…

flutter 加密安全

前言&#xff1a;数据安全 数据的加密解密操作在 日常网络交互中经常会用到&#xff0c;现在密码的安全主要在于 秘钥的安全&#xff0c;如论 DES 3DES AES 还是 RSA, 秘钥的算法&#xff08;计算秘钥不固定&#xff09; 和 保存&#xff0c;都决定了你的数据安全&#xff1b;…

【重温设计模式】享元模式及其Java示例

享元模式的介绍 在编程世界中&#xff0c;我们常常面临着如何有效管理系统资源的挑战。这就好比我们在生活中&#xff0c;面对有限的物质资源&#xff0c;如何做到既满足需求又节约使用&#xff0c;是一门艺术。在设计模式中&#xff0c;有一种模式&#xff0c;恰如其分地解决…

Android之UI Automator框架源码分析(第九篇:UiDevice获取UiAutomation对象的过程分析)

前言 学习UiDevice对象&#xff0c;就需要看它的构造方法&#xff0c;构造方法中有UiDevice对象持有一些对象&#xff0c;每个对象都是我们分析程序的重点&#xff0c;毕竟UiDevice对象的功能&#xff0c;依赖这些组合的对象 备注&#xff1a;当前对象持有的对象&#xff0c;初…

leedcode刷题--day7(字符串)

23 文章讲解 力扣地址 C class Solution { public:void reverseString(vector<char>& s) {int left 0;int right s.size() - 1; // right 应该初始化为 s.size() - 1while (left < right) {swap(s[left], s[right]); // 直接交换 s[left] 和 s[right] 的值lef…

蓝桥杯-单片机组基础5——外部中断与LED的控制(附小蜜蜂课程代码)

蓝桥杯单片机组备赛指南请查看这篇文章&#xff1a;戳此跳转蓝桥杯备赛指南文章 本文章针对蓝桥杯-单片机组比赛开发板所写&#xff0c;代码可直接在比赛开发板上使用。 型号&#xff1a;国信天长4T开发板&#xff08;绿板&#xff09;&#xff0c;芯片&#xff1a;IAP15F2K6…

Excel常用公式总结非常实用

16个最实用的Excel万能公式 1、多条件判断 IF(And(条件1,条件2..条件N),条件成立返回值) IF(or(条件1,条件2..条件N),条件成立返回值) 2、多条件查找 Lookup(1,0/((条件1*条件2*...条件N)),返回值区域&#xff09; 3、多条件求和 Sumifs(值区域,判断区域1,条件1,判断区域2,条…