Spring依赖注入源码分析

news2025/1/12 22:49:43

1. 前言

Spring的核心之一就是依赖注入,Spring提供了@Autowired注解来给bean注入依赖。除了注入最基本的bean之外,Spring还做了一些扩展,例如你可以注入Optional,以此来判断依赖的bean是否存在;你还可以注入Map来获得所有候选的bean;当然了ListSet自然也是支持的。
Spring依赖注入是怎么实现的呢?

2. AutowiredAnnotationBeanPostProcessor

Spring的依赖注入是通过一个叫AutowiredAnnotationBeanPostProcessor的后置处理器来完成的,先看类图:
image.png
它实现了BeanPostProcessor接口,代表它是bean的后置处理器,这一点很好理解,给bean注入所需的依赖,不就是扩展bean嘛。

AutowiredAnnotationBeanPostProcessor何时被注册到容器???
以AnnotationConfigApplicationContext为例,在它的构造函数里会启动Spring容器,启动前会手动向容器内注册几个支撑Spring核心功能的bean,其中就包含AutowiredAnnotationBeanPostProcessor。
构造函数会实例化AnnotatedBeanDefinitionReader,它的构造函数里会调用AnnotationConfigUtils#registerAnnotationConfigProcessors()手动注册bean。

if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    /**
     * @see AutowiredAnnotationBeanPostProcessor
     * 完成依赖注入的后置处理器
     */
    RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}

3. 收集注入点

要想完成依赖注入,首先得知道,bean需要注入哪些依赖的bean?
所以在依赖注入前,Spring得先收集注入点。这个收集工作也是由AutowiredAnnotationBeanPostProcessor完成的,方法是AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition()。收集注入点这个步骤是在bean实例化后,初始化前触发的,Spring会调用AbstractAutowireCapableBeanFactory#doCreateBean()来创建bean,创建bean的第一步就是实例化bean,接着就是对bean进行解析寻找注入点,然后通过AbstractAutowireCapableBeanFactory#populateBean()填充bean的属性,即依赖注入。

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
    // 查找注入元数据,注入点
    InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
    // 写入RootBeanDefinition
    metadata.checkConfigMembers(beanDefinition);
}

收集注入点需要解析bean,这个过程是有代价的,所以Spring会将解析过的InjectionMetadata缓存起来,key是beanName,value是InjectionMetadata。

private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
    String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
    InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
    if (InjectionMetadata.needsRefresh(metadata, clazz)) {
        synchronized (this.injectionMetadataCache) {
            metadata = this.injectionMetadataCache.get(cacheKey);
            if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                if (metadata != null) {
                    metadata.clear(pvs);
                }
                metadata = buildAutowiringMetadata(clazz);
                this.injectionMetadataCache.put(cacheKey, metadata);
            }
        }
    }
    return metadata;
}

真正收集注入点的方法是buildAutowiringMetadata(),首先Spring会递归解析,因为父类属性也是要注入的;然后遍历属性Field,找出需要注入的Field;再遍历方法Method,找出需要注入的Method。注入点会统一包装成InjectedElement对象,它有两个重要的子类AutowiredFieldElementAutowiredMethodElement,分别代表属性注入和方法注入。

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    // 循环递归,因为父类的也要处理
    do {
        final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
        // 属性注入
        ReflectionUtils.doWithLocalFields(targetClass, field -> {
            AnnotationAttributes ann = findAutowiredAnnotation(field);
            if (ann != null) {
                if (Modifier.isStatic(field.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static fields: " + field);
                    }
                    return;
                }
                boolean required = determineRequiredStatus(ann);
                currElements.add(new AutowiredFieldElement(field, required));
            }
        });
        // 方法注入
        ReflectionUtils.doWithLocalMethods(targetClass, method -> {
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                return;
            }
            AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                if (Modifier.isStatic(method.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static methods: " + method);
                    }
                    return;
                }
                if (method.getParameterCount() == 0) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation should only be used on methods with parameters: " +
                                method);
                    }
                }
                boolean required = determineRequiredStatus(ann);
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                currElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });
        elements.addAll(0, currElements);
        targetClass = targetClass.getSuperclass();
    }
    while (targetClass != null && targetClass != Object.class);
    return new InjectionMetadata(clazz, elements);
}

beanClass和注入点集合会被封装成InjectionMetadata对象,代表注入元数据。

4. 依赖注入

依赖注入是通过后置处理器的一个扩展点来实现的,扩展点是InstantiationAwareBeanPostProcessorAdapter#postProcessProperties(),AutowiredAnnotationBeanPostProcessor实现也很简单,因为前面已经完成了注入点的收集,所以这里仅仅是从缓存里取出InjectionMetadata对象,然后完成注入。

@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    // 查找依赖元数据 前面收集扩展点时已经缓存起来了
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    try {
        // 依赖注入
        metadata.inject(bean, beanName, pvs);
    } catch (BeanCreationException ex) {
        throw ex;
    } catch (Throwable ex) {
        throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
    }
    return pvs;
}

依赖注入的职责交给了InjectionMetadata#inject(),它会遍历所有的注入点,然后依次完成注入。

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    Collection<InjectedElement> checkedElements = this.checkedElements;
    // 优先checkedElements,其次injectedElements
    Collection<InjectedElement> elementsToIterate =
            (checkedElements != null ? checkedElements : this.injectedElements);
    if (!elementsToIterate.isEmpty()) {
        for (InjectedElement element : elementsToIterate) {
            /**
             * 分为属性注入和Setter方法注入 两种
             * @see AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject
             * @see AutowiredAnnotationBeanPostProcessor.AutowiredMethodElement#inject
             */
            element.inject(target, beanName, pvs);
        }
    }
}

依赖注入的过程由具体的子类去实现,我们以Field注入为例,对应的子类是AutowiredFieldElement。

@Override
    protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
        Field field = (Field) this.member;
        Object value;
        // 查找依赖的bean是有代价的,这里会缓存起来
        if (this.cached) {
            value = resolvedCachedArgument(beanName, this.cachedFieldValue);
        } else {
            // 创建依赖描述符
            DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
            desc.setContainingClass(bean.getClass());
            Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
            Assert.state(beanFactory != null, "No BeanFactory available");
            TypeConverter typeConverter = beanFactory.getTypeConverter();
            try {
                // 容器查找依赖的bean
                value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
            } catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
            }
            synchronized (this) {
                if (!this.cached) {// 加入缓存
                    if (value != null || this.required) {
                        this.cachedFieldValue = desc;
                        registerDependentBeans(beanName, autowiredBeanNames);
                        if (autowiredBeanNames.size() == 1) {
                            String autowiredBeanName = autowiredBeanNames.iterator().next();
                            if (beanFactory.containsBean(autowiredBeanName) &&
                                    beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
                                this.cachedFieldValue = new ShortcutDependencyDescriptor(
                                        desc, autowiredBeanName, field.getType());
                            }
                        }
                    } else {
                        this.cachedFieldValue = null;
                    }
                    this.cached = true;
                }
            }
        }
        if (value != null) {
            // 通过反射设置属性
            ReflectionUtils.makeAccessible(field);
            field.set(bean, value);
        }
    }
}
  1. 依赖注入的第一步,就是从容器里查找出要依赖的bean,这个查找过程比较复杂,且是有代价的,所以Spring会将查找到的bean缓存起来,后续再注入时直接取缓存即可。
  2. 如果缓存没有,则创建依赖描述符对象DependencyDescriptor,调用DefaultListableBeanFactory#resolveDependency()向容器查找依赖bean。
  3. 最终,通过反射给Field赋值。

5. 查找依赖bean

查找依赖bean的方法是DefaultListableBeanFactory#resolveDependency(),除了最简单的依赖注入,Spring还做了一些扩展,例如可以支持注入Optional、ObjectFactory等对象,你甚至可以配合@Lazy注解一起使用,注入一个延迟加载的bean。对于这种非常规注入,Spring要特殊处理。

public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
                                @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    /**
     * 方法参数名作为beanName注入,所以需要参数名称解析器
     * Spring支持多种获取方法参数名的策略
     */
    descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
    if (Optional.class == descriptor.getDependencyType()) {
        // 支持Optional注入
        return createOptionalDependency(descriptor, requestingBeanName);
    } else if (ObjectFactory.class == descriptor.getDependencyType() ||
            ObjectProvider.class == descriptor.getDependencyType()) {
        return new DependencyObjectProvider(descriptor, requestingBeanName);
    } else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
        return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
    } else {
        // 如果注入的属性加了@Lazy注解 创建代理对象
        Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(descriptor, requestingBeanName);
        if (result == null) {
            // 否则容器查找依赖的bean
            result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
        }
        return result;
    }
}

对于常规注入,方法是DefaultListableBeanFactory#doResolveDependency()

@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
                                  @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

    InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
    try {
        // 尝试快捷注入,前提是已经确定注入的beanName
        Object shortcut = descriptor.resolveShortcut(this);
        if (shortcut != null) {
            return shortcut;
        }
        // 依赖类型
        Class<?> type = descriptor.getDependencyType();
        // 解析@Value注解 取出value属性
        Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
        if (value != null) {
            if (value instanceof String) {
                String strVal = resolveEmbeddedValue((String) value);
                BeanDefinition bd = (beanName != null && containsBean(beanName) ?
                        getMergedBeanDefinition(beanName) : null);
                value = evaluateBeanDefinitionString(strVal, bd);
            }
            TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
            try {
                return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
            } catch (UnsupportedOperationException ex) {
                // A custom TypeConverter which does not support TypeDescriptor resolution...
                return (descriptor.getField() != null ?
                        converter.convertIfNecessary(value, type, descriptor.getField()) :
                        converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
            }
        }
        /**
         * 如果注入的是 数组、Collection、Map
         * 则要返回多个bean
         */
        Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
        if (multipleBeans != null) {
            return multipleBeans;
        }
        /**
         * 容器内查找所有的候选bean
         * beanName -> bean
         */
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
        if (matchingBeans.isEmpty()) {
            if (isRequired(descriptor)) {
                // 没有候选bean,且是必须的 则抛出异常
                raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
            }
            return null;
        }

        // 确定注入的beanName
        String autowiredBeanName;
        // 确定注入的bean实例
        Object instanceCandidate;
        if (matchingBeans.size() > 1) {
            // 存在多个候选bean,决定注入的beanName 3个策略
            autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
            if (autowiredBeanName == null) {
                if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
                    return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
                } else {
                    return null;
                }
            }
            // 根据beanName获取bean
            instanceCandidate = matchingBeans.get(autowiredBeanName);
        } else {
            // 只有单一候选bean的处理
            Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
            autowiredBeanName = entry.getKey();
            instanceCandidate = entry.getValue();
        }

        if (autowiredBeanNames != null) {
            autowiredBeanNames.add(autowiredBeanName);
        }
        if (instanceCandidate instanceof Class) {
            instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
        }
        Object result = instanceCandidate;
        if (result instanceof NullBean) {
            if (isRequired(descriptor)) {
                raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
            }
            result = null;
        }
        if (!ClassUtils.isAssignableValue(type, result)) {
            throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
        }
        return result;
    } finally {
        ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
    }
}
  1. Spring首先会尝试快捷注入,快捷注入的前提条件是已经确定注入的beanName,直接通过容器取出bean即可。
  2. 其次,Spring判断要注入的是不是一个bean集合,例如List、Map等。如果注入的是集合,Spring会把容器内所有候选bean都取出来,然后构建对应的集合对象返回。
  3. 接下来就是注入单一bean实例了,Spring先通过findAutowireCandidates()找出所有的候选bean构建一个Map,如果没有候选bean且required=true,则只能抛出异常。
  4. 如果只有一个候选bean,则直接注入即可。如果存在多个候选bean,则通过determineAutowireCandidate()来决定最终注入的beanName。

6. 候选bean筛选

如果容器内有多个候选bean,Spring该注入哪一个呢???
筛选最终注入的beanName方法是DefaultListableBeanFactory#determineAutowireCandidate()

protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
    Class<?> requiredType = descriptor.getDependencyType();
    // 查找@Primary bean
    String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
    if (primaryCandidate != null) {
        return primaryCandidate;
    }
    // OrderComparator#getPriority() 优先级
    String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);
    if (priorityCandidate != null) {
        return priorityCandidate;
    }
    // 兜底方案 根据属性名/方法名 匹配beanName
    for (Map.Entry<String, Object> entry : candidates.entrySet()) {
        String candidateName = entry.getKey();
        Object beanInstance = entry.getValue();
        if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
                matchesBeanName(candidateName, descriptor.getDependencyName())) {
            return candidateName;
        }
    }
    return null;
}
  1. Spring优先查找加了@Primary注解的bean,作为首选注入。
  2. 其次通过OrderComparator#getPriority()获取bean的优先级。
  3. 如果还是没有高优先级的bean,Spring还有一个兜底方案,尝试通过属性名/方法名去匹配beanName。

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

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

相关文章

Leetcode:617. 合并二叉树(C++)

目录 问题描述&#xff1a; 实现代码与解析&#xff1a; 递归&#xff1a; 原理思路&#xff1a; 迭代&#xff1a; 原理思路&#xff1a; 问题描述&#xff1a; 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中一棵覆盖到另一棵之上时&am…

leetcode 399. 除法求值-java题解

题目所属分类 flod最短路算法 原题链接 给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件&#xff0c;其中 equations[i] [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。 另有一些以数组 queri…

编译metabase

Linux Centos7 配置Metabase编译打包环境 安装Oracle JDK1.8&#xff08;如果已经安装&#xff0c;则可以省略此步骤&#xff0c;必须是Oracle JDK&#xff09; 在线下载Oracle JDK 1.8 将下载好的tar包放入linux目录下 2、解压tar进行安装 tar -zxvf jdk-8u212-linux-x64.t…

SSL/TLS协议信息泄露漏洞(CVE-2016-2183)

最近服务器扫描出SSL/TLS协议信息泄露漏洞(CVE-2016-2183) TLS是安全传输层协议&#xff0c;用于在两个通信应用程序之间提供保密性和数据完整性。 TLS, SSH, IPSec协商及其他产品中使用的DES及Triple DES密码存在大约四十亿块的生日界&#xff0c;这可使远程攻击者通过Sweet…

总结几个常用的Git命令的使用方法

目录 1、Git的使用越来越广泛 2、设置Git的用户名和密码并查看 3、建立自己的 Git 仓库 4、将自己的代码提交到远程 (origin) 仓库 5、同步远程仓库的更新到本地仓库 6、分支管理 7、获取远程仓库的内容 1、Git的使用越来越广泛 现在很多的公司或者机构都在使用Git进行项目和代…

Elasticsearch基础1——搜索引擎发展史和工作流程、es\es-head\kibana的基础安装

文章目录一、搜索引擎1.1 搜索引擎的发展背景1.2 Lucene和Elasticsearch1.3 Solr和Elasticsearch对比1.4 数据搜索方式1.5 搜索引擎1.5.1 搜索引擎工作流程1.5.2 网络爬虫原理流程1.5.3 网页分析1.5.4 正排索引和倒排索引二、Elasticsearch基础安装1.2 概述简介2.2 安装2.2.1 W…

tensorflow算子注册以及op详解

在自定义的算子时&#xff0c;经常遇到一些函数和宏&#xff0c;这里介绍一下常见的函数和宏 REGISTER_OP 首先我们来思考REGISTER_OP的作用是什么&#xff1f;当我们定义一个tensorflow的算子&#xff0c;首先我们需要tensorflow知道这个算子&#xff0c;也就是说我们要把这…

WeLink的使用

我这里是注册的企业端 流程>手机号验证码 注册成功后登陆 进入首页面 按操作逐步完成信息需求 因个体使用情况不同 在角色分类和组织架构中可根据自己部门或单位的分工分类 【拉人】&#xff1a; 三种方式 主要就是网址超链接和企业码 前提需要用户先注册 【加入审核】是根…

Nginx——反向代理解决跨域问题(Windows)

这个破玩意是真麻烦&#xff0c;必须写一篇文章避避坑了。一、先看看大佬的解释&#xff0c;了解反向代理和跨域问题吧&#xff1a;Nginx反向代理什么是跨域问题二、OK&#xff0c;直接开工&#xff0c;装Nginx下载地址: http://nginx.org/en/download.html如图所示, 选择相应的…

Flink多流转换(Flink Stream Unoin、Flink Stream Connect、Flink Stream Window Join)

文章目录多流转换1、分流操作1.1、在flink 1.13版本中已弃用.split()进行分流1.2、使用&#xff08;process function&#xff09;的侧输出流&#xff08;side output&#xff09;进行分流2、基本合流操作2.1、联合&#xff08;Flink Stream Union&#xff09;2.2、连接&#x…

【Go】实操使用go连接clickhouse

前言 近段时间业务在一个局点测试clickhouse&#xff0c;用java写的代码在环境上一直连接不上clickhouse服务&#xff0c;报错信息也比较奇怪&#xff0c;No client available&#xff0c;研发查了一段时间没查出来&#xff0c;让运维这边继续查&#xff1a; 运维同学查了各种…

OAuth 2.0授权框架详解

简介 在现代的网站中&#xff0c;我们经常会遇到使用OAuth授权的情况&#xff0c;比如有一个比较小众的网站&#xff0c;需要用户登录&#xff0c;但是直接让用户注册就显得非常麻烦&#xff0c;用户可能因为这个原因而流失&#xff0c;那么该网站可以使用OAuth授权&#xff0…

FactoryBean和BeanFactory的区别

1. 前言 “BeanFactory和FactoryBean的区别是什么&#xff1f;&#xff1f;&#xff1f;” 这是Spring非常高频的一道面试题&#xff0c;BeanFactory是Spring bean容器的顶级接口&#xff0c;负责创建和维护容器内所有的bean对象。而FactoryBean是用来创建一类bean的接口&…

数字新基建之数据云

自2021年“新基建”概念火爆以来&#xff0c;相关的政策和技术都不断跟进和发展&#xff0c;由于“新基建”本质上是基础设施向数字化、智能化、网络化方向发展&#xff0c;因此更多的科技领域从业者和投资者都将其称为“数字新基建”。而数据库、数据仓库、大数据平台和数据云…

C语言:整数的存储方式

整数的存储方式 char类型在存储时是按照ASCII码值进行存储&#xff0c;存储方式与整型一致 有符号数与无符号数 char一个字节signed charunsigned char int四个字节signed intunsigned int 各种类型数据均分为有符号和无符号类型&#xff0c;当定义一个int类型或char类型的数…

备库为什么会延迟好几个小时?

在上一篇文章中,我和你介绍了几种可能导致备库延迟的原因。你会发现,这些场景里,不论是偶发性的查询压力,还是备份,对备库延迟的影响一般是分钟级的,而且在备库恢复正常以后都能够追上来。 但是,如果备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能…

百度搜索留痕推广资源整理如何收录排名的?

每日分享&#xff1a;百度对图文类内容的优质标准 &#xff08;1&#xff09;文字的字体、字号与间距需要适配网页&#xff0c;文档分段合理&#xff0c;结构有序&#xff0c;阅读体验舒适。 &#xff08;2&#xff09;在文章中使用小标题准确概括段意&#xff0c;通过加粗、…

vue3 setup语法糖父子组件传值,让女友看得明明白白

前言 最近在想做个cloud项目,gitee上找了个模板项目&#xff0c;前端使用到vue3 typeScript&#xff0c;最近使用到vue3 的父子组件之间的传值&#xff0c;顺便学习一下&#xff0c;在此总结一下&#xff0c;若有不足之处&#xff0c;望大佬们可以指出。 vue3官网&#xff1a…

栈--专题讲解

文章目录基本概念模拟栈数据结构-栈&#xff1a;stack头文件定义基本操作实例&#xff1a;火车进栈题目大意解题思路AC代码基本概念 栈的定义 栈(stack)是限定仅在表尾进行插入或者删除的线性表。对于栈来说&#xff0c;表尾端称为栈顶&#xff08;top&#xff09;&#xff0c…

web服务器----基于http协议搭建的静态网站详解

一&#xff0c;WWW的简介 1、什么是 www www 是 world wide web 的缩写&#xff0c;也就是全球信息广播的意思。通常说的上网就是使用 www 来查询用户所需要的信息。www 可以结合文字、图形、影像以及声音等多媒体&#xff0c;并通过可以让鼠标单击超链接的方式将信息以 Inter…