Tomcat源码:启动类Bootstrap与Catalina的加载

news/2024/5/10 4:23:28/文章来源:https://blog.csdn.net/wzngzaixiaomantou/article/details/129696960

参考资料:

《Tomcat源码解析系列(一)Bootstrap》

《Tomcat源码解析系列(二)Catalina》

《Tomcat - 启动过程:初始化和启动流程》

《Tomcat - 启动过程:类加载机制详解》

《Tomcat - 启动过程:Catalina的加载》

相关资料:

《Tomcat:servlet与servlet容器》

        写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。

前言

        本文开始我们将会介绍tomcat的源码,整个流程会按照tomcat的启动顺序来开展,在此之前建议先阅读下前文理清servlet与servlet容器之间的关系。

        tomcat的源码可以在管网下载:传送门

        

          在tomcat的bin目录下有两个启动tomcat的文件, 一个是startup.bat, 它用于windows环境下启动tomcat; 另一个是startup.sh, 它用于linux环境下tomcat的启动. 两个文件中的逻辑是一样的, 我们只分析其中的startup.bat。而startup.bat文件实际上就做了一件事情: 启动catalina.bat。

        catalina.bat中下面这段指定了tomcat的启动类为Bootstrap这个类,catalina.bat最终执行了Bootstrap类中的main方法来启动tomcat。(catalina.bat的详细内容可以参考这篇文章《详解Tomcat系列(一)-从源码分析Tomcat的启动》,本文就不做深入解析了)

set _EXECJAVA=%_RUNJAVA%
set MAINCLASS=org.apache.catalina.startup.Bootstrap
set ACTION=start

目录

一、启动类Bootstrap

        1、main

        2、类加载器的初始化

        2.1、commonLoader

        2.2、catalinaLoader与sharedLoader 

        3、init

        4、load与start

二、Catalina初始化

        1、load

        1.1、第一段

        1.2、第三段

         2、start

补充

        tomcat的类加载器模式


一、启动类Bootstrap

        首先来看下整个启动过程,我们可以看到Bootstrap作为启动入口首先进行了初始化方法init然后load方法加载了Catalina,下面我们就开始介绍Bootstrap。

        1、main

        Bootstrap的main方法首先会创建一个 Bootstrap 对象,调用它的 init 方法初始化,然后根据启动参数,调用Bootstrap对象的不同方法,默认模式为start,该模式下将会先后调用load与start方法。

    private static final Object daemonLock = new Object();private static volatile Bootstrap daemon = null;public static void main(String args[]) {// 创建一个 Bootstrap 对象synchronized (daemonLock) {if (daemon == null) {Bootstrap bootstrap = new Bootstrap();try {// 调用init方法初始化bootstrap.init();} catch (Throwable t) {handleThrowable(t);t.printStackTrace();return;}daemon = bootstrap;} else {Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);}}// 根据启动参数,分别调用 Bootstrap 对象的不同方法try {// 默认参数为startString command = "start"; if (args.length > 0) {command = args[args.length - 1];}if (command.equals("startd")) {...} else if (command.equals("stopd")) {...} else if (command.equals("start")) {daemon.setAwait(true);daemon.load(args);daemon.start();if (null == daemon.getServer()) {System.exit(1);}} else if (command.equals("stop")) {...} else if (command.equals("configtest")) {...                    } else {log.warn("Bootstrap: command \"" + command + "\" does not exist.");}} catch (Throwable t) {if (t instanceof InvocationTargetException &&t.getCause() != null) {t = t.getCause();}handleThrowable(t);t.printStackTrace();System.exit(1);}}
}

        2、类加载器的初始化

        2.1、commonLoader

        init方法内部首先调用了initClassLoaders方法,看名字可以得得知这里将要进行的便是类加载器的初始化,类加载器的内容在我的《Java8之类的加载》、《Java8之类加载机制class源码分析》中有过详细介绍,这里就不做赘述了,不了解的朋友可以先看完这两篇文章在继续看本文。

public void init() throws Exception {// 初始化classloader(包括catalinaLoader),下文将具体分析initClassLoaders();// 其余代码
}

         而在initClassLoaders中,又调用了createClassLoader进行了类加载器的创建,这里我们关注下传入的参数为common与null,下文将会用到。

ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;private void initClassLoaders() {commonLoader = createClassLoader("common", null);if (commonLoader == null) {commonLoader = this.getClass().getClassLoader();}// 其余代码
}

        来到createClassLoader方法,方法中第一步就是根据传入的参数name从Catalina.Properties文件里找对应的loader,上文传入的参数为common,因此这里寻找的是common.loader。

        我们找到catalina.property文件,其中对应的值如下,可以看到是个路径。

# Note: Values are enclosed in double quotes ("...") in case either the
#       ${catalina.base} path or the ${catalina.home} path contains a comma.
#       Because double quotes are used for quoting, the double quote character
#       may not appear in a path.
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

         获取到common.loader后再将其构造成Repository列表,再将Repository列表传入ClassLoaderFactory.createClassLoader 方法。

private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {// 从CatalinaProperties文件里找common.loader,String value = CatalinaProperties.getProperty(name + ".loader");// 如果name+.loader对应值为空,直接返回父类加载器,后文将会用到if ((value == null) || (value.equals("")))return parent;value = replace(value);List<Repository> repositories = new ArrayList<>();// 构造Repository列表String[] repositoryPaths = getPaths(value);for (String repository : repositoryPaths) {try {@SuppressWarnings("unused")URL url = new URL(repository);repositories.add(new Repository(repository, RepositoryType.URL));continue;} catch (MalformedURLException e) {}if (repository.endsWith("*.jar")) {repository = repository.substring(0, repository.length() - "*.jar".length());repositories.add(new Repository(repository, RepositoryType.GLOB));} else if (repository.endsWith(".jar")) {repositories.add(new Repository(repository, RepositoryType.JAR));} else {repositories.add(new Repository(repository, RepositoryType.DIR));}}// 将Repository列表传入ClassLoaderFactory.createClassLoaderreturn ClassLoaderFactory.createClassLoader(repositories, parent);
}

        ClassLoaderFactory.createClassLoader中将会根据不同的类型采用不同的处理方案,最终创建URLClassLoader对象。

    public static ClassLoader createClassLoader(List<Repository> repositories,final ClassLoader parent)throws Exception {// 路径集合Set<URL> set = new LinkedHashSet<>();if (repositories != null) {// 遍历传入的路径for (Repository repository : repositories)  {// 根据不同的类型采用不同的处理方案,这里仅展示DIR与JAR的处理if (repository.getType() == RepositoryType.URL) {...} else if (repository.getType() == RepositoryType.DIR) {File directory = new File(repository.getLocation());directory = directory.getCanonicalFile();if (!validateFile(directory, RepositoryType.DIR)) {continue;}URL url = buildClassLoaderUrl(directory);if (log.isDebugEnabled()) {log.debug("  Including directory " + url);}set.add(url);} else if (repository.getType() == RepositoryType.JAR) {File file=new File(repository.getLocation());file = file.getCanonicalFile();if (!validateFile(file, RepositoryType.JAR)) {continue;}URL url = buildClassLoaderUrl(file);if (log.isDebugEnabled()) {log.debug("  Including jar file " + url);}set.add(url);} else if (repository.getType() == RepositoryType.GLOB) {...}}}final URL[] array = set.toArray(new URL[0]);// 使用URLClassLoader加载这些路径下的类return AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {@Overridepublic URLClassLoader run() {if (parent == null) {return new URLClassLoader(array);} else {return new URLClassLoader(array, parent);}}});}

        到这里我们就走完了commonLoader的创建过程,其内容为URLClassLoader对象。

        2.2、catalinaLoader与sharedLoader 

        回到initClassLoaders方法,我们继续看catalinaLoader与sharedLoader的初始化,同样是调用createClassLoader方法,但父类加载器参数此时由null变为了commonLoader。

    private void initClassLoaders() {try {commonLoader = createClassLoader("common", null);if (commonLoader == null) {commonLoader = this.getClass().getClassLoader();}catalinaLoader = createClassLoader("server", commonLoader);sharedLoader = createClassLoader("shared", commonLoader);} catch (Throwable t) {handleThrowable(t);log.error("Class loader creation threw exception", t);System.exit(1);}}

        Catalina.Properties中server.loader、shared.loader值为空,结合createClassLoader的内容这里将传入的父类加载器传了回去,因此实际上这三个类加载器都是同一个URLClassLoader对象。

server.loader=
shared.loader=
private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {// 从CatalinaProperties文件里找common.loader,String value = CatalinaProperties.getProperty(name + ".loader");// 如果name+.loader对应值为空,直接返回父类加载器,后文将会用到if ((value == null) || (value.equals("")))return parent;// 其余代码
}

        至此,Bootstrap类中的类加载器初始化完毕。

        3、init

        使用上一步创建的catalinaClassLoader 加载了org.apache.catalina.startup.Catalina类,并创建了一个对象catalinaDaemon,然后通过反射调用这个对象的 setParentClassLoader 方法,传入的参数是 sharedClassLoader。

public void init() throws Exception {// 初始化classloader(包括catalinaLoader),下文将具体分析initClassLoaders();// 设置当前的线程的contextClassLoader为catalinaLoaderThread.currentThread().setContextClassLoader(catalinaLoader);SecurityClassLoad.securityClassLoad(catalinaLoader);// 通过catalinaLoader加载Catalina,并初始化startupInstance 对象Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");Object startupInstance = startupClass.getConstructor().newInstance();// 通过反射调用了setParentClassLoader 方法String methodName = "setParentClassLoader";Class<?> paramTypes[] = new Class[1];paramTypes[0] = Class.forName("java.lang.ClassLoader");Object paramValues[] = new Object[1];paramValues[0] = sharedLoader;Method method =startupInstance.getClass().getMethod(methodName, paramTypes);method.invoke(startupInstance, paramValues);catalinaDaemon = startupInstance;
}

        4、load与start

       load与start都是通过上一步获取到的catalinaDaemon对象反射调用catalina类的load与start方法,这两个过程我们会在后续的catalina内容中介绍。

private void load(String[] arguments) throws Exception {String methodName = "load";Object param[];Class<?> paramTypes[];if (arguments==null || arguments.length==0) {paramTypes = null;param = null;} else {paramTypes = new Class[1];paramTypes[0] = arguments.getClass();param = new Object[1];param[0] = arguments;}Method method =catalinaDaemon.getClass().getMethod(methodName, paramTypes); // 反射调用catalina的load方法method.invoke(catalinaDaemon, param);
}
    public void start() throws Exception {if (catalinaDaemon == null) {init();}Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);method.invoke(catalinaDaemon, (Object [])null);}

二、Catalina初始化

        上文中Bootstrap类的load与start方法实质上就是反射调用catalina类的load与start方法,本节我们来介绍下该类。

        1、load

        load(String[])本质上还是调用了load方法

public void load(String args[]) {try {// 处理命令行的参数if (arguments(args)) { load();}} catch (Exception e) {e.printStackTrace(System.out);}
}

        load() 方法的逻辑挺清晰的,大概可以分为三段,第一个 try-catch 之前算一段,第一个 try-catch 算第二段,第一个 try-catch 到第二个 try-catch 结束算第三段。 

public void load() {// 如果已经加载则退出if (loaded) {return;}loaded = true;long t1 = System.nanoTime();initDirs();initNaming();ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));File file = configFile();// 创建digester对象,用于解析server.xmlDigester digester = createStartDigester();try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) {InputStream inputStream = resource.getInputStream();InputSource inputSource = new InputSource(resource.getURI().toURL().toString());inputSource.setByteStream(inputStream);digester.push(this);// 解析server.xmldigester.parse(inputSource);} catch (Exception e) {// ...}return;}getServer().setCatalina(this);getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());initStreams();// 调用StandardServer.init启动Servertry {getServer().init();} catch (LifecycleException e) {// ...}long t2 = System.nanoTime();}

        1.1、第一段

        首先是initDirs与initNaming,其中initDirs无内容,且备注为“Will be removed in Tomcat 10 onwards”,initNaming方法主要是些额外系统变量的设置,因此都无需关注。

        这一行代码代码的作用是设置Tomcat要加载的配置的配置源,也就是 conf 目录下的 server.xml 文件。

ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));

       接下来这一行创建了一个 Digester对象,这个对象是用来解析 server.xml 文件的

Digester digester = createStartDigester();

        从注释和大部分相似的代码可以看出,Digester 就是用来解析 server.xml 并创建对应的默认实现类对象的。比如碰到 <Server> 标签就创建默认的 StarndardServer 类对象。 

protected Digester createStartDigester() {long t1=System.currentTimeMillis();Digester digester = new Digester();digester.setValidating(false);digester.setRulesValidation(true);……digester.addObjectCreate("Server","org.apache.catalina.core.StandardServer","className");digester.addSetProperties("Server");digester.addSetNext("Server","setServer","org.apache.catalina.Server");digester.addObjectCreate("Server/GlobalNamingResources","org.apache.catalina.deploy.NamingResourcesImpl");digester.addSetProperties("Server/GlobalNamingResources");digester.addSetNext("Server/GlobalNamingResources","setGlobalNamingResources","org.apache.catalina.deploy.NamingResourcesImpl");……return digester;}

        第二段中的内容其实就是调用Digester对象来解析server.xml,内部使用了SAXParser来解析 ,解析完了之后,xml 里定义的各种标签就有对应的实现类对象了。

        1.2、第三段

        第三段中使用了getServer()方法,其获取的是catalina类中的成员变量,其赋值是在 digester.parse解析xml的时候完成的。

protected Server server = null;

         最后调用了server实现类中的init方法,server的内容我们会在后续进行介绍。

        getServer().setCatalina(this);getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());initStreams();try {getServer().init();} catch (LifecycleException e) {// ...}

         2、start

        在 load 方法之后,Tomcat 就初始化了一系列的组件,接着就可以调用 start 方法进行启动了。

    public void start() {if (getServer() == null) {load();}if (getServer() == null) {log.fatal(sm.getString("catalina.noServer"));return;}long t1 = System.nanoTime();try {getServer().start();} catch (LifecycleException e) {// ...return;}long t2 = System.nanoTime();if (useShutdownHook) {if (shutdownHook == null) {shutdownHook = new CatalinaShutdownHook();}Runtime.getRuntime().addShutdownHook(shutdownHook);LogManager logManager = LogManager.getLogManager();if (logManager instanceof ClassLoaderLogManager) {((ClassLoaderLogManager) logManager).setUseShutdownHook(false);}}if (await) {await();stop();}}

         整段代码逻辑还是比较好理解的,核心过程就是getServer().start()调用Server对象的 start() 方法来启动 Tomcat。Server相关的内容我们会在后续进行介绍,本文暂不做讨论。

        这里提一下ShutDownHook关闭钩子,关闭钩子的内容我之前有过介绍,可以看下这篇文章《java8:关闭钩子shutdown hook》,这里实际上是调用 Catalina 对象的 stop 方法来停止 tomcat,而其内部又继续调用了Server对象的stop方法。

        最后就进入 if 语句了,await 是在 Bootstrap 里调用的时候设置为 true 的,也就是本文开头的时候提到的三个方法中的一个。await 方法的作用是停住主线程,等待用户输入shutdown 命令之后,停止等待,之后 main 线程就调用 stop 方法来停止Tomcat。

补充

        tomcat的类加载器模式

        了解类加载器的朋友都知道Java默认的类加载机制是通过双亲委派模型来实现的(不了解的可以看下我这篇文章《Java8之类的加载》、《Java8之类加载机制class源码分析》),不过tomcat却没有使用这种模式,下面我们来简单说明下原因。

        原因在于一个Tomcat容器允许同时运行多个Web程序,每个Web程序依赖的类又必须是相互隔离的。

        如果Tomcat使用双亲委派模式来加载类的话,将导致Web程序依赖的类变为共享的。举个例子,假如我们有两个Web程序,一个依赖A库的1.0版本,另一个依赖A库的2.0版本,他们都使用了类xxx.xx.Clazz,其实现的逻辑因类库版本的不同而结构完全不同。那么这两个Web程序的其中一个必然因为加载的Clazz不是所使用的Clazz而出现问题。

        为了解决这个问题,tomcat采用了如下的设计方式

                       

         最上面的三个类加载器我们已经了解过了,重点在于下面这些,即我们在initClassLoaders方法中创建的三个类加载器,其作用分别如下:

        Common类加载器,负责加载Tomcat和Web应用都复用的类

        (1)Catalina类加载器,负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见

        (2)Shared类加载器,负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见

        (2.1)WebApp类加载器,负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见

        (2.2)Jsp类加载器,每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔

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

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

相关文章

不用科学上网,免费的GPT-4 IDE工具Cursor保姆级使用教程

大家好&#xff0c;我是可乐。 过去的一周&#xff0c;真是疯狂的一周。 GPT-4 震撼发布&#xff0c;拥有了多模态能力&#xff0c;不仅能和GPT3一样进行文字对话&#xff0c;还能读懂图片&#xff1b; 然后斯坦福大学发布 Alpaca 7 B&#xff0c;性能匹敌 GPT-3.5&#xff…

易基因:PIWI/piRNA在人癌症中的表观遗传调控机制(DNA甲基化+m6A+组蛋白修饰)|综述

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。2023年03月07日&#xff0c;南华大学衡阳医学院李二毛团队在《Molecular Cancer》杂志发表了题为“The epigenetic regulatory mechanism of PIWI/piRNAs in human cancers”的综述文章&am…

数据处理时代,绕不开的数据分析

数据分析的出现是因为人类难以理解海量数据所呈现出来的信息&#xff0c;不能从中找到相应的规律来对现实中的事物进行对应&#xff0c;我们都知道数据有很高的价值&#xff0c;但不能利用的价值&#xff0c;没有任何意义。 为了解决这一问题&#xff0c;数据分析在长期的数据…

Golang每日一练(leetDay0012)

目录 34. 查找元素首末位置 Find-first-and-last-position-of-element-in-sorted-array &#x1f31f;&#x1f31f; 35. 搜索插入位置 Search Insert Position &#x1f31f; 36. 有效的数独 Valid Sudoku &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 …

[vue问题]Uncaught SyntaxError: Not available in legacy mode

[vue问题]Uncaught SyntaxError: Not available in legacy mode问题描述问题分析解决方案直接回退vue-i18n的版本解决错误提示的问题问题描述 Uncaught SyntaxError: Not available in legacy modeat Object.createCompileError (message-compiler.cjs.js?af13:58:1)at creat…

GTC 2023 | 「皮衣刀客」黄仁勋畅谈 AI Top 5,科学计算、生成式 AI、Omniverse 榜上有名

内容一览&#xff1a;北京时间 3 月 21 日 23:00&#xff0c;英伟达创始人兼 CEO 黄仁勋在 GTC 2023 上发表主题演讲&#xff0c;介绍了生成式 AI、元宇宙、大语言模型、云计算等领域最新进展。 关键词&#xff1a;英伟达 黄仁勋 GTC 2023 「Don’t Miss This Defining Momen…

辉煌优配|沪指震荡涨0.25%,建筑、家居等板块拉升,数字经济概念活跃

22日早盘&#xff0c;两市股指盘中强势上扬&#xff0c;接近午盘涨幅收窄&#xff1b;两市半日成交近6000亿元&#xff0c;北向资金小幅净流出。 到午间收盘&#xff0c;沪指涨0.25%报3263.85点&#xff0c;深成指涨0.39%&#xff0c;创业板指微跌0.01%&#xff0c;两市合计成交…

html(1)

创建html项目 新建html项目&#xff0c;用记事本打开&#xff1a; 只需要浏览器就可以执行里面的代码&#xff0c;不需要安装额外的运行环境&#xff08;例如JDK&#xff09; html不需要编译&#xff0c;浏览器读取后就可以执行 上述hello world在文件是如下代码&#xff1a…

静态版通讯录的实现(详解)

前言&#xff1a;内容包括三个模块&#xff1a;测试通讯录模块&#xff0c;声明模块&#xff0c;通讯录实现模块 实现一个通讯录&#xff1a; 1 可以存放100个人的信息 每个人的信息&#xff1a; 名字 性别 年龄 电话 地址 2 增加联系人信息 删除联系人信息 查找联系人信息…

windows无盘启动技术开发之传统BIOS(Legacy BIOS)引导程序开发之二

by fanxiushu 2023-03-21 转载或引用请注明原始作者&#xff0c;接上文&#xff0c;这篇文章其实主要就是讲述上文中 Int13HookEntry 这个C函数的实现过程&#xff0c;看起来就一个函数&#xff0c;可实现起来一点也不轻松。首先得准备编译环境&#xff0c;因为是16位程序&…

LeetCode岛屿问题通用解决模板

文章目录前言第一题&#xff1a;求岛屿的周长模板整理遍历方向确定边界重复遍历问题处理模板解第一题第二题&#xff1a;求岛屿数量第三题&#xff1a;岛屿的最大面积第四题&#xff1a;统计子岛屿第五题&#xff1a;统计封闭岛屿的数目第六题&#xff1a;最大人工岛总结前言 …

04.hadoopHDFS

win java访问hadoop //复制文件夹,配置环境变量//配置HADOOP_HOME为我们的路径 ,hadoop-3.3.0 ,记得JAVA_HOME不要带有空格,!!!默认java安装环境有空格C:\Program Files//要在cmd hadoop -fs 查看是否配置成功//%HADOOP_HOME%\bin到path//maven添加依赖hadoop3.1.0//创建目录Be…

常见的CMS后台getshell姿势总结

目录 WordPress dedecms aspcms 南方数据企业系统 phpmyadmin日志 pageadmin 无忧企业系统 WordPress 默认后台登录地址 /wp-login.php /wp-admin 登录后在外观的编辑里面找一个模板&#xff0c;我们在404模板 (404.php)里面写入一句话后门 可以蚁剑连接 上传一个压缩…

自定义类型的超详细讲解ᵎᵎ了解结构体和位段这一篇文章就够了ᵎ

目录 1.结构体的声明 1.1基础知识 1.2结构体的声明 1.3结构体的特殊声明 1.4结构体的自引用 1.5结构体变量的定义和初始化 1.6结构体内存对齐 那对齐这么浪费空间&#xff0c;为什么要对齐 1.7修改默认对齐数 1.8结构体传参 2.位段 2.1什么是位段 2.2位段的内存分配…

web前端框架——Vue的特性

目录 前言&#xff1a; 一.vue 二.特性 1.轻量级 2.数据绑定 3.指令 4.插件 三.比较Angular 、React 、Vue 框架之间的比较 1. Angular Angular的优点&#xff1a; 2. React React 的优点&#xff1a; 3.vue 3.Vue的优点&#xff1a; 前言&#xff1a; 本篇文章…

QT开发笔记(多媒体)

多媒体 多媒体&#xff08;Multimedia&#xff09;是多种媒体的综合&#xff0c;一般包括文本&#xff0c;声音和图像等多种媒体形式。 在计算机系统中&#xff0c;多媒体指组合两种或两种以上媒体的一种人机交互式信息交流和传播媒体。 使用的媒体包括文字、图片、照片、声音…

头歌c语言实训项目-函数(2)

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 第1关&#xff1a;模拟投掷骰子游戏: 题目&#xff1a; 代码思路&#xff1a; 代码表示&#xff1a; 第…

20230322英语学习

Why Are So Many Gen Z-Ers Drawn to Old Digital Cameras? 老式数码相机&#xff1a;Z世代的复古潮流 The latest digital cameras boast ever-higher resolutions, better performance in low light, smart focusing and shake reduction – and they’re built right into …

牛客C/C++刷题笔记(五)

122、对于"int *pa[5]&#xff1b;"的描述中&#xff0c;&#xff08; &#xff09;是正确的。 123、以下叙述中正确的是&#xff08;&#xff09; C语言的源程序中对缩进没有要求,所以A选项错误。C语言中区分大小写,main函数不能写成Main或_main,所以B选项错误。一…

声声不息,新“声”报到

魅力声音大家庭总群&#xff08;10&#xff09;大玲&#xff0c;刚见到新来的四川孩儿——樱桃&#xff0c;真是太可爱了&#xff01;可不就是&#xff0c;这孩儿真是招人稀罕&#xff0c;我现在就把她拉到咱大群里“大玲” 邀请 “樱桃” 加入群聊所有人 咱们大家庭迎来了第一…