手写spring13(xml自动扫描bean注册)

news2024/11/21 1:45:00

文章目录

  • 目标
    • 包扫描
    • 注解配置的使用
    • 占位符属性的填充
  • 设计
  • 类结构
  • 一、实现
    • 1、处理占位符配置——PropertyPlaceholderConfigurer
    • 2、定义@Scope、@Component拦截注解
    • 3、处理对象扫描装配——ClassPathBeanDefinitionScanner
    • 4、解析xml中调用扫描
  • 二、测试
    • 1、准备
    • 2、属性配置文件
    • 3、pring.xml 配置对象
    • 4、单元测试(占位符)
    • 5、单元测试(包扫描)
  • 总结


目标

目前的手写源码是通过xml的方法,扫描、注册;

本章目标是 包的扫描注册、注解配置的使用、占位符属性的填充等,更加自动化

本章要实现的效果

包扫描

通过xml文件里,添加context:component-scan这个标签,来指定扫描包路径

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <context:component-scan base-package="springframework.test.bean" />
</beans>

注解配置的使用

添加@Component注解,如果在指定包下,则会被扫描到并且注册到BeanDefinition里,最终getBean

@Component("userService")
public class UserService implements IUserService {
	private String token;
}

占位符属性的填充

${token}解析配置文件的数据,并且进行属性填充

<bean id="userService" class="springframework.test.bean.UserService">
        <property name="token" value="${token}" />
</bean>

设计

首先我们要考虑,为了可以简化 Bean 对象的配置,让整个 Bean 对象的注册都是自动扫描的,那么需要的元素包括:扫描路径入口、XML解析扫描信息、给需要扫描的Bean对象做注解标记、扫描Class对象摘取Bean注册的基本信息,组装注册信息、注册成Bean对象
那么在这些条件元素的支撑下,就可以实现出通过自定义注解和配置扫描路径的情况下,完成 Bean 对象的注册。

解决一个配置中占位符属性的知识点,比如可以通过 ${token} 给 Bean 对象注入进去属性信息,那么这个操作需要用到 BeanFactoryPostProcessor,因为它可以处理 在所有的 BeanDefinition 加载完成后,实例化 Bean 对象之前,提供修改 BeanDefinition 属性的机制。

整体设计结构如下图:

在这里插入图片描述
结合bean的生命周期,包扫描只不过是扫描特定注解的类,提取类的相关信息组装成BeanDefinition注册到容器中

1、在XmlBeanDefinitionReader中解析<context:component-scan />标签,扫描类组装BeanDefinition然后注册到容器中的操作在ClassPathBeanDefinitionScanner#doScan中实现。

自动扫描注册主要是扫描添加了自定义注解的类,在xml加载过程中提取类的信息,组装 BeanDefinition 注册到 Spring 容器中。
所以我们会用到 <context:component-scan /> 配置包路径并在 XmlBeanDefinitionReader 解析并做相应的处理。这里的处理会包括对类的扫描、获取注解信息等

2、因为我们需要完成对占位符配置信息的加载,所以需要使用到 BeanFactoryPostProcessor 在所有的 BeanDefinition 加载完成后,实例化 Bean 对象之前,修改 BeanDefinition 的属性信息


类结构

在这里插入图片描述
1、整个类的关系结构来看,其实涉及的内容并不多,主要包括的就是 xml 解析类 XmlBeanDefinitionReader 对 ClassPathBeanDefinitionScanner#doScan 的使用。

2、在 doScan 方法中处理所有指定路径下添加了注解的类,拆解出类的信息:名称、作用范围等,进行创建 BeanDefinition 好用于 Bean 对象的注册操作。

3、PropertyPlaceholderConfigurer 目前看上去像一块单独的内容,后续会把这块的内容与自动加载 Bean 对象进行整合,也就是可以在注解上使用占位符配置一些在配置文件里的属性信息


一、实现

1、处理占位符配置——PropertyPlaceholderConfigurer

/**
 * @desc 处理占位符配置
 */
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {

    /**
     * Default placeholder prefix: {@value}
     */
    public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";

    /**
     * Default placeholder suffix: {@value}
     */
    public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";


    // 资源文件位置
    private String location;


    /**
     * @desc: 占位符属性配置解析,通过BeanFactoryPostProcessor修改beanDefintion属性
     **/
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        try {
            // 加载属性文件
            DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource resource = resourceLoader.getResource(location);
            Properties properties = new Properties();
            properties.load(resource.getInputStream());

            String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
            for (String beanName : beanDefinitionNames) {
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);

                PropertyValues propertyValues = beanDefinition.getPropertyValues();
                for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {
                    Object value = propertyValue.getValue();
                    if (!(value instanceof String)){
                        continue;
                    }
                    String strVal = (String) value;
                    StringBuilder buffer = new StringBuilder(strVal);
                    // 获取定位符的内容
                    int startIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_PREFIX);
                    int stopIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);
                    if(startIdx != -1 && stopIdx != -1 && startIdx < stopIdx){
                        String propKey = strVal.substring(startIdx + 2, stopIdx);
                        String propVal = properties.getProperty(propKey);
                        buffer.replace(startIdx,stopIdx+1,propVal);
                        propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(),buffer.toString()));
                    }

                }
            }

        } catch (IOException e) {
            throw new BeansException("Could not load properties", e);
        }
    }


    // 设置文件资源路径
    public void setLocation(String location) {
        this.location = location;
    }
}

1、依赖于 BeanFactoryPostProcessor 在 Bean 生命周期的属性,可以在 Bean 对象实例化之前,改变属性信息
所以这里通过实现 BeanFactoryPostProcessor 接口,完成对配置文件的加载以及摘取占位符中的在属性文件里的配置

2、通过截取${}里面的内容,然后通过properties.getProperty,获取到配置文件里面的值

3、这样就可以把提取到的配置信息放置到属性配置中了,

buffer.replace(startIdx,stopIdx+1,propVal);
propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(),buffer.toString()));

2、定义@Scope、@Component拦截注解

/**
 * @desc: 作用域注解
 **/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

    String value() default "singleton";

}

用于配置作用域的自定义注解,方便通过配置Bean对象注解的时候,拿到Bean对象的作用域。不过一般都使用默认的 singleton

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {

    String value() default "";

}

Component 自定义注解大家都非常熟悉了,用于配置到 Class 类上的。除此之外还有 Service、Controller,不过所有的处理方式基本一致,这里就只展示一个 Component 即可。


3、处理对象扫描装配——ClassPathBeanDefinitionScanner

/**
 * @desc 处理对象扫描装配
 */
public class ClassPathScanningCandidateComponentProvider {

    public Set<BeanDefinition> findCandidateComponents(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        // 扫描指定包路径下所有包含指定注解的类
        Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(basePackage, Component.class);
        for (Class<?> clazz : classes) {
            candidates.add(new BeanDefinition(clazz));
        }
        return candidates;
    }
}

这里先要提供一个可以通过配置路径 basePackage=cn.ljc.springframework.test.bean,解析出 classes 信息的工具方法 findCandidateComponents,通过这个方法就可以扫描到所有 @Component 注解的 Bean 对象了。

/**
 * @desc bean定义扫描器
 */
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {


    // 注册Bean定义
    private BeanDefinitionRegistry registry;

    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
        this.registry = registry;
    }

    /**
     * @desc: 扫描包
     **/
    public void doScan(String... basePackages) {
        for (String basePackage : basePackages) {
        	// 获取所有的@component注解的bean定义
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            for (BeanDefinition beanDefinition : candidates) {
                // 解析Bean的作用域
                String beanScope = resolveBeanScope(beanDefinition);
                if (StrUtil.isNotEmpty(beanScope)) {
                    beanDefinition.setScope(beanScope);
                }
                // 注册bean定义
                registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);
            }
        }

    }


    /**
     * @desc: 获取bean的作用域
     **/
    private String resolveBeanScope(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        Scope scope = beanClass.getAnnotation(Scope.class);
        if (scope != null) {
            return scope.value();
        }
        return StrUtil.EMPTY;
    }


    /**
     * @desc: 确定bean名称
     **/
    private String determineBeanName(BeanDefinition beanDefinition) {
        Class<?> beanClass = beanDefinition.getBeanClass();
        Component component = beanClass.getAnnotation(Component.class);
        String value = component.value();
        if (StrUtil.isEmpty(value)) {
            // 小写首字母
            value = StrUtil.lowerFirst(beanClass.getSimpleName());
        }
        return value;
    }

}

ClassPathBeanDefinitionScanner继承自 ClassPathScanningCandidateComponentProvider 的具体扫描包处理的类,在 doScan 中除了获取到扫描的类信息以后,还需要获取 Bean 的作用域和类名,如果不配置类名基本都是把首字母缩写。


4、解析xml中调用扫描

/**
 * 解析XML处理Bean注册
 */
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
    }

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        super(registry, resourceLoader);
    }


    @Override
    public void loadBeanDefinitions(Resource resource) throws BeansException {
        try {
            InputStream inputStream = resource.getInputStream();
            doLoadBeanDefinitions(inputStream);
        } catch (Exception e) {
            throw new BeansException("IOException parsing XML document from " + resource, e);
        }
    }

    @Override
    public void loadBeanDefinitions(Resource... resources) throws BeansException {
        for (Resource resource : resources) {
            loadBeanDefinitions(resource);
        }
    }

    @Override
    public void loadBeanDefinitions(String location) throws BeansException {
        ResourceLoader resourceLoader = getResourceLoader();
        Resource resource = resourceLoader.getResource(location);
        loadBeanDefinitions(resource);
    }

    @Override
    public void loadBeanDefinitions(String... locations) throws BeansException {
        for (String location : locations) {
            loadBeanDefinitions(location);
        }
    }


    protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException, DocumentException {

        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        Element root = document.getRootElement();

        // 解析xml文件中的context:component-scan 标签,扫描包中的类并提取相关信息,用于组装 BeanDefinition
        Element componentScan = root.element("component-scan");
        if (componentScan != null) {
        	// 获取指定的包路径
            String scanPath = componentScan.attributeValue("base-package");
            if (StrUtil.isEmpty(scanPath)) {
                throw new BeansException("The value of base-package attribute can not be empty or null");
            }
            // 扫描包,把component注册
            scanPackage(scanPath);
        }

		// ....解析xml每个标签的数据,并填充到beanDefintion(之前的代码这里就不显示了,这里只展示本章新增内容)
		
            // 注册 BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }



    /**
     * @desc: 扫描包
     **/
    private void scanPackage(String scanPath) {
        String[] basePackages  = StrUtil.splitToArray(scanPath, ',');
        ClassPathBeanDefinitionScanner scanner  = new ClassPathBeanDefinitionScanner(getRegistry());
        scanner.doScan(basePackages);
    }
}

关于 XmlBeanDefinitionReader 中主要是在加载配置文件后,处理新增的自定义配置属性 component-scan,解析后调用 scanPackage 方法,其实也就是我们在 ClassPathBeanDefinitionScanner#doScan 功能。

另外这里需要注意,为了可以方便的加载和解析xml,XmlBeanDefinitionReader 已经全部替换为 dom4j 的方式进行解析处理。


二、测试

1、准备

@Component("userService")
public class UserService implements IUserService {

    private String token;


    public String queryUserInfo() {
        try {
            Thread.sleep(new Random(1).nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "ljc,100001,上海";
    }

    public String register(String userName) {
        try {
            Thread.sleep(new Random(1).nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "注册用户:" + userName + " success!";
    }

    @Override
    public String toString() {
        return "UserService#token = { " + token + " }";
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

给 UserService 类添加一个自定义注解 @Component(“userService”)一个属性信息 String token。这是为了分别测试包扫描和占位符属性。


2、属性配置文件

token.properties

token=RejDlI78hu223Opo983Ds

这里配置一个 token 的属性信息,用于通过占位符的方式进行获取


3、pring.xml 配置对象

spring-property.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	         http://www.springframework.org/schema/beans/spring-beans.xsd
		 http://www.springframework.org/schema/context">

    <bean class="cn.ljc.springframework.beans.factory.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:token.properties"/>
    </bean>

    <bean id="userService" class="springframework.test.bean.UserService">
        <property name="token" value="${token}" />
    </bean>


</beans>

加载 classpath:token.properties 设置占位符属性值 ${token}

spring-scan.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	         http://www.springframework.org/schema/beans/spring-beans.xsd
		 http://www.springframework.org/schema/context">

    <context:component-scan base-package="springframework.test.bean" />

</beans>

添加 component-scan 属性,设置包扫描根路径,用于获取指定包路径下的@component注解的bean


4、单元测试(占位符)

@Test
    public void test_property() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-property.xml");
        IUserService userService = applicationContext.getBean("userService", IUserService.class);
        System.out.println("测试结果:" + userService);
    }

测试结果

测试结果:UserService#token = { RejDlI78hu223Opo983Ds }

通过测试结果可以看到 UserService 中的 token 属性,已经通过占位符的方式,设置进去配置文件里的 token.properties 的属性值了。


5、单元测试(包扫描)

@Test
    public void test_scan() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-scan.xml");
        IUserService userService = applicationContext.getBean("userService", IUserService.class);
        System.out.println("测试结果:" + userService.queryUserInfo());
    }

测试结果

测试结果:ljc,100001,上海

通过这个测试结果可以看出来,现在使用注解的方式就可以让 Class 注册完成 Bean 对象了。


总结

占位符的处理,通过上面代码可以知道,BeanFactoryPostProcessor的扩展,spring内部就是通过这个BeanFactoryPostProcessor来实现的。

包扫描则是则是获取到指定的xml标签,获取包路径,然后通过doScan扫描到包下指定的**@component@scope**,然后注册到BeanDefintion

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

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

相关文章

网络原理笔记一

文章目录1、局域网&#xff08;LAN&#xff09;2、广域网&#xff08;WAN&#xff09;3、网络通信基础1、IP地址2、端口号3、认识协议4、协议分层1、网络模型2、网络数据传输的基本流程1、局域网&#xff08;LAN&#xff09; 局域网全称&#xff1a;Local Area Network&#x…

【数据结构】顺序表深度剖析

目录 &#x1f6eb;前言&#x1f6eb;&#xff1a; &#x1f680;一、线性表概述&#x1f680;&#xff1a; &#x1f6f0;️二、顺序表&#x1f6f0;️&#xff1a; 1.概念及结构&#xff1a; 2.接口实现&#xff1a; ①.工程文件&#xff1a; ②.接口实现&#xff1a; ③.…

2023 铁人三项 CTF --- Misc wp

文章目录Misc一生壹世Misc手的必备技能套娃-1套娃-2套娃-3套娃-4套娃-5Misc 一生壹世 根据提示压缩包密码是谐音&#xff0c;所以猜测密码是1314&#xff0c;直接解压得到四个txt文件 然后用010分按照一生一世打开4个文件&#xff0c;发现每个文件的第一个字节拼起来是8950…

LeetCode题目笔记——1807. 替换字符串中的括号内容

文章目录题目描述题目难度——中等方法一&#xff1a;使用字典代码/Python代码/C总结题目描述 给你一个字符串 s &#xff0c;它包含一些括号对&#xff0c;每个括号中包含一个 非空 的键。 比方说&#xff0c;字符串 “(name)is(age)yearsold” 中&#xff0c;有 两个 括号对…

什么是聚宽量化?

聚宽量化交易其实是通过编程将策略写成计算机可识别的代码&#xff0c;具体说可以是以python这门编程语言将与券商那边的平台搭建&#xff0c;例如用聚宽的向导式策略生成器就能很快的将股票交易接口翻译出来的策略&#xff0c;以便将平时需要查询的融资账户的持仓情况来编写代…

区块链将传统的产业运行逻辑,改造成以个体为主导的运行逻辑

新的产业之所以被成为新产业&#xff0c;其中一个很重要的原因在于&#xff0c;它的底层运行逻辑&#xff0c;正在发生一场深刻而又彻底的改变。然而&#xff0c;如果深入分析这种能够给产业的内在运行逻辑产生重塑和改变的&#xff0c;依然在于区块链本身。从某种意义上来讲&a…

YOLOv8 深度详解!一文看懂,快速上手

YOLOv8 是 ultralytics 公司在 2023 年 1月 10 号开源的 YOLOv5 的下一个重大更新版本&#xff0c;目前支持图像分类、物体检测和实例分割任务&#xff0c;在还没有开源时就收到了用户的广泛关注。 考虑到 YOLOv8 的优异性能&#xff0c;MMYOLO 也在第一时间组织了复现&#xf…

一篇文章搞定字符串的旋转问题(数组旋转问题)!

目录 前言 一.如何实现字符串的旋转&#xff1f; 1.用冒泡的思想实现 2.分别逆序左右再整体逆序来实现 二.如何快速判断一个字符串是否为另外一个字符串旋转之后的字符串&#xff1f; 1.相关库函数的简略学习 2.利用库函数快速实现判断 总结 博客主页&#xff1a;张栩睿…

数据结构基础知识(一)

1 数据结构的基本概念和算法 考点 数据结构基本概念算法 1.1 数据结构的基本概念 数据 数据是指所有能输入到计算机中的描述客观事物的符号&#xff0c;包括文本、声音、 图像、符号等。 数据元素 数据元素十数据的基本单位&#xff0c;也称节点或记录。 数据项 数据项…

【数仓】FLink+CK

1.项目分层 ODS&#xff1a;原始数据&#xff0c;包含日志和业务数据DWD&#xff1a;根据数据对象为单位进行分流。比如订单、页面访问等。DIM&#xff1a;维度数据DWM&#xff1a;对于部分数据对象进行进一步加工&#xff0c;比如独立访问、跳出行为&#xff0c;也可以和维度…

谷粒商城项目笔记之高级篇(三)

目录1.9.22 提交订单的问题1)、订单号显示、应付金额回显2)、提交订单消息回显3&#xff09;、为了确保锁库存失败后&#xff0c;订单和订单项也能回滚&#xff0c;需要抛出异常1.9.23 创建库存上锁、解锁 业务交换机&队列1&#xff09;、流程梳理2&#xff09;、解锁库存实…

Vsftpd服务的部署及优化详解(图文)

目录 前言 实验环境 一、vsftpd的安装及启用 1、具体步骤 2、开启匿名用户访问功能并测试 二、vsftpd基本信息 三、匿名用户访问控制 四、本地用户访问控制 五、虚拟用户访问 1、建立虚拟用户访问 2、虚拟用户家目录的独立设定 3、用户配置独立 前言 vsftpd是“…

TapTap 算法平台的 Serverless 探索之路

分享人&#xff1a;陈欣昊&#xff0c;TapTap/IEM/AI平台负责人 摘要&#xff1a;本文主要介绍心动网络算法平台在Serverless上的实践。 《TapTap算法平台的 Serverless 探索之路》 Serverless 在构建应用上为我们节省了大量的运维与开发人力&#xff0c;在基本没投入基建人力…

(02)Cartographer源码无死角解析-(49) 2D点云扫描匹配→相关性暴力匹配1:SearchParameters

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下: (02)Cartographer源码无死角解析- (00)目录_最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/127350885 文末…

LeetCode分类刷题----链表篇

链表链表1.移除链表元素203.移除链表元素707.设计链表2.反转链表206.反转链表3.两两交换链表中的节点24.两两交换链表中的节点4.删除链表中的倒数第N个节点19.删除链表的倒数第N个节点5.链表相交07.链表相交6.环形链表141.环形链表142.环形链表II链表 1.移除链表元素 203.移除…

成功解决VMware安装操作系统出现分辨率的问题

文章目录问题重现问题原因问题解决方法一&#xff1a;拓展&#xff1a;1. 电脑分辨率&#xff1a;2. xrandr命令3. 查询后如果没有合适的分辨率解决方案参考资料问题重现 如下图&#xff1a; 在VMware16上安装ubuntu操作系统的时候&#xff0c;出现分辨率问题&#xff0c; 导致…

如何录屏有声音?如何录制带声音的视频

平常我们会通过录屏的方式录制电脑画面&#xff0c;然后再保存下来。那您是不是遇到过这种情况&#xff1a;录制的录屏文件只有画面没有声音。没有声音的视频还能修复吗&#xff1f;如何录屏有声音&#xff1f;怎样才能录制带声音的视频&#xff1f;今天小编教大家如何在录屏的…

前端基础(十三)_定时器(间歇定时器、延迟定时器)

定时器 定时器共两种&#xff0c;setInterval及setTimeout&#xff1a; 1、setInterval&#xff1a;重复执行或者叫间歇执行&#xff0c;即隔某个时间就执行一次 2、setTimeout&#xff1a;延迟执行&#xff0c;延迟某个特定的时间开始执行&#xff0c;只执行一次 语法&#x…

代码随想录算法训练营第10天 232.用栈实现队列、225. 用队列实现栈

代码随想录算法训练营第10天 232.用栈实现队列、225. 用队列实现栈 用栈实现队列 力扣题目链接(opens new window) 使用栈实现队列的下列操作&#xff1a; push(x) – 将一个元素放入队列的尾部。 pop() – 从队列首部移除元素。 peek() – 返回队列首部的元素。 empty() –…

十分好用的跨浏览器测试工具,建议收藏!!!

跨浏览器测试是确保web应用程序的功能在不同浏览器、浏览器版本和操作系统直接保持功能和质量一致的过程&#xff0c;可以为用户提供更好的用户体验&#xff0c;帮助企业通过更易访问的网站获得满意客户&#xff0c;可以使web应用程序在不同平台上兼容。在跨浏览器测试过程中&a…