【Spring底层分析】Spring AOP基本使用+万字底层源码阅读分析

article/2025/7/3 18:24:53

一、AOP基本使用

三步:

  • 将业务逻辑组件和切面类都加入到容器中,告诉Spring哪个是切面类(@Aspect)
  • 在切面类上的每一个通知方法上标注通知注解,告诉Spring何时(@Before、@After、@Around……)何地运行(切入点表达式)
  • 开启基于注解的aop模式,@EnableAspectJAutoProxy

定义业务逻辑类、切面类:

public class MathCalculator {public int div(int i, int j){return i / j;}
}
@Aspect //需要标注此注解,spring才知道这是切面类
public class LogAspects {@Pointcut("execution(public int org.example.MathCalculator.*(..))")public void pointCut(){};//@Before(value = "public int org.example.MathCalculator.div(int, int)")//@Before("public int org.example.MathCalculator.*(..)")@Before("pointCut()")public void logStart(JoinPoint joinPoint){Object[] args = joinPoint.getArgs();System.out.println("除法方法:" + joinPoint.getSignature().getName() + "开始运行,参数列表是:" + Arrays.asList(args));}@After("pointCut()")public void logEnd(){System.out.println("除法方法结束运行");}@AfterReturning(value = "pointCut()", returning = "result")public void logReturn(Object result){System.out.println("除法正常运行并返回结果:" + result);}@AfterThrowing(value = "pointCut()", throwing = "exception")public void logException(Exception exception){System.out.println("除法异常运行,异常信息:" + exception);}}

将业务逻辑类和切面类注册到spring容器中(这里我用的方式是配置类方式):

@EnableAspectJAutoProxy
@Configuration
public class AopConfiguration {// 业务逻辑类加入容器中@Beanpublic MathCalculator mathCalculator(){return new MathCalculator();}// 切面类加入容器中@Beanpublic LogAspects logAspects(){return new LogAspects();}
}

测试:

public class AopTest {@Testpublic void test01(){AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfiguration.class);//MathCalculator mathCalculator = new MathCalculator();不是自己创建对象,而是从spring拿对象,这样切面才会生效MathCalculator mathCalculator = applicationContext.getBean(MathCalculator.class);mathCalculator.div(4,2); //测试目标方法是否被增强applicationContext.close();}
}

在这里插入图片描述

二、AOP原理

总结:

1)、@EnableAspectJAutoProxy 开启AOP功能
2)、@EnableAspectJAutoProxy 会给容器中注册一个组件:AnnotationAwareAspectJAutoProxyCreator
3)、AnnotationAwareAspectJAutoProxyCreator是一个后置处理器;

4)、容器的创建流程:
1)、registerBeanPostProcessors()注册后置处理器;创建AnnotationAwareAspectJAutoProxyCreator后置处理器

​ 2)、 finishBeanFactoryInitialization () 初始化剩下的单实例bean

​ 1)、如创建业务逻辑类组件、切面类组件

​ 2)、AnnotationAwareAspectJAutoProxyCreator会拦截以上组件的创建过程:组件创建完之后,判断组件是否需要增强?

​ 是:切面的通知方法,包装成增强器(Advisor);给业务逻辑组件创建一个代理对象(cglib)

5)、执行目标方法

​ 1)代理对象执行目标方法

​ 1)CglibAopProxy.intercept()来拦截目标方法

​ 2)得到目标方法的拦截器链(增强器包装成拦截器MethodInterceptor)

​ 3)利用拦截器的链式机制,依次进入每一个拦截器进行执行

​ 4)效果:

​ 正常执行:前置通知->目标方法->后置通知->返回通知
​ 出现异常:前置通知->目标方法->后置通知->异常通知

分析AOP原理时,我们从这个注解开始分析:

启示,如果想研究xx原理,就可以从@EnableXXX注解分析,分析是否给容器注入了什么组件,我们只需要搞清楚这个组件的作用是什么,就可以分析出原理。

一、从@EnableAspectJAutoProxy注解开始分析

1、@EnableAspectJAutoProxy

在这里插入图片描述

  • AspectJAutoProxyRegistrar.class

在这里插入图片描述

  • 跟进registerAspectJAnnotationAutoProxyCreatorIfNecessary方法中,一路跟进,会进入以下方法:

在这里插入图片描述

也就是说@EnableAspectJAutoProxy注解的作用就是给容器中注入bean定义,为后边往容器中注入组件做准备。

internalAutoProxyCreator(组件名),AnnotationAwareAspectJAutoProxyCreator(组件类型)

所以接下来我们就是探究AnnotationAwareAspectJAutoProxyCreator组件的是功能是什么,何时工作?

  • AnnotationAwareAspectJAutoProxyCreator.class

在这里插入图片描述

  • AspectJAwareAdvisorAutoProxyCreator.class

在这里插入图片描述

  • AbstractAdvisorAutoProxyCreator.class

在这里插入图片描述

  • AbstractAutoProxyCreator.class

在这里插入图片描述

可以得出结论,AbstractAutoProxyCreator是一个后置处理器,还实现了aware接口,以下是重写的方法:

在AbstractAutoProxyCreator类中重写了后置处理器的两个方法:

在这里插入图片描述

在这里插入图片描述

在AbstractAdvisorAutoProxyCreator类中重写了aware接口的方法:

在这里插入图片描述

  • SmartInstantiationAwareBeanPostProcessor相较于普通的后置处理器,额外还有以下两个需要重写的方法XXXInstantiation,这两个方法的执行时机也与XXXInitialization方法执行时机(bean初始化前后执行)不同。我们之后会分析。

在这里插入图片描述

在这里插入图片描述

以上是与AOP有关的组件的大致介绍。接下来我们启动AOP的测试demo,从容器创建、创建单实例bean、调用目标方法来分析AOP是怎么起作用的。

这里启动的demo就是本文介绍AOP基本使用的那个demo。

二、启动容器

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1)registerBeanPostProcessors():注册所有后置处理器到容器中

1、先获取ioc容器已经定义了的需要创建对象的所有BeanPostProcessor的名字。其中一个就是AbstractAutoProxyCreator后置处理器的,这个定义就是在通过@EnableAspectJAutoProxy创建的

在这里插入图片描述

2、注册后置处理器

2.1、优先注册实现了PriorityOrdered接口的BeanPostProcessor;

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.2、再给容器中注册实现了Ordered接口的BeanPostProcessor;这里注册的就是我们的AbstractAutoProxyCreator。因为AbstractAutoProxyCreator后置处理器实现了@order接口。

在这里插入图片描述

2.3、注册没实现优先级接口的BeanPostProcessor;

后置处理器注册到容器中的过程:

1、创建bean实例:createBeanInstance()

2、属性赋值:populateBean()

3、初始化bean:initializeBean(),进入initializeBean()

3.1.invokeAwareMethods() -> 如果该bean实现aware接口,则执行重写aware接口的方法

3.2.applyBeanPostProcessorsBeforeInitialization()->执行该bean实现后置处理器接口,重写的postProcessBeforeInitialization()

3.3.invokeInitMethods()->执行初始化方法

3.4.applyBeanPostProcessorsAfterInitialization()->执行该bean实现后置处理器接口,重写的postProcessAfterInitialization()

3、将后置处理器注册到beanFactory中。

在这里插入图片描述

以上,AbstractAutoProxyCreator后置处理器已经注册到容器中了,这个后置处理器会在注册单实例bean(MathCalculator)时起作用。

2)finishBeanFactoryInitialization:注册所有单实例bean到容器中

在这里插入图片描述

1、先尝试用后置处理器获取代理对象【只有当后置处理是InstantiationAwareBeanPostProcessor类型时才有效】

resolveBeforeInstantiation()

在这里插入图片描述

由于AbstractAutoProxyCreator后置处理器就是InstantiationAwareBeanPostProcessor类型的,因此就会执行AbstractAutoProxyCreator里重写的postProcessBeforeInstantiation方法。【我们主要关心MathCalculator、LogAspects】

1.1、applyBeanPostProcessorsBeforeInstantiation()->postProcessBeforeInstantiation()

public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {Object cacheKey = this.getCacheKey(beanClass, beanName);if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {if (this.advisedBeans.containsKey(cacheKey)) {return null;}if (this.isInfrastructureClass(beanClass) || this.shouldSkip(beanClass, beanName)) {this.advisedBeans.put(cacheKey, Boolean.FALSE);return null;}}TargetSource targetSource = this.getCustomTargetSource(beanClass, beanName);if (targetSource != null) {if (StringUtils.hasLength(beanName)) {this.targetSourcedBeans.add(beanName);}Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);Object proxy = this.createProxy(beanClass, beanName, specificInterceptors, targetSource);this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;} else {return null;}}===========================最后直接返回null

1.2、applyBeanPostProcessorsAfterInitialization()->postProcessAfterInitialization()

因为postProcessBeforeInstantiation返回null,因此这里这个方法不执行。

2、真正创建bean实例

doCreateBean()
createBeanInstance()

在这里插入图片描述

populateBean()
initializeBean()

在这里插入图片描述

applyBeanPostProcessorsAfterInitialization()->postProcessAfterInitialization()

在这里插入图片描述

在这里插入图片描述

wrapIfNecessary():如果需要则包装成代理对象

在这里插入图片描述

1)获取当前bean的且已排好序的增强方法:Object[] specificInterceptors

  • 获取所有增强器(通知方法)
  • 找哪些通知方法是需要切入当前bean方法的
  • 给增强器排序

2)保存当前bean到advisedBeans中:this.advisedBeans.put(cacheKey, Boolean.TRUE);

3)创建当前bean的代理对象:this.createProxy()

在这里插入图片描述

  • 获取所有增强器(通知方法)

    在这里插入图片描述

  • 保存到proxyFactory

  • 创建代理对象:Spring自动决定

    • JdkDynamicAopProxy(config)->jdk动态代理
    • ObjenesisCglibAopProxy(config)->cglib的动态代理
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {if (NativeDetector.inNativeImage() || !config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {return new JdkDynamicAopProxy(config);} else {Class<?> targetClass = config.getTargetClass();if (targetClass == null) {throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");} else {return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) && !ClassUtils.isLambdaClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));}}}
    

4)最后返回代理对象给容器:return proxy


注意,以下情况则返回的是原始对象:

  • 核心基础设施类Advisor, Advice, Pointcut 的实现
  • 切面类:被 @Aspect 注解的类(通过 shouldSkip() 显式排除)
  • 无匹配的 Advisor 的类

解释:核心基础设施类是怎么得到的?

假设有以下切面:

@Aspect
public class SecurityAspect {@Around("@annotation(RequireAuth)")public Object checkAuth(ProceedingJoinPoint pjp) {// 权限检查逻辑}
}

Spring会创建以下基础设施对象:

// 1. 创建Pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("@annotation(RequireAuth)");// 2. 创建Advice
Advice advice = new MethodInterceptor() {public Object invoke(MethodInvocation mi) {// 包装checkAuth方法的逻辑}
};// 3. 创建Advisor
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);

那么,Advice与Advisor的区别在哪?

  • Advisor:封装了 Pointcut(切点) + Advice(增强)。定义"在哪里增强"和"如何增强"的完整规则

  • Advice:只包含增强行为。定义"具体增强什么"

    • @Around:AspectJAroundAdvice
    • @Before:AspectJMethodBeforeAdvice
    • @After:AspectJAfterAdvice
    • @AfterReturning:AspectJAfterReturningAdvice
    • @AfterThrowing:AspectJAfterThrowingAdvice
@Aspect
public class LogAspect {// 👇 被拆解为 Pointcut + Advice@Before("execution(* com.service.*.*(..))") public void logBefore() {System.out.println("Before method");}
}

转换过程:

  1. @Before 注解 → 创建 AspectJExpressionPointcut(Pointcut)
  2. logBefore() 方法 → 创建 MethodBeforeAdviceInterceptor(Advice)
  3. 组合两者 → 创建 InstantiationModelAwarePointcutAdvisor(Advisor)

三、调用目标方法

前面我们分析完启动容器后,容器会生成代理对象。当代理对象调用目标方法时,就会走增强的逻辑,具体是怎样的过程,我们现在来分析。

在这里插入图片描述

在这里插入图片描述

👆容器中保存了组件的代理对象(cglib增强后的对象),这个对象里面保存了详细信息(比如增强器,目标对象,xxx)

1)CglibAopProxy.intercept()

在这里插入图片描述

1、根据ProxyFactory对象获取将要执行的目标方法拦截器链:List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

getInterceptorsAndDynamicInterceptionAdvice()
  • List interceptorList = new ArrayList(advisors.length);interceptorList保存所有拦截器5:一个默认的ExposeInvocationInterceptor 和4个增强器;

    • 遍历所有的增强器,将其转为Interceptor;

      Interceptor[] interceptors = registry.getInterceptors(advisor)

      interceptorList.addAll(Arrays.asList(interceptors));

    • 将增强器转为List; 如果是MethodInterceptor,直接加入到集合中。如果不是,使用AdvisorAdapter将增强器转为MethodInterceptor; 转换完成返回MethodInterceptor数组。(统一转换为 MethodInterceptor目的是标准化执行接口,实现调用链的通用处理机制)

      MethodInterceptor[] interceptors = registry.getInterceptors(advisor);

      interceptorList.addAll(Arrays.asList(interceptors));

      public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {List<MethodInterceptor> interceptors = new ArrayList(3);Advice advice = advisor.getAdvice();if (advice instanceof MethodInterceptor) { // 如果增强器是MethodInterceptor类型,那么直接添加interceptors.add((MethodInterceptor)advice);}Iterator var4 = this.adapters.iterator();while(var4.hasNext()) {AdvisorAdapter adapter = (AdvisorAdapter)var4.next();if (adapter.supportsAdvice(advice)) { // 否则使用适配器进行转换interceptors.add(adapter.getInterceptor(advisor));}}if (interceptors.isEmpty()) {throw new UnknownAdviceTypeException(advisor.getAdvice());} else {return (MethodInterceptor[])interceptors.toArray(new MethodInterceptor[0]);}}===================================1public class AspectJAfterReturningAdvice extends AbstractAspectJAdvice implements AfterReturningAdvice, AfterAdvice, Serializable {}
      --->未实现MethodInterceptor接口,因此需要使用对应适配器转换。1.1、转换方法:adapter.getInterceptor(advisor)public MethodInterceptor getInterceptor(Advisor advisor) {AfterReturningAdvice advice = (AfterReturningAdvice)advisor.getAdvice();return new AfterReturningAdviceInterceptor(advice);}
      也就是说,将AspectJAfterReturningAdvice-->AfterReturningAdviceInterceptor1.2、还有AspectJMethodBeforeAdvice-->MethodBeforeAdviceInterceptor2、其他的advice本身就实现了MethodInterceptor接口,因此无需转变:public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice, Serializable {}public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable {}public class AspectJAfterAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice, Serializable {}=================================public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable {private final AfterReturningAdvice advice;public AfterReturningAdviceInterceptor(AfterReturningAdvice advice) {Assert.notNull(advice, "Advice must not be null");this.advice = advice;}@Nullablepublic Object invoke(MethodInvocation mi) throws Throwable {Object retVal = mi.proceed();this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());return retVal;}
      }
      =================================@FunctionalInterfacepublic interface MethodInterceptor extends Interceptor {@NullableObject invoke(@Nonnull MethodInvocation invocation) throws Throwable;}可以看到,MethodInterceptor接口里只包含一个invoke抽象方法,将advice都转换为MethodInterceptor类型,就是为了之后统一调用invoke方法。
      

得到的拦截器链如下:

在这里插入图片描述

2、如果没有拦截器链,直接执行目标方法。

3、如果有拦截器链,把需要执行的目标对象、目标方法、拦截器链等信息传入创建一个CglibMethodInvocation对象,
并调用 Object retVal = mi.proceed();

retVal = (new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed();

有了拦截器链后,我们现在来分析proceed方法,探究是怎么个拦截法。

在这里插入图片描述

递归过程就是:

1、依次按顺序0-4遍历每个拦截器,然后执行invoke方法。

2、0-3号的拦截器中的invoke方法中,第一步都是直接递归调用proceed方法。

3、直到遍历到4号拦截器,也就是MethodBeforeAdviceInterceptor拦截器。

在这里插入图片描述

4、接着再一次进入proceed方法时,由于满足出递归的条件判断,因此执行目标方法。

5、目标方法执行完则返回到上一个拦截器的invoke方法中。3号拦截器就是AspectJAfterAdvice。

在这里插入图片描述

6、接着继续返回到上一层拦截器的invoke方法中。2号拦截器是AfterReturningAdviceInterceptor。执行对应的增强方法。(只有没有异常才会执行。)

7、接着继续返回到上一层拦截器的invoke方法中。1号拦截器是AspectJAfterThrowingAdvice。执行对应的增强方法。(有异常才会执行。)

以上,就是整个拦截器调用过程。


http://www.hkcw.cn/article/ZorPvDVHRp.shtml

相关文章

线性代数复习

一.行列式 1.定义和性质 &#xff08;1&#xff09;第一种定义 例如&#xff1a;二阶行列式&#xff0c;其结果是以这里两个向量为邻边的平行四边形的面积&#xff08;三阶行列式也就是体积&#xff09; 总结&#xff1a;n阶行列式是由这n个向量组成的&#xff0c;其结果为这…

C#数字图像处理(三)---待完善

文章目录 前言1.图像平移1.1 图像平移定义1.2 图像平移编程实例 2.图像镜像2.1 图像镜像定义2.2 图像镜像编程实例 3.图像缩放3.1 图像缩放定义3.2 灰度插值法3.3 图像缩放编程实例 4.图像旋转4.1 图像旋转定义4.2 图像旋转编程实例 前言 在某种意义上来说&#xff0c;图像的几…

webfuture:提示“Strict-Transport-Security头未设置”漏洞的解决方法

问题描述&#xff1a; Web 服务器对于 HTTP 请求的响应头中缺少 Strict-Transport-Security&#xff0c;这将导致浏览器提供的安全特性失效。 当 Web 服务器的 HTTP 头中包含 Strict-Transport-Security 头时&#xff0c;浏览器将持续使用 HTTPS 来访问 Web 站点&#xff0c;可…

激光雷达的强度像和距离像误差与噪声分析(2)2025.6.2

激光雷达强度像与距离像的误差、噪声及主要影响因素分析 一、距离像误差来源及影响因素 1. 系统误差 激光特性&#xff1a; 波长选择&#xff1a;如905nm/1550nm激光在大气中的散射差异&#xff0c;短波长易受雾霾影响&#xff0c;导致能量衰减。功率不足&#xff1a;远距离…

Artificial Analysis2025年Q1人工智能发展六大趋势总结

2025年第一季度人工智能发展六大趋势总结 ——基于《Artificial Analysis 2025年Q1人工智能报告》 趋势一&#xff1a;AI持续进步&#xff0c;竞争格局白热化 前沿模型竞争加剧&#xff1a;OpenAI凭借“o4-mini&#xff08;高智能版&#xff09;”保持领先&#xff0c;但谷歌&…

2024年数维杯国际大学生数学建模挑战赛D题城市弹性与可持续发展能力评价解题全过程论文及程序

2024年数维杯国际大学生数学建模挑战赛 D题 城市弹性与可持续发展能力评价 原题再现&#xff1a; 中国人口老龄化趋势的加剧和2022年首次出现人口负增长&#xff0c;表明未来一段较长时期内我国人口将呈现下降趋势。这一趋势必将影响许多城市的高质量和可持续发展&#xff0c…

第18讲、Odoo接口开发详解:原理、类型与实践

1. 引言 Odoo作为一个功能强大的开源ERP和业务应用套件&#xff0c;其开放性和可扩展性是核心优势之一。接口&#xff08;API&#xff09;开发在Odoo生态中扮演着至关重要的角色&#xff0c;它使得Odoo能够与外部系统、第三方应用、移动端以及Web前端进行数据交换和功能集成。…

react实现markdown文件预览

文章目录 react实现markdown文件预览1、实现md文件预览2、解决图片不显示3、实现效果 react实现markdown文件预览 1、实现md文件预览 1️⃣第一步&#xff1a;安装依赖&#xff1a; npm install react-markdown remark-gfmreact-markdown&#xff1a;将 Markdown 渲染为 Rea…

AI大数据模型如何与thingsboard物联网结合

一、 AI大数据与ThingsBoard物联网的结合可以从以下几个方面实现&#xff1a; 1. 数据采集与集成 设备接入&#xff1a;ThingsBoard支持多种通信协议&#xff08;如MQTT、CoAP、HTTP、Modbus、OPC-UA等&#xff09;&#xff0c;可以方便地接入各种物联网设备。通过这些协议&am…

一张图,生成一个网站!

大家好&#xff01;我是羊仔&#xff0c;专注AI工具、智能体、编程。 最近羊仔在网上冲浪的时候&#xff0c;又发现一个超级有意思的AI工具&#xff0c;简直是效率神器&#xff01; 今天要跟大家聊聊的&#xff0c;就是这个最近在GitHub上爆火的开源项目—— LlamaCoder&#…

ToolsSet之:数值提取及批处理

ToolsSet是微软商店中的一款包含数十种实用工具数百种细分功能的工具集合应用&#xff0c;应用基本功能介绍可以查看以下文章&#xff1a; Windows应用ToolsSet介绍https://blog.csdn.net/BinField/article/details/145898264 ToolsSet中Number菜单下的Numeric Batch是一个数…

会计科目主数据:企业数字化转型的“数据总线“与财务核算基石

在数字化浪潮席卷全球的今天&#xff0c;企业数据管理面临前所未有的挑战与机遇。作为财务管理的核心要素&#xff0c;会计科目不仅是ERP系统的基础架构&#xff0c;更是连接企业各业务系统的"数据总线"。本文将深入解析会计科目作为主数据的本质特征、跨系统应用模式…

微服务-Sentinel

目录 背景 Sentinel使用 Sentinel控制台 Sentinel控制规则 Sentinel整合OpenFeign 背景 在微服务项目架构中&#xff0c;存在多个服务相互调用场景&#xff0c;在某些情况下某个微服务不可用时&#xff0c;上游调用者若一直等待&#xff0c;会产生资源的消耗&#xff0c;极端情…

机器学习知识图谱——逻辑回归算法(Logistic Regression)

目录 一、图解逻辑回归 (Logistic Regression)算法知识图谱 二、什么是逻辑回归? 三、应用场景 四、算法核心思想 五、数学表达式公式 六、分类规则 七、损失函数(Log Loss) 八、优点 与 缺点 九、与线性回归的区别 十、Python 简易代码示例 机器学习知识图谱——…

【机器学习基础】机器学习入门核心算法:Mini-Batch K-Means算法

机器学习入门核心算法&#xff1a;Mini-Batch K-Means算法 一、算法逻辑工作流程与传统K-Means对比 二、算法原理与数学推导1. 目标函数2. Mini-Batch更新规则3. 学习率衰减机制4. 伪代码 三、模型评估1. 内部评估指标2. 收敛性判断3. 超参数调优 四、应用案例1. 图像处理 - 颜…

前端框架Vue

vue基础知识点 首先介绍用 HTML 写结构 script 里写 Vue&#xff0c;不需要环境 1.差值表达式{{ }} <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>Hello Vue</title><script src"https://cdn.jsdeliv…

ESP32系列AT固件快速开发——Wi-Fi MQTT

目录 【烧录固件时硬件接线】 【烧录固件】 【AT指令WiFi部分】 设置 Wi-Fi 模式 (Station/SoftAP/StationSoftAP) 查询 Wi-Fi 状态和 Wi-Fi 信息 【AT指令MQTT部分】 Demo:已验证的Wi-Fi连接MQTT连接、发布与订阅 设置MQTT用户属性 设置MQTT连接属性&#xff08;测试…

重温经典算法——并归排序

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 基本原理 归并排序基于分治思想&#xff0c;递归地将数组拆分为两个子数组&#xff0c;分别排序后合并。时间复杂度为 O(n log n)&#xff0c;空间复杂度 O(n)&#xff08;…

05-power BI高级筛选器filter与Values人工造表

返回一个表&#xff0c;用于表示另一个表或表达的子集&#xff0c;不能够单独使用&#xff0c; fileter函数对筛选的表进行横向的逐行扫描&#xff0c;这样的函数也叫迭代函数 例&#xff1a;countrows(fileter(表筛选条件))filter的第一参数必须是唯一值得表&#xff0c; 如果…

贪心算法应用:欧拉路径(Fleury算法)详解

Java中的贪心算法应用&#xff1a;欧拉路径&#xff08;Fleury算法&#xff09;详解 一、欧拉路径与欧拉回路基础 1.1 基本概念 欧拉路径&#xff08;Eulerian Path&#xff09;是指在一个图中&#xff0c;经过图中每一条边且每一条边只经过一次的路径。如果这条路径的起点和…