Spring高手之路23——AOP触发机制与代理逻辑的执行

文章目录

  • 1. 从整体视角学习Bean是如何被AOP代理的
  • 2. AOP代理的触发机制
    • 2.1 postProcessAfterInitialization方法源码分析
    • 2.2 wrapIfNecessary方法源码分析
    • 2.3 时序图演示触发机制
  • 3. AOP代理逻辑的执行
    • 3.1 AOP代理如何使用拦截器
    • 3.2 proceed方法源码分析
    • 3.3 时序图

1. 从整体视角学习Bean是如何被AOP代理的

为了全面理解Bean是如何被AOP代理的,我们把前面几篇文章串联一下,需要关注以下几点,并针对每个关键点学习相应的源码部分:

1. AOP代理的触发机制(本章需要讲解的)
理解Spring如何决定哪些Bean需要被代理。

关键点:

  • BeanPostProcessor接口Spring AOP的自动代理创建器实现了这个接口,通过它在Bean初始化的前后进行处理。

源码部分:
AbstractAutoProxyCreator(位于org.springframework.aop.framework.autoproxy包中)

  • 方法:postProcessAfterInitialization
  • 方法:wrapIfNecessary

2. 确定哪些Bean需要代理(前面已讲)
理解Spring如何确定哪些Bean需要被代理,这通常涉及到扫描和匹配切面。

这部分内容在Spring高手之路21——深入剖析Spring AOP代理对象的创建的第2节“匹配增强器Advisors(源码分析+时序图说明)”已经讲解,可自行前往翻阅学习。

关键点:

  • Advisor和Advice:这些是Spring AOP中的切面和通知。

源码部分:

  • AbstractAdvisorAutoProxyCreator的方法getAdvicesAndAdvisorsForBean(位于org.springframework.aop.framework.autoproxy包中)

3. 创建代理对象(前面已讲)
理解Spring如何创建代理对象,包括使用JDK动态代理和CGLIB代理。

这部分内容在Spring高手之路22——AOP切面类的封装与解析的第3.2节“TargetSource 的构建”有提到,可自行前往翻阅学习。

关键点:

  • 代理工厂(ProxyFactory):用于创建代理对象。
  • 动态代理实现JDK动态代理和CGLIB)。

源码部分:

  • ProxyFactory的方法getProxy(位于org.springframework.aop.framework包中)

  • JdkDynamicAopProxy的方法invoke(位于org.springframework.aop.framework包中)

  • CglibAopProxy的方法getProxy(位于org.springframework.aop.framework包中)

4. 代理对象的配置(前面已讲)
理解如何配置代理对象的属性,如切入点和通知。

这部分内容在Spring高手之路22——AOP切面类的封装与解析的第3.2节“TargetSource 的构建”有提到,可自行前往翻阅学习。

关键点:

  • AdvisedSupport类:包含了代理配置的相关信息。

源码部分:

  • AdvisedSupport(位于org.springframework.aop.framework包中)

5. 执行代理逻辑(本章需要讲解的)
理解代理对象在运行时如何拦截方法调用并执行通知逻辑。

关键点:

  • 拦截器链:代理对象通过拦截器链来执行切面逻辑。

源码部分:

  • ReflectiveMethodInvocation的方法proceed(位于org.springframework.aop.framework包中)

推荐学习路径

  1. 从高层理解代理触发机制
  • 查看AbstractAutoProxyCreator类,特别是wrapIfNecessarypostProcessAfterInitialization方法,理解代理触发的整体流程。
  1. 深入理解切面和通知的获取
  • 查看AbstractAdvisorAutoProxyCreator类,特别是getAdvicesAndAdvisorsForBean方法,理解如何获取适用于目标Bean的切面和通知。
  1. 代理对象的创建细节
  • 查看ProxyFactory类,特别是getProxy方法,理解代理对象的具体创建过程。
  1. 理解动态代理的实现
  • 查看JdkDynamicAopProxyCglibAopProxy类,理解JDK动态代理和CGLIB代理的具体实现细节。
  1. 配置代理对象
  • 查看AdvisedSupport类,理解代理对象的配置和管理。
  1. 执行代理逻辑
  • 查看ReflectiveMethodInvocation类,特别是proceed方法,理解代理对象在运行时如何拦截方法调用并执行切面逻辑。

2. AOP代理的触发机制

  还记得BeanPostProcessor接口吗?这在“Spring高手之路13——BeanFactoryPostProcessor与BeanDefinitionRegistryPostProcessor解析”有讲到。

BeanPostProcessor接口提供了两个方法:

  • postProcessBeforeInitialization:在Bean初始化之前调用。
  • postProcessAfterInitialization:在Bean初始化之后调用。

Spring AOP利用postProcessAfterInitialization方法在Bean初始化完成后,检查并决定是否需要对这个Bean进行代理。

2.1 postProcessAfterInitialization方法源码分析

本节源码基于 spring-aop-5.3.16

在这里插入图片描述

代码提出来分析

/**
 * 如果该bean被子类标识为需要代理的bean,则使用配置的拦截器创建一个代理。
 * @see #getAdvicesAndAdvisorsForBean
 */
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    // 如果bean不为空
    if (bean != null) {
        // 获取缓存键,通常是bean的类和名称的组合,用于在缓存中存取与该bean相关的元数据
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 从earlyProxyReferences集合中移除缓存键,如果当前bean不在该集合中,表示需要代理该bean
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 如果需要的话,对bean进行包装(代理)
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    // 如果bean为null或不需要代理,直接返回原始bean
    return bean;
}

这个方法主要功能

  • 确定代理需求:该方法的主要目的是确定是否需要为Bean创建代理。如果需要,wrapIfNecessary方法将负责实际的代理创建过程。
  • 优化性能:通过缓存键和earlyProxyReferences集合,可以避免重复处理同一个Bean,提高性能。
  • 集成AOP功能:为需要代理的Bean创建代理对象,使得AOP切面能够在Bean的方法调用前后执行。

2.2 wrapIfNecessary方法源码分析

代码提出来分析

/**
 * 如果需要,对给定的bean进行包装(代理),即如果它符合被代理的条件。
 * @param bean 原始的bean实例
 * @param beanName bean的名称
 * @param cacheKey 用于元数据访问的缓存键
 * @return 包装(代理)后的bean,或原始的bean实例
 */
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 如果beanName不为空且targetSourcedBeans集合包含这个beanName,直接返回原始bean
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    // 如果advisedBeans缓存中显示这个bean不需要代理,直接返回原始bean
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    // 如果bean的类是基础设施类(如Spring内部使用的类)或应该跳过代理
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        // 将这个bean标记为不需要代理
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

    // 获取适用于这个bean的通知和切面
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    // 如果有通知和切面,则创建代理
    if (specificInterceptors != DO_NOT_PROXY) {
        // 将这个bean标记为需要代理
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // 创建代理对象
        Object proxy = createProxy(
                bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        // 缓存代理对象的类型
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }

    // 如果没有适用于这个bean的通知和切面,将这个bean标记为不需要代理
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

  wrapIfNecessary方法的主要功能是检查给定的Bean是否需要进行AOP代理,并在必要时为其创建代理对象。该方法在Bean初始化后被调用,以确保符合条件的Bean能够被AOP代理,从而使AOP切面能够在Bean的方法调用前后执行。

2.3 时序图演示触发机制

在这里插入图片描述

详细分析和说明:

  1. Caller请求Bean实例
  • Caller调用BeanFactory来获取Bean实例。
  • 这一步表示应用程序或其他Bean调用者需要获取一个Bean实例。
  1. BeanFactory创建Bean实例
  • BeanFactory创建Bean实例,准备进行初始化。
  • 这包括通过Spring的依赖注入机制构造Bean实例。
  1. 调用postProcessAfterInitialization
  • BeanFactory调用AOPProxypostProcessAfterInitialization方法。
  • 这是Spring AOP框架用于在Bean初始化后进行处理的关键步骤。
  1. 获取缓存键
  • AOPProxy获取缓存键(cacheKey),通常是Bean的类和名称的组合。
  • 缓存键用于在缓存中存取与Bean相关的元数据。
  1. 检查earlyProxyReferences集合
  • AOPProxy检查earlyProxyReferences集合,判断当前Bean是否已经处理过。
  • 这是为了避免重复处理同一个Bean,提高性能。
  1. 判断Bean是否需要代理
  • 通过检查earlyProxyReferences集合,决定是否需要继续代理。
  • 如果Bean需要代理,进入wrapIfNecessary方法。
  1. 调用wrapIfNecessary
  • wrapIfNecessary方法负责实际的代理创建过程。
  • 包括进一步的检查和代理对象的创建。
  1. 检查targetSourcedBeans集合
  • 判断Bean是否在targetSourcedBeans集合中,如果是,直接返回原始Bean
  • targetSourcedBeans集合用于存储一些特殊的Bean,不需要代理。

targetSourcedBeans 集合主要用于管理那些使用动态目标源的 Bean。在创建代理时,如果一个 Bean在这个集合中,Spring 会进行特殊处理,通常直接返回原始 Bean,而不是创建新的代理对象。这种机制确保了特殊 Bean的正确处理和高效运行。

  1. 检查advisedBeans集合
  • 检查缓存中是否已经标记了该Bean不需要代理,如果是,直接返回原始Bean
  • 提高处理效率,避免重复检查。
  1. 检查基础设施类和跳过条件
  • 判断Bean是否为Spring的基础设施类或是否有其他跳过条件。
  • 基础设施类通常不需要代理,因为它们是框架内部使用的。
  1. 获取通知和切面
  • 调用getAdvicesAndAdvisorsForBean方法,获取适用于该Bean的通知(Advice)和切面(Advisor)。
  • Advisor对象包含了切面逻辑,Advice对象包含了实际的通知逻辑。
  1. 检查是否有specificInterceptors
  • 判断是否有适用于该Bean的通知和切面。
  • 如果有,继续创建代理对象。
  1. 创建代理对象
  • 调用createProxy方法,通过ProxyFactory创建代理对象。
  • 代理对象将拦截方法调用,并应用切面逻辑。
  1. 返回代理对象
  • 代理对象创建完成后,返回给AOPProxy
  • AOPProxy将代理对象返回给BeanFactory
  1. 返回原始Bean或代理对象:
  • 如果Bean不需要代理,返回原始Bean
  • 如果需要代理,返回代理对象。
  1. BeanFactory返回结果:
  • BeanFactory将最终的Bean实例(可能是原始Bean或代理对象)返回给Caller

3. AOP代理逻辑的执行

3.1 AOP代理如何使用拦截器

AOP代理拦截方法调用时,它会依次调用配置的拦截器链。每个拦截器都通过invoke方法处理方法调用。整个流程如下:

  • 方法调用被代理拦截:当一个方法被调用时,AOP代理会首先拦截这个调用。
  • 拦截器链处理:调用会被传递给拦截器链中的第一个拦截器。
  • 执行拦截器链:每个拦截器的invoke方法都会执行其逻辑,并决定是否调用下一个拦截器。
  • 目标方法执行:如果所有拦截器都允许,最终会调用目标方法本身。
  • 返回结果或处理异常:目标方法执行完成后,结果或异常会逐个传递回拦截器链,直到返回给最初调用方法的客户端。

比如给个简单拦截器的例子:

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 在目标方法执行前
        System.out.println("Before method: " + invocation.getMethod().getName());

        // 调用目标方法
        Object result = invocation.proceed();

        // 在目标方法执行后
        System.out.println("After method: " + invocation.getMethod().getName());

        return result;
    }
}

在这个示例中,invoke方法是拦截器的核心。

  • 当方法被调用时,invoke方法会先打印“方法调用前”,然后调用invocation.proceed()执行目标方法,最后在方法执行后或发生异常时打印相应的信息。
  • invocation.proceed()方法用于继续调用拦截器链中的下一个拦截器,直到最终调用目标方法。

3.2 proceed方法源码分析

在这里插入图片描述

代码提出来分析:

/**
 * 逐个调用拦截器链中的拦截器,并在最后执行目标方法(连接点)。
 * @return 调用链中最后一个拦截器或目标方法的返回值
 * @throws Throwable 如果拦截器或目标方法抛出异常
 */
@Override
@Nullable
public Object proceed() throws Throwable {
    // 我们从索引 -1 开始,并提前递增索引。
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 如果当前拦截器索引等于拦截器链的最后一个索引,则调用目标方法
        return invokeJoinpoint();
    }

    // 获取当前索引的拦截器或拦截建议
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    // 检查是否是 InterceptorAndDynamicMethodMatcher 类型
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        // 如果是,评估动态方法匹配器
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        // 获取目标类的 Class 对象
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
        // 检查方法是否匹配
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            // 如果匹配成功,调用拦截器的 invoke 方法
            return dm.interceptor.invoke(this);
        } else {
            // 动态匹配失败,跳过这个拦截器并调用链中的下一个
            return proceed();
        }
    } else {
        // 如果是普通拦截器,直接调用其 invoke 方法
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

/**
 * 使用反射调用连接点。
 * 子类可以重写此方法以使用自定义调用。
 * 
 * @return 连接点的返回值
 * @throws Throwable 如果调用连接点导致异常
 */
@Nullable
protected Object invokeJoinpoint() throws Throwable {
    // 使用反射调用目标对象的目标方法,并传递参数
    return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
}

  proceed方法和invokeJoinpoint方法共同实现了AOP拦截器链的执行逻辑。proceed方法依次执行拦截器链中的每个拦截器,并在链的末尾调用目标方法。invokeJoinpoint方法通过反射调用目标方法并返回其结果。这两个方法结合起来,使得AOP能够在目标方法执行前后插入各种横切关注点,如事务管理、日志记录等。

这里判断动态方法匹配器是干嘛的?

  • 静态方法匹配器:在编译时或配置时确定方法是否匹配,例如方法名、参数类型等。这种匹配是静态的,无法根据实际调用时的参数值进行判断。
  • 动态方法匹配器:在方法调用时动态评估方法匹配条件,例如根据实际传递的参数值判断是否匹配。这种匹配是动态的,可以提供更灵活的控制。

普通的AOP通常使用静态匹配,这里不用深究动态匹配器。

3.3 时序图

在这里插入图片描述

时序图详细说明:

  1. Caller调用代理方法
  • Caller调用代理对象的方法,开始AOP的执行过程。
  • 代理对象是通过AOP创建的,包含了拦截器链。
  1. 创建ReflectiveMethodInvocation实例
  • 在代理对象内部,Spring AOP创建一个ReflectiveMethodInvocation实例。该实例封装了目标方法、拦截器链以及方法参数。
  • ReflectiveMethodInvocation负责处理方法调用的整个过程。
  1. 调用proceed()
  • 代理对象调用ReflectiveMethodInvocationproceed()方法,开始执行拦截器链。
  1. 遍历拦截器链
  • ReflectiveMethodInvocation遍历拦截器链中的每个拦截器。
  • 对每个拦截器,ReflectiveMethodInvocation调用其invoke(this)方法。
  • 拦截器在调用目标方法之前和之后执行自定义逻辑。
  1. 调用invokeJoinpoint()
  • 如果拦截器链中没有拦截器或所有拦截器已经执行完毕,ReflectiveMethodInvocation直接调用invokeJoinpoint()方法。
  • invokeJoinpoint()通过反射调用目标方法。
  1. 调用目标方法
  • ReflectiveMethodInvocation通过反射调用实际的目标方法。
  • 目标方法执行并返回结果。
  1. 返回结果
  • 目标方法执行完成后,结果返回给ReflectiveMethodInvocation
  • ReflectiveMethodInvocation将结果返回给所有拦截器,依次向上返回。
  1. 代理对象返回最终结果
  • ReflectiveMethodInvocation最终将结果返回给代理对象。
  • 代理对象将结果返回给Caller

欢迎一键三连~

有问题请留言,大家一起探讨学习

----------------------Talk is cheap, show me the code-----------------------

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

特价电影票对接接口平台推荐?微客云影票

特价电影票对接接口平台推荐 一、常见的较好平台 微客云影票与全国 12000 多家影院建立了合作&#xff0c;覆盖范围广&#xff0c;其提供的电影票价格普遍低于市场价&#xff0c;平均每张票能省下不少钱&#xff0c;在万达、CGV 等大型影院优惠更为显著。 二、判断平台好坏的…

【Qt】Qt C++ Widget中嵌入qml

1. 效果 2. 方法 使用QQuickWidget方式 QQuickWidget *view new QQuickWidget;view->setSource(QUrl::fromLocalFile("myqmlfile.qml"));view->show();除了QQuickWidget方式还可以使用QQuickView方式&#xff0c;请自行查阅资料 3. 代码 3.1 工程目录 3.2 …

Weblogic部署

要安装weblogic&#xff0c;首先要有java环境&#xff0c;因此需要先安装jdk。 这里需要注意&#xff0c;weblogic版本不同&#xff0c;对应的jdk版本也不同&#xff0c;我在这里就踩了很多坑&#xff0c;我这里下载的是fmw_12.2.1.4.0_wls_lite_generic.jar对应的是jdk-8u333…

visual prompt tuning和visual instruction tuning

visual prompt tuning&#xff1a;作为一种微调手段&#xff0c;其目的是节省参数量&#xff0c;训练时需要优化的参数量小。 输入&#xff1a;视觉信息image token可学习的prompt token 处理任务&#xff1a;比如常见的分类任务 visual prompt tuning visual instruction tu…

唯徳知识产权管理系统 DownloadFileWordTemplate 文件读取漏洞复现

0x01 产品简介 唯徳知识产权管理系统,由深圳市唯德科创信息有限公司精心打造,旨在为企业及代理机构提供全方位、高效、安全的知识产权管理解决方案。该系统集成了专利、商标、版权等知识产权的全面管理功能,并通过云平台实现远程在线办公,提升工作效率。是一款集知识产权申…

客户说了算!精益产品开发,让中小企业精准触达用户需求!——张驰咨询

随着全球经济环境的波动&#xff0c;特别是疫情后经济复苏进程的反复&#xff0c;很多中小制造企业面临产品滞销、同质化严重和内卷竞争加剧的困境。市场饱和、利润微薄&#xff0c;还在新产品开发上遇到了研发人才不足、资金短缺等问题。许多企业在这场市场博弈中徘徊在生死边…

IP协议及相关特性

IP协议负责地址管理和路由选择。它的组成为&#xff1a; 接下来我们将对其中较重要的部分进行介绍。 4位版本&#xff1a;这里的四位版本只有两个取值 分别为IPv4和IPv6&#xff0c;这两个额分别为不同的IP协议&#xff0c;但是现在主流的还是IPv4但是近年来IPv6在中国的普及率…

关于报表新入职及进阶培训入口教程

关于报表新入职及进阶培训入口教程 一、网站二、登陆三、报名入口四、缴费及注意事项1. 报名2. 注意事项 五、学习1.在首页时&#xff0c;可以点个人中心进入学习。2.进入后&#xff0c;可以进入如下步骤。 六、完 一、网站 教育事业统计在线培训 二、登陆 注&#xff1a;如果…

C++速通LeetCode简单第17题-爬楼梯(全网最简单)

思路要点&#xff1a;将问题转化为求斐波那契数列的第n项&#xff0c;然后迭代。 思路分析&#xff1a;最后一次爬的阶数不是1就是2&#xff0c;假设爬n阶的方法数是f(n)&#xff0c;假设最后一次爬1阶&#xff0c;那么爬前面的 n-1阶的方法数是f(n-1)&#xff1b;假设最后一次…

20240911泰山杯初赛--temp

Wireshark打开temp.pcap流量包&#xff0c;发现有很多ICMP协议包。 一些ICMP数据包较大&#xff0c;且可发现&#xff0c;明显在传输HTTP协议数据内容&#xff1a; 右键&#xff0c;【显示分组字节】&#xff0c;进一步分析这些HTTP数据&#xff1a; GET /test.html HTTP/1.…

C盘清理不能偷懒!用这方法快速清理10G以上垃圾 操作简单又安全

C盘清理不能偷懒&#xff01;用这方法快速清理10G以上垃圾 操作简单又安全。到现在为止&#xff0c;还有很多的人不知道怎么清理C盘&#xff0c;主要是因为Windows操作系统相对复杂&#xff0c;其文件管理和存储结构对于非专业人士来说可能难以理解。许多用户可能不清楚哪些文件…

重磅!WOS、Scopus数据库相继更新,多本期刊被剔除(附下载)

关注GZH【欧亚科睿学术】&#xff0c;一键获取最新期刊目录列表 节后首天&#xff0c;科睿唯安及爱思唯尔相继更新了数据库&#xff0c;小编给大家总结了各数据库的期刊变动情况&#xff0c;供大家参考。详情如下&#xff1a; 01 SCIE/SSCI目录更新 2024年9月17日&#xff0c…

计算机毕业设计公交站点线路查询网站登录注册搜索站点线路车次/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序

选题背景‌&#xff1a; 随着城市化进程的加快&#xff0c;公共交通成为城市居民出行的重要方式。然而&#xff0c;传统的公交站点线路查询方式往往依赖于纸质地图或简单的电子显示屏&#xff0c;查询效率低下且信息更新不及时。因此&#xff0c;开发一个功能全面、易于使用的…

【SQL】百题计划:SQL对于空值的比较判断。

[SQL]百题计划 方法&#xff1a; 使用 <> (!) 和 IS NULL [Accepted] 想法 有的人也许会非常直观地想到如下解法。 SELECT name FROM customer WHERE referee_Id <> 2;然而&#xff0c;这个查询只会返回一个结果&#xff1a;Zach&#xff0c;尽管事实上有 4 个…

MAGDA:多智能体指南驱动的诊断助手

MAGDA&#xff1a;多智能体指南驱动的诊断助手 秒懂大纲提出背景精细拆解输入输出全流程创意视角中文意译 论文&#xff1a;MAGDA: Multi-agent guideline-driven diagnostic assistance 秒懂大纲 ├── MAGDA: Multi-agent guideline-driven diagnostic assistance【研究主…

DockerLinux安装DockerDocker基础

Linux软件安装 yum命令安装 通过yum命令安装软件,是直接把软件安装到Linux系统中 安装和卸载都比较麻烦,因为软件和系统是强关联的 Docker docker是一种容器技术,可以解决软件和系统强关联关系,使得软件的安装和卸载更方便,它可以将我们的应用以及依赖进行打包,制作出一个镜…

教程 | ArcGIS Pro如何自动保存数据编辑内容

目录 1、工程自动保存 2、数据编辑自动保存 世界上最痛苦的事情就是&#xff1a; 软件崩溃&#xff0c;我没保存&#xff01;&#xff01;&#xff01; 电脑死机&#xff0c;我没保存&#xff01;&#xff01;&#xff01; 突然断电&#xff0c;我没保存&#xff01;&…

【电脑组装】✈️从配置拼装到安装系统组装自己的台式电脑

目录 &#x1f378;前言 &#x1f37b;一、台式电脑基本组成 &#x1f37a;二、组装 &#x1f379;三、安装系统 &#x1f44b;四、系统设置 &#x1f440;五、章末 &#x1f378;前言 小伙伴们大家好&#xff0c;上篇文章分享了在平时开发的时候遇到的一种项目整合情况&…

如何关闭前端Chrome的debugger反调试

1、禁用浏览器断点 2. 把控制台独立一个窗口

GitLab CI_CD 从入门到实战笔记

第1章 认识GitLab CI/CD 1.3 GitLab CI/CD的几个基本概念 GitLab CI/CD由以下两部分构成。 &#xff08;1&#xff09;运行流水线的环境。它是由GitLab Runner提供的&#xff0c;这是一个由GitLab开发的开源软件包&#xff0c;要搭建GitLab CI/CD就必须安装它&#xff0c;因…