手撕spring bean的加载过程

news2024/9/27 7:25:30

这里我们采用手撕源码的方式,开始探索spring boot源码中最有意思的部分-bean的生命周期,也可以通过其中的原理理解很多面试以及工作中偶发遇到的问题。

springboot基于约定大于配置的思想对spring进行优化,使得这个框架变得更加轻量化,集成各种starter组件时使其能够更加全面。

1、SpringApplication启动类的配置与软件包的反射加载

通常我们在建立一个新的spring boot项目时,利用idea脚手架生成模板内部会自带一个标注有SpringApplication注解的启动类,如下所示:

/**
 * @author : spring
 * {@code @description:}
 * {@code @date} : 2024/2/4
 * {@code @modified} By: spring
 * {@code @project:} spring-plus
 */
@SpringApplication(scanBeanPackagePath = "com/hlc/springplus/test")
public class ApplicationStarter {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new DefaultApplicationContext(ApplicationStarter.class);
        TestBean contextBean = (TestBean) applicationContext.getBean("test");
        contextBean.test();
    }
}

由于本文的代码全部都是手撕,所以会与spring boot的源码有所不同,个人主义完美凸显。

言回正传,用过spring boot的人都知道它主要的特色基础是它的“容器”的概念,我们可以通过配置文件、注解、导入以及反射实例化后调用通用应用上下文注入的方式将我们的bean交给spring容器管理,那么这里启动类启动后“容器”是怎么识别出我们标准或配置的bean信息同时将其实例化、配置属性、配置名称.....的呢?

那么下面就是一个过程

获取启动类上标注注解中的包路径值
SpringApplication annotation = appconfig.getAnnotation(SpringApplication.class);

String beanPackagePath = annotation.scanBeanPackagePath();
String path = beanPackagePath.replace('.', '/');

这里的path就是我们实际的包路径,为什么需要将.替换城/呢?实际上我们配置的包路径是软件包中的相对路径,并不是Resource获取时规定的路径格式。

获取当前类的类加载器并根据路径加载URL获取文件
 ClassLoader classLoader = DefaultApplicationContext.class.getClassLoader();
 URL resource = classLoader.getResource(path);

 File file = new File(resource.getFile());

DefaultApplicationContext就是我当前类的名称,后续串完全部的流程会将全部的代码挂出的,这里的类加载器获取资源的方式是比较常用的。

需要注意的是这里拿到的file有可能是文件夹,也可能是文件。

通过文件夹或文件夹获取字节码
 for (File item : files) {
      int begin = item.getAbsolutePath().indexOf("com");
        int end = item.getAbsolutePath().indexOf('.');

        String className = item.getAbsolutePath().substring(begin, end).replace('\\', '.');

        try {
            Class<?> clazz = Class.forName(className);
            if (clazz.isAnnotationPresent(Component.class)) {
                beanClazzList.add(clazz);
                //收集后置处理器(意图是收集后置处理器而不是收集bean对象)
                if (BeanPostprocessor.class.isAssignableFrom(clazz)) {
                    beanPostprocessorList.add((BeanPostprocessor) clazz.getDeclaredConstructor().newInstance());
                }
            }
        } catch (Exception e) {

            throw new RuntimeException(e.getMessage());
        }                            
 }

上面的类路径获取方式以及判断字节码是否实现了接口BeanPostprocessor的判断、字节码是否标注了注解Component的判断都是比较常用的方法。

那么通过此三步就将全部需要加载的字节码文件都获取到我们的成员变量beanClazzList列表中去了。

2、ApplicationContext接口的定义以及相关注解的配置

虽然我们解决了待加载bean的字节码列表的收集问题,但是spring boot的容器我们还没有加载出来,也没有实现相关注解的配置,注解标注了bean的身份、名称、类型、加载方式、加载条件、加载顺序、依赖关系等。

ApplicationContext接口的定义
public interface ApplicationContext extends BeanFactory {
    Object getBean(String beanName);

    <T> void registerBean(T bean, Class<T> clazz, String name);
}

ApplicationContext接口的释义是“应用上下文”,在计算机科学中,上下文表示进程在执行过程中系统内部的资源情况与中断向量表的记录情况,总之代表的是进程所处的逻辑环境。这里顾名思义ApplicationContext代表的也自然就是bean所处的环境。也就是我们口中的spring boot的容器。

对于基本的ApplicationContext能力而言,它应当具备获取bean对象与注册bean对象的能力。所以这里定义的两个基础能力接口。在考虑如何实现它之前,我们还需要配置以下bean相关的其他注解:

组件注解(标注类型为bean组件)

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Component {
    String name() default "";
}

注入注解(标注为某类型注入某bean的属性值)

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {
    String value() default "";
}

作用域注解(标注单例、原型等生存周期的bean类型)

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Scope {
    String value();
}

懒加载注解(标注即用即加载还是立刻加载)

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Lazy {
}

初始化方法注解(标注bean的初始化方法)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitBeanMethod {
}

3、实现ApplicationContext接口与bean加载原理

上述我们定义了相关接口与注解,接下来我们实现容器上下文接口以及描述bean是如何加载到容器内部管理的。

那么这里我们就先将实现ApplicationContext接口的DefaultApplicationContext.java代码放在下方:

public class DefaultApplicationContext implements ApplicationContext {
    private final Class<?> appconfig;

    private List<Class<?>> beanClazzList = new LinkedList<>();
    private Map<String, Object> singletonBeanMap = new ConcurrentHashMap<>();

    private Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
    private List<BeanPostprocessor> beanPostprocessorList = new LinkedList<>();

    public DefaultApplicationContext(Class<?> appconfig) {
        this.appconfig = appconfig;
        //1、扫描启动类注解的字节码列表
        scanBeansByPackage(beanClazzList);
        //2、注册bean的BeanDefinition初始配置信息
        initBeanDefinition(beanClazzList, beanDefinitionMap);
        //3、实例化单例bean并存入map中
        instanceSingletonBeans(beanDefinitionMap, singletonBeanMap);
    }

    @Override
    public Object getBean(String beanName) {
        BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
        if (null != beanDefinition && "prototype".equals(beanDefinition.getScope())) {
            try {
                return beanDefinition.getBeanClazz().getDeclaredConstructor().newInstance();
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                     NoSuchMethodException e) {
                throw new RuntimeException(e.getMessage());
            }
        }
        return singletonBeanMap.get(beanName);
    }

    @Override
    public <T> void registerBean(T bean, Class<T> clazz, String name) {
        singletonBeanMap.put(name, bean);
    }


    /**
     * 扫描bean的字节码列表
     *
     * @param beanClazzList bean的字节码列表(待填充)
     */
    protected void scanBeansByPackage(List<Class<?>> beanClazzList) {

        if (null != appconfig && appconfig.isAnnotationPresent(SpringApplication.class)) {

            SpringApplication annotation = appconfig.getAnnotation(SpringApplication.class);
            if (null != annotation) {

                String beanPackagePath = annotation.scanBeanPackagePath();
                String path = beanPackagePath.replace('.', '/');

                ClassLoader classLoader = DefaultApplicationContext.class.getClassLoader();
                URL resource = classLoader.getResource(path);

                if (resource != null) {
                    File file = new File(resource.getFile());
                    if (file.isDirectory()) {
                        File[] files = file.listFiles();
                        if (files != null) {
                            for (File item : files) {

                                loadAndFilterBeanClazzes(beanClazzList, item);
                            }
                        }
                    } else {

                        loadAndFilterBeanClazzes(beanClazzList, file);
                    }
                }
            } else {
                throw new RuntimeException("Annotation SpringApplication is not exist");
            }
        } else {
            throw new RuntimeException("Annotation SpringApplication is not exist and appconfig is null");
        }
    }

    /**
     * 加载bean的字节码列表并过滤
     *
     * @param beanClazzList bean的字节码列表(待填充)
     * @param item          文件或文件夹
     */
    private void loadAndFilterBeanClazzes(List<Class<?>> beanClazzList, File item) {
        int begin = item.getAbsolutePath().indexOf("com");
        int end = item.getAbsolutePath().indexOf('.');

        String className = item.getAbsolutePath().substring(begin, end).replace('\\', '.');

        try {
            Class<?> clazz = Class.forName(className);
            if (clazz.isAnnotationPresent(Component.class)) {
                beanClazzList.add(clazz);
                //收集后置处理器(意图是收集后置处理器而不是收集bean对象)
                if (BeanPostprocessor.class.isAssignableFrom(clazz)) {
                    beanPostprocessorList.add((BeanPostprocessor) clazz.getDeclaredConstructor().newInstance());
                }
            }
        } catch (Exception e) {

            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 注册bean的BeanDefinition初始配置信息
     *
     * @param beanClazzList     bean的字节类型列表
     * @param beanDefinitionMap bean的BeanDefinition初始配置信息池子
     */
    private void initBeanDefinition(List<Class<?>> beanClazzList, Map<String, BeanDefinition> beanDefinitionMap) {
        if (null != beanClazzList && !beanClazzList.isEmpty()) {
            for (Class<?> clazz : beanClazzList) {
                BeanDefinition beanDefinition = new BeanDefinition();
                Component component = clazz.getAnnotation(Component.class);
                Scope scope = clazz.getAnnotation(Scope.class);
                Lazy lazy = clazz.getAnnotation(Lazy.class);

                beanDefinition.setBeanClazz(clazz);
                beanDefinition.setLazy(null != lazy);
                beanDefinition.setScope(null != scope ? scope.value() : "prototype");
                String beanName = component.name();
                if (beanName.isEmpty()) {
                    beanName = clazz.getSimpleName();
                }
                beanDefinitionMap.put(beanName, beanDefinition);
            }
        }
    }

    /**
     * 实例化单例bean
     *
     * @param beanDefinitionMap bean定义信息
     * @param singletonBeanMap  单例bean池子
     */
    private void instanceSingletonBeans(Map<String, BeanDefinition> beanDefinitionMap, Map<String, Object> singletonBeanMap) {

        if (null != beanDefinitionMap && !beanDefinitionMap.isEmpty()) {
            for (Class<?> clazz : beanDefinitionMap.values().stream().map(BeanDefinition::getBeanClazz).toList()) {
                if (clazz.isAnnotationPresent(Scope.class) && "prototype".equals(clazz.getAnnotation(Scope.class).value())) {
                    continue;
                }
                if (!clazz.isAnnotationPresent(Lazy.class)) {
                    //实例化bean
                    try {
                        Component component = clazz.getAnnotation(Component.class);
                        String beanName = component.name();
                        if (null == beanName || beanName.isEmpty()) {
                            beanName = clazz.getSimpleName();
                        }
                        //1、实例化bean
                        Object newInstance = clazz.getDeclaredConstructor().newInstance();
                        //2、属性填充
                        attributeAutoWiredPadding(clazz, newInstance);
                        //3、aware能力透传
                        awareBeanInstancePadding(newInstance);
                        //4、初始化
                        //4.1、后置处理器 初始化前执行
                        for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {
                            newInstance = beanPostprocessor.beforeInitialization(newInstance, beanName);
                        }
                        //4.2、初始化bean执行
                        //检查是否实现初始化Bean的接口
                        initializeBeanInstancePadding(newInstance);
                        //检查是否配置过init方法
                        initBeanMethodInstancePadding(newInstance);
                        //4.3、后置处理器能力 初始化后执行
                        for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {
                            newInstance = beanPostprocessor.afterInitialization(newInstance, beanName);
                        }

                        singletonBeanMap.put(beanName, newInstance);
                    } catch (InvocationTargetException | NoSuchMethodException | InstantiationException |
                             IllegalAccessException e) {
                        throw new RuntimeException(e.getMessage());
                    }
                }
            }
        }
    }



    /**
     * bean的属性填充
     *
     * @param beanClazz   bean的字节类型
     * @param newInstance 实例化的bean
     */
    private void attributeAutoWiredPadding(Class<?> beanClazz, Object newInstance) {
        if (null != beanClazz) {
            Field[] fields = beanClazz.getDeclaredFields();

            for (Field field : fields) {

                if (field.isAnnotationPresent(AutoWired.class)) {

                    field.setAccessible(true);
                    Class<?> declaringClass = field.getType();

                    AutoWired autoWired = field.getAnnotation(AutoWired.class);
                    String name = autoWired.value();

                    if (null == name || name.isEmpty()) {
                        name = declaringClass.getSimpleName();
                    }

                    Object fieldBean = singletonBeanMap.get(name);

                    if (null == fieldBean) {

                        List<Class<?>> beanClazzList = new LinkedList<>();
                        beanClazzList.add(declaringClass);
                        initBeanDefinition(beanClazzList, beanDefinitionMap);

                        Map<String, BeanDefinition> definitionMap = new HashMap<>();
                        definitionMap.put(name, beanDefinitionMap.get(name));
                        instanceSingletonBeans(definitionMap, singletonBeanMap);

                        try {
                            field.set(newInstance, singletonBeanMap.get(name));
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException(e.getMessage());
                        }
                    } else {

                        try {
                            field.set(newInstance, fieldBean);
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException(e.getMessage());
                        }
                    }
                }
            }
        }
    }

    /**
     * bean的Aware接口的实现类填充
     *
     * @param bean bean实例对象
     */
    private void awareBeanInstancePadding(Object bean) {
        if (null != bean) {
            if (bean instanceof Aware) {
                if (bean instanceof ApplicationContextAware) {
                    ((ApplicationContextAware) bean).setApplicationContext(this);
                }
                if (bean instanceof BeanNameAware) {
                    ((BeanNameAware) bean).setBeanName();
                }
            }
        }
    }

    /**
     * bean的初始化方法填充
     *
     * @param bean 实例化的bean
     */
    private void initializeBeanInstancePadding(Object bean) {
        if (null != bean) {
            if (bean instanceof InitializingBean) {
                ((InitializingBean) bean).afterPropertiesSet();
            }
        }
    }

    /**
     * bean的初始化方法填充
     *
     * @param newInstance 实例化的bean
     */
    private void initBeanMethodInstancePadding(Object newInstance) {
        if (null != newInstance) {
            Method[] methods = newInstance.getClass().getDeclaredMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(InitBeanMethod.class)) {
                    method.setAccessible(true);
                    try {
                        method.invoke(newInstance);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new RuntimeException(e.getMessage());
                    }
                }
            }
        }
    }
    private void sortBeanInstanceClazzList() {

    }
}

这里我们着重描述一下关于bean加载原理的这一块代码:

private void instanceSingletonBeans(Map<String, BeanDefinition> beanDefinitionMap, Map<String, Object> singletonBeanMap) {

        if (null != beanDefinitionMap && !beanDefinitionMap.isEmpty()) {
            for (Class<?> clazz : beanDefinitionMap.values().stream().map(BeanDefinition::getBeanClazz).toList()) {
                if (clazz.isAnnotationPresent(Scope.class) && "prototype".equals(clazz.getAnnotation(Scope.class).value())) {
                    continue;
                }
                if (!clazz.isAnnotationPresent(Lazy.class)) {
                    //实例化bean
                    try {
                        Component component = clazz.getAnnotation(Component.class);
                        String beanName = component.name();
                        if (null == beanName || beanName.isEmpty()) {
                            beanName = clazz.getSimpleName();
                        }
                        //1、实例化bean
                        Object newInstance = clazz.getDeclaredConstructor().newInstance();
                        //2、属性填充
                        attributeAutoWiredPadding(clazz, newInstance);
                        //3、aware能力透传
                        awareBeanInstancePadding(newInstance);
                        //4、初始化
                        //4.1、后置处理器 初始化前执行
                        for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {
                            newInstance = beanPostprocessor.beforeInitialization(newInstance, beanName);
                        }
                        //4.2、初始化bean执行
                        //检查是否实现初始化Bean的接口
                        initializeBeanInstancePadding(newInstance);
                        //检查是否配置过init方法
                        initBeanMethodInstancePadding(newInstance);
                        //4.3、后置处理器能力 初始化后执行
                        for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {
                            newInstance = beanPostprocessor.afterInitialization(newInstance, beanName);
                        }

                        singletonBeanMap.put(beanName, newInstance);
                    } catch (InvocationTargetException | NoSuchMethodException | InstantiationException |
                             IllegalAccessException e) {
                        throw new RuntimeException(e.getMessage());
                    }
                }
            }
        }
    }

到这里我们已经了解在获取待加载bean的字节码列表之后,我们需要将bean的配置信息存储到我们的beanDefinitionMap中,再根据beanDefinitionMap将其中的单例bean信息加载成一个个bean放入单例bean map中,这里的存储key统一都是beanName。

看以上代码我们不难分析出通过bean配置信息加载bean的过程中,一个bean需要经过6步周期性工作才会被放入容器中给我们使用。以下是图示:

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【从Python基础到深度学习】1. 安装Python PyCharm

前言&#xff1a; 为了帮助大家快速入门机器学习-深度学习&#xff0c;从今天起我将用100天的时间将大学本科期间的所学所想分享给大家&#xff0c;和大家共同进步。【从Python基础到深度学习】系列博客中我将从python基础开始通过知识和代码实践结合的方式进行知识的分享和记…

Apollo分布式配置中心

携程框架部门研发的开源配置管理中心&#xff0c;能够集中化管理应用不同环境、不同集群的配置&#xff0c;配置修改后能够实时推送到应用端 用户在配置中心对配置进行修改并发布配置中心通知Apollo客户端有配置更新Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知…

网络安全挑战:威胁建模的应对策略与实践

在数字威胁不断演变的时代&#xff0c;了解和降低网络安全风险对各种规模的组织都至关重要。威胁建模作为安全领域的一个关键流程&#xff0c;提供了一种识别、评估和应对潜在安全威胁的结构化方法。本文将深入探讨威胁建模的复杂性&#xff0c;探索其机制、方法、实际应用、优…

基于高通滤波器的ECG信号滤波及心率统计matlab仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 ECG信号简介 4.2 高通滤波器原理 4.3 心率统计 5.完整工程文件 1.课题概述 通过高通滤波器对ECG信号进行滤波&#xff0c;然后再统计其心率。 2.系统仿真结果 3.核心程序与模型 版本&#xff1a…

Arm发布新的人工智能Cortex-M处理器

Arm发布了一款新的Cortex-M处理器&#xff0c;旨在为资源受限的物联网&#xff08;IoT&#xff09;设备提供先进的人工智能功能。这款新的Cortex-M52声称是最小的、面积和成本效率最高的处理器&#xff0c;采用了Arm Helium技术&#xff0c;使开发者能够在单一工具链上使用简化…

巴尔加瓦算法图解:算法运用。

树 如果能将用户名插入到数组的正确位置就好了&#xff0c;这样就无需在插入后再排序。为此&#xff0c;有人设计了一种名为二叉查找树(binary search tree)的数据结构。 每个node的children 都不大于两个。对于其中的每个节点&#xff0c;左子节点的值都比它小&#xff0c;…

ChatGPT高效提问—prompt常见用法

ChatGPT高效提问—prompt常见用法 1.1 角色扮演 ​ prompt最为常见的用法是ChatGPT进行角色扮演。通常我们在和ChatGPT对话时&#xff0c;最常用的方式是一问一答&#xff0c;把ChatGPT当作一个单纯的“陪聊者”。而当我们通过prompt为ChatGPT赋予角色属性后&#xff0c;即使…

debian12 - openssh-9.6.P1的编译安装

文章目录 debian12 - openssh-9.6.P1的编译安装概述笔记备注END debian12 - openssh-9.6.P1的编译安装 概述 在debian12上, 源码编译安装了openssl3.2 导致ssh失败. lostspeeddebian12d4x64:~$ openssl version OpenSSL 3.2.0 23 Nov 2023 (Library: OpenSSL 3.2.0 23 Nov 2…

ywtool inspect命令

一.巡检介绍 日巡检是通过定时任务每天凌晨2点30进行巡检周巡检时通过定时任务每周日的凌晨3点进行巡检日巡检日志文件路径:/usr/local/ywtools/log/daily,每周日凌晨2点会将这一周的日巡检日志进行压缩,压缩路径:/usr/local/ywtools/log/backup/daily周巡检日志文件路径:/usr…

Docker-Learn(一)使用Dockerfile创建Docker镜像

1.创建并运行容器 编写Dockerfile&#xff0c;文件名字就是为Dockerfile 在自己的工作工作空间当中新建文件&#xff0c;名字为Docerfile vim Dockerfile写入以下内容&#xff1a; # 使用一个基础镜像 FROM ubuntu:latest # 设置工作目录 WORKDIR /app # 复制当前目…

消息中间件之RocketMQ源码分析(八)

RocketMQ中的消息过滤 RocketMQ设计了消息过滤&#xff0c;来解决大量无意义流量的传输:即对于客户端不需要的消息&#xff0c; Broker就不会传输给客户端&#xff0c;以免浪费宽带&#xff0c;RocketMQ4.2.0支持Tag过滤、SQL92过滤、Filter Server过滤 Tag过滤 第一步:用户发…

MogaNet:高效的多阶门控聚合网络

文章目录 摘要1、简介2、相关工作2.1、视觉Transformers2.2、ViT时代的卷积网络 3、从多阶博弈论交互的角度看表示瓶颈4、方法论4.1、MogaNet概述4.2、多阶门控聚合4.3、通过通道聚合进行多阶特征重新分配4.4、实现细节 5、实验5.1、ImageNet分类5.2、密集预测任务5.3、消融实验…

C++ 日期计算器

日期计算器 概要 Date类的规划Date类的实现Date 构造函数Date 拷贝构造函数~Date 析构函数GetMonthDay 求某年某月的天数operator 赋值操作符重载operator 加等操作符重载operator 加号操作符重载operator- 减等操作符重载operator- 减法操作符重载 &#xff08;日期 - 天数&am…

linux C编程入门

Ubuntu 下也有一些可以进行编程的工具&#xff0c;但是大多都只是编辑器&#xff0c; 也就是只能进行代码编辑&#xff0c;如果要编译的话就需要用到 GCC 编译器&#xff0c;使用 GCC 编译器肯定就 要接触到Makefile。 1&#xff1a;hello world!!! 我们所说的编写代码包括两部…

Compose | UI组件(十四) | Navigation-Data - 页面导航传递数据

文章目录 前言传参流程实例说明普通方式传值定义接受参数格式定义接受参数类型获取参数传入参数传参和接受参数效果图 结合 ViewModel 传递参数定义ViewModel在 navigation 定义 ViewModel 实例&#xff0c;并且传入 LoginScreen传入输入框中的值&#xff0c;并且跳转传值获取值…

计算机今年炸了99%的人都踩了这个坑

24年408考研&#xff0c;如果只用王道的复习资料&#xff0c;最多考100-120分 就是这么的现实&#xff0c;王道的资料虽然好&#xff0c;但是并不能覆盖全部的知识点和考点&#xff0c;而且24年的408真题考的很怪&#xff0c;总结起来就是下面这些特点&#xff1a; 偏&#x…

2024年【G2电站锅炉司炉】模拟试题及G2电站锅炉司炉考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 G2电站锅炉司炉模拟试题是安全生产模拟考试一点通生成的&#xff0c;G2电站锅炉司炉证模拟考试题库是根据G2电站锅炉司炉最新版教材汇编出G2电站锅炉司炉仿真模拟考试。2024年【G2电站锅炉司炉】模拟试题及G2电站锅炉…

「数据结构」八大排序2:快排、归并排序

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;初阶数据结构 &#x1f387;欢迎点赞收藏加关注哦&#xff01; 八大排序2 &#x1f349;快速排序&#x1f34c;霍尔版本&#x1f34c;挖坑法&#x1f34c;前后指针法 &#x1f349;快排优化&am…

Docker 容器监控-CIG

目录 一、CIG说明 1. CAdvisor 2. InfluxDB 3. Grafana 二、环境搭建 1. 创建目录 2. 编写 docker-compose.yml 3. 检查并运行容器 三、进行测试 1. 查看 influxdb 存储服务 是否能正常访问 2. 查看 cAdvisor 收集服务能否正常访问 3. 查看 grafana 展现服务&#…

服装设计公司,如何用钉钉实现企业数字化成功转型?

钉钉作为数字化工作平台&#xff0c;为某服装设计公司实现了组织管理的数字化转型&#xff0c;构建了一站式的工作平台。通过钉钉赋能&#xff0c;有利于企业推进组织架构、员工沟通、产品运营和客户服务等方面的数字化、智能化转型。 借助钉钉平台&#xff0c;该服设公司轻松实…