SpringBoot源码解读与原理分析(二十七)嵌入式Tomcat

news/发布时间2024/5/15 0:17:31

文章目录

    • 前言
    • 8.1 嵌入式Tomcat简介
      • 8.1.1 嵌入式Tomcat与普通Tomcat
      • 8.1.2 Tomcat整体架构
      • 8.1.3 Tomcat的核心工作流程
    • 8.2 SpringBoot中嵌入式容器的模型
      • 8.2.1 WebServer
      • 8.2.2 WebServerFactory
      • 8.2.3 ServletWebServerFactory和ReactiveWebServerFactory
      • 8.2.4 ConfigurableServletWebServerFactory
    • 8.3 嵌入式Tomcat的初始化
      • 8.3.1 创建TomcatWebServer
        • 8.3.1.1 prepareContext
        • 8.3.1.2 getTomcatWebServer
          • (1)获取Context
          • (2)阻止Connector初始化
          • (3)启动Tomcat
          • (4)阻止Tomcat结束
      • 8.3.2 Web容器关闭相关的回调
        • 8.3.2.1 WebServerGracefulShutdownLifecycle
        • 8.3.2.2 WebServerStartStopLifecycle
    • 8.4 嵌入式Tomcat的启动
    • 8.5 小结

前言

当Web应用需要部署运行时,传统的做法是将项目打包成war包,然后部署到外置的Web容器中(如最常用的Tomcat容器)。SpringBoot的一大重要特性是支持嵌入式Web容器,基于SpringBoot的Web应用仅凭一个单独的jar包即可独立运行。

8.1 嵌入式Tomcat简介

8.1.1 嵌入式Tomcat与普通Tomcat

嵌入式Tomcat是一种可以嵌入到Web应用中,无需单独部署的Tomcat容器。

普通的外置Tomcat与嵌入式Tomcat从核心、本质上看没有任何区别,都可以承载Web应用的运行。

但SpringBoot整合嵌入式Tomcat容器时在底层设定了一些额外的限制。

  • 部署应用的限制:由于嵌入式Tomcat不是独立的Web容器,而是嵌入到特定的Web应用中的,因此该嵌入式Tomcat容器只能部署这一个特定的Web应用。
  • web.xml的限制:SpringBoot整合嵌入式Tomcat后不再对web.xml文件予以支持。
  • Servlet原生三大组件的限制:原生的基于Servlet 3.0及以上规范的Web项目,其类路径下的Servlet、Filter、Listener可以被自动扫描并注册,而SpringBoot整合嵌入式Tomcat后该特性失效。如果需要开启该特性,需要配合@ServletConponentScan注解使用。
  • JSP的限制:SpringBoot整合嵌入式Tomcat后,如果以独立jar包的方式启动项目(java -jar),则JSP页面会失效;如果以war包的方式部署到外置的Tomcat容器,则JSP页面可以正常运行。

8.1.2 Tomcat整体架构

Tomcat整体架构
图片来源于CSDN文章:Tomcat.02结构图&启动&server.xml&连接器

从架构图可以得出Tomcat的架构设计如下:

  • 一个Tomcat服务器是一个Server;
  • 一个Server包含多个服务Service,其中提供默认HTTP请求响应服务的是Catalina;
  • 一个Service包含多个Connector,用于与客户端交互,实现接收客户端请求并转发到Engine和接收Engine响应结果并响应给客户端;
  • 一个Service还包含一个Container Engine,用于真正处理客户端的请求,并响应结果;
  • 一个Container Engine包含多个Host,每个Host可以装在多个Web应用;
  • 一个Web应用对应一个Context,一个Context包含多个Servlet。

8.1.3 Tomcat的核心工作流程

Tomcat作为一个Web服务器,其核心工作是接收客户端发起的HTTP请求,转发给服务器端的Web应用处理,处理完成后将结果响应给客户端。

其工作流程大致如下:

1、请求进入Tomcat容器,Tomcat容器内部根据请求URL,判断该请求应该由哪个应用来处理,并将请求封装为ServletRequest对象,转发至对应的Web应用中的Context。
2、Context接收到ServletRequest对象后,根据请求URI定位可以接收当前请求的Servlet,并将请求转发给具体的Servlet进行处理。
3、在转发到Servlet之前,容器会检查对应的Servlet是否已加载,如果没有加载,则会利用反射机制创建Servlet对象,并调用其init方法完成初始化,之后再进行逻辑处理。
4、Servlet处理完成后,将响应结果以ServletResponse对应响应给Service中的Connector,由Connector响应给客户端,至此完成一次请求处理。

8.2 SpringBoot中嵌入式容器的模型

SpringBoot支持的嵌入式容器包括Tomcat、Jetty、Undertow、Netty等。

8.2.1 WebServer

代码清单1WebServer.java|TomcatWebServer.javapublic interface WebServer {void start() throws WebServerException;void stop() throws WebServerException;// ...
}public class TomcatWebServer implements WebServer {private final Tomcat tomcat;// ...
}

由 代码清单1 可知,WebServer是SpringBoot针对所有嵌入式Web容器制定的顶级接口,定义了嵌入式Web容器的启动和停止动作。

实现WebServer接口的实现类通常会在内部组合一个真正的嵌入式容器,如TomcatWebServer中包含一个Tomcat对象,并重写start方法实现嵌入式Web容器的启动逻辑,重写stop方法实现嵌入式Web容器的停止和销毁逻辑。

8.2.2 WebServerFactory

代码清单2WebServerFactory.javapublic interface WebServerFactory {
}

由 代码清单2 可知,WebServerFactory接口没有定义任何方法,仅为标记性接口

WebServerFactory是所有具备创建WebServer能力的工厂对象的根接口。

代码清单3ConfigurableWebServerFactory.javapublic interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPageRegistry {void setPort(int port);void setSsl(Ssl ssl);// ...// 优雅停机default void setShutdown(Shutdown shutdown) {}
}

由 代码清单3 可知,ConfigurableWebServerFactory是WebServerFactory的扩展接口,具备对WebServerFactory的配置能力(包括配置端口、SSL等)。

值得注意的是,嵌入式Web容器具有优雅停机的特性,即容器在关闭时不直接终止进程,而是预留一些时间使容器内部的业务线程全部处理完毕后才关停容器服务。

8.2.3 ServletWebServerFactory和ReactiveWebServerFactory

代码清单4ServletWebServerFactory.java|ReactiveWebServerFactory.javapublic interface ServletWebServerFactory {WebServer getWebServer(ServletContextInitializer... initializers);
}public interface ReactiveWebServerFactory {WebServer getWebServer(HttpHandler httpHandler);
}

由 代码清单4 可知,这是两个平级接口,分别是Servlet和Reactive场景下的嵌入式容器创建工厂。getWebServer方法的定义仅是入参不同(不同类型的Web容器在创建时传入的初始化组件不同),但都返回WebServer对象。

8.2.4 ConfigurableServletWebServerFactory

代码清单5ConfigurableServletWebServerFactory.javapublic interface ConfigurableServletWebServerFactory extends ConfigurableWebServerFactory, ServletWebServerFactory {void setContextPath(String contextPath);void setInitializers(List<? extends ServletContextInitializer> initializers);void addInitializers(ServletContextInitializer... initializers);void setInitParameters(Map<String, String> initParameters);// ...
}

由 代码清单5 可知,ConfigurableServletWebServerFactory是ConfigurableWebServerFactory和ServletWebServerFactory组合后产生的子接口,具备更多能力,如可以设置访问Web应用所需的context-path、设置ServletContextInitializer、设置初始化参数等。

8.3 嵌入式Tomcat的初始化

在 SpringBoot源码解读与原理分析(二十三)IOC容器的刷新(四) 中提到,IOC容器刷新的第9步的onRefresh方法是一个模板方法,需要子类实现。

ServletWebServerApplicationContext类就是其中一个子类,重写了onRefresh方法,用于嵌入式Web容器的初始化。

代码清单6ServletWebServerApplicationContext.java@Override
protected void onRefresh() {super.onRefresh();try {createWebServer();} // catch ...
}private void createWebServer() {WebServer webServer = this.webServer;ServletContext servletContext = getServletContext();// 如果WebServer和ServletContext均为null,则需要创建嵌入式Web容器if (webServer == null && servletContext == null) {// 获取WebServerFactoryServletWebServerFactory factory = getWebServerFactory();// 创建WebServerthis.webServer = factory.getWebServer(getSelfInitializer());// 回调优雅停机的钩子getBeanFactory().registerSingleton("webServerGracefulShutdown",new WebServerGracefulShutdownLifecycle(this.webServer));// 回调容器启停的生命周期钩子getBeanFactory().registerSingleton("webServerStartStop",new WebServerStartStopLifecycle(this, this.webServer));} else if (servletContext != null) {try {getSelfInitializer().onStartup(servletContext);} catch (ServletException ex) {throw new ApplicationContextException("Cannot initialize servlet context", ex);}}initPropertySources();
}protected ServletWebServerFactory getWebServerFactory() {String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);// ...return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

由 代码清单6 可知,createWebServer方法中包含WebServer的创建,还有两个与生命周期回调相关的钩子。

8.3.1 创建TomcatWebServer

由 代码清单6 可知,创建嵌入式Web容器的入口是ServletWebServerFactory,其中嵌入式Tomcat容器的创建在其实现类TomcatServletWebServerFactory的getWebServer方法中实现。

代码清单7TomcatServletWebServerFactory.javapublic static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
private String protocol = DEFAULT_PROTOCOL;
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {// ...Tomcat tomcat = new Tomcat();// 给嵌入式Tomcat创建一个临时文件夹,用于存放Tomcat运行中需要的文件File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");tomcat.setBaseDir(baseDir.getAbsolutePath());// Connector中默认放入的protocol为NIO模式Connector connector = new Connector(this.protocol);connector.setThrowOnFailure(true);// 向Service中添加Connector,并执行定制规则tomcat.getService().addConnector(connector);customizeConnector(connector);tomcat.setConnector(connector);// 关闭热部署(因为嵌入式Tomcat不存在修改web.xml、war包等情况)tomcat.getHost().setAutoDeploy(false);configureEngine(tomcat.getEngine());for (Connector additionalConnector : this.additionalTomcatConnectors) {tomcat.getService().addConnector(additionalConnector);}// 生成TomcatEmbeddedContext上下文prepareContext(tomcat.getHost(), initializers);// 创建TomcatWebServerreturn getTomcatWebServer(tomcat);
}

由 代码清单7 可知,获取到WebServerFactory后,下一步会执行getWebServer方法创建嵌入式Tomcat。

该方法核心步骤大致分为三步:创建Tomcat对象,并初始化基础的Connector和Engine;prepareContext方法初始化Context,构建应用上下文;getTomcatWebServer方法创建最终的TomcatWebServer对象。

8.3.1.1 prepareContext
代码清单8TomcatServletWebServerFactory.javaprotected void prepareContext(Host host, ServletContextInitializer[] initializers) {File documentRoot = getValidDocumentRoot();// 创建ContextTomcatEmbeddedContext context = new TomcatEmbeddedContext();// ...// 配置生命周期监听器等// ...// 应用ServletContextInitializerServletContextInitializer[] initializersToUse = mergeInitializers(initializers);// Context添加到Host中host.addChild(context);configureContext(context, initializersToUse);postProcessContext(context);
}

由 代码清单8 可知,prepareContext方法的环节包括创建Tomcat内置上下文对象、配置生命周期监听器、应用ServletContextInitializer等。

  • ServletContextInitializer的设计

ServletContextInitializer本身并不是Servlet相关规范中定义的API,它是SpringBoot 1.4.0以后定义的API接口,这也说明ServletContextInitializer与某个具体的Web容器没有任何关系

借助IDEA,可以发现ServletContextInitializer的其中一个实现类RegistrationBean,而它又是Servlet三大核心组件的注册实现类ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean的父类。

如此来看,Servlet三大核心组件应会通过ServletContextInitializer与嵌入式Tomcat对接。

代码清单9TomcatServletWebServerFactory.javaprotected void configureContext(Context context, ServletContextInitializer[] initializers) {TomcatStarter starter = new TomcatStarter(initializers);if (context instanceof TomcatEmbeddedContext) {TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;embeddedContext.setStarter(starter);embeddedContext.setFailCtxIfServletStartFails(true);}context.addServletContainerInitializer(starter, NO_CLASSES);// ...
}

由 代码清单8、9 可知,prepareContext方法倒数第2行有对ServletContextInitializer的配置应用,即调用configureContext方法,该方法会在内部实例化一个TomcatStarter对象,并加载到Servlet容器中。

代码清单10TomcatStarter.javaclass TomcatStarter implements ServletContainerInitializer {private final ServletContextInitializer[] initializers;@Overridepublic void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {try {for (ServletContextInitializer initializer : this.initializers) {initializer.onStartup(servletContext);}} // catch ...}
}

由 代码清单10 可知,TomcatStarter对象本身也是一个ServletContainerInitializer,被Servlet容器加载后,调用其onStartup方法。在该方法内部,会循环调用所有ServletContextInitializer的onStartup方法,由此完成Servlet原生三大核心组件的注册。

8.3.1.2 getTomcatWebServer

prepareContext方法执行完后,会执行getTomcatWebServer方法,以创建TomcatWebServer对象。

代码清单11TomcatServletWebServerFactory.javaprotected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}
代码清单12TomcatWebServer.javapublic TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {Assert.notNull(tomcat, "Tomcat Server must not be null");this.tomcat = tomcat;this.autoStart = autoStart;this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;initialize();
}

由 代码清单11、12 可知,TomcatWebServer的初始化逻辑在其initialize方法中。

(1)获取Context
代码清单13TomcatWebServer.javaprivate void initialize() throws WebServerException {// ...//(1)获取第一个可用的ContextContext context = findContext();//...
}private Context findContext() {for (Container child : this.tomcat.getHost().findChildren()) {if (child instanceof Context) {return (Context) child;}}throw new IllegalStateException("The host does not contain a Context");
}

由 代码清单13 可知,initialize方法的第一个核心动作获取Tomcat中第一个可用的Context,即调用findContext方法。findContext方法会从Host中获取第一个类型为Context的子元素并返回。

实际上,这里获取到的就是在prepareContext方法中创建的TomcatEmbeddedContext。

(2)阻止Connector初始化
代码清单14TomcatWebServer.javaprivate void initialize() throws WebServerException {// ...//(2)添加LifecycleListener,移除Connectorcontext.addLifecycleListener((event) -> {if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {// javadoc:移除Service中的Connector,那么当服务启动后协议绑定就不会发生。removeServiceConnectors();}});// ...
}

由 代码清单14 可知,initialize方法的第二个核心动作是添加一个LifecycleListener,该监听器的功能是移除Service中的Connector,即阻止Connector初始化。

在 代码清单7 中,getWebServer方法创建了Connector并放入了Service中,为什么这里要移除掉呢?

那是因为,创建嵌入式Web容器的时机是在IOC容器刷新的第9步,早于第11步的 初始化所有剩下的单实例finishBeanFactoryInitialization方法,此时IOC容器中绝大多数单实例bean对象尚未初始化,还不具备提供服务的能力。

Connector的功能是与客户端交互,一旦Connector初始化完成,意味着Tomcat可以对外提供服务,即客户端可以成功访问到Tomcat服务。

为了访问客户端成功访问到暂时无法提供服务的Tomcat服务,需要先将Connector移除。

(3)启动Tomcat
代码清单15TomcatWebServer.javaprivate void initialize() throws WebServerException {// ...//(3)启动Tomcatthis.tomcat.start();// ...
}

由 代码清单15 可知,initialize方法的第三个核心动作是启动Tomcat。

代码清单16Tomcat.javapublic void start() throws LifecycleException {getServer();server.start();
}public Server getServer() {// ...server = new StandardServer();// ...// 端口设置为-1,代表这是嵌入式Tomcatserver.setPort( -1 );Service service = new StandardService();service.setName("Tomcat");server.addService(service);return server;
}
代码清单17LifecycleBase.java@Override
public final synchronized void start() throws LifecycleException {// 前置判断 ...// 初始化if (state.equals(LifecycleState.NEW)) {init();} // else if ...try {setStateInternal(LifecycleState.STARTING_PREP, null, false);// 启动自身startInternal();// ...} // catch ...
}

由 代码清单16、17 可知,Tomcat引导Server初始化和启动时,会在获取到Server后调用其start方法,而start方法定义在所有Tomcat核心组件的共同父类LifecycleBase上。

值得注意的是,获取Server时端口号被设置为-1,代表这是嵌入式Tomcat,方便后续使用。

代码清单18LifecycleBase.java@Override
public final synchronized void init() throws LifecycleException {if (!state.equals(LifecycleState.NEW)) {invalidTransition(Lifecycle.BEFORE_INIT_EVENT);}try {setStateInternal(LifecycleState.INITIALIZING, null, false);initInternal();setStateInternal(LifecycleState.INITIALIZED, null, false);} // catch ...
}protected abstract void initInternal() throws LifecycleException;

由 代码清单18 可知,init方法依然由父类LifecycleBase定义,其中间的initInternal方法是一个模板方法,由子类实现。

代码清单19StandardServer.java@Override
protected void initInternal() throws LifecycleException {super.initInternal();// ...for (Service service : services) {service.init();}
}

由 代码清单19 可知,StandardServer类实现了initInternal方法,在该方法中会触发Service的初始化。

通过查阅源码发现,service.init()内部会触发Connector的初始化,以及后续所有核心组件的初始化逻辑都大致相同。完整的逻辑可以通过下面这张时序图体现出来:

在这里插入图片描述

(4)阻止Tomcat结束
代码清单20TomcatWebServer.javaprivate void initialize() throws WebServerException {// ...//(4)阻止Tomcat结束startDaemonAwaitThread();// ...
}private void startDaemonAwaitThread() {// 创建一个新线程Thread awaitThread = new Thread("container-" + (containerCounter.get())) {@Overridepublic void run() {TomcatWebServer.this.tomcat.getServer().await();}};awaitThread.setContextClassLoader(getClass().getClassLoader());// 将该线程设置为非守护进程awaitThread.setDaemon(false);awaitThread.start();
}

由 代码清单20 可知,initialize方法的第四个核心动作是启动一个新的awaitThread线程,以阻止Tomcat进程结束,其内部实现的run方法是回调Server的await方法。

  • Daemon(守护)线程

在一个Java应用中,只要有一个非Daemon线程在运行,Daemon线程就不会停止,整个应用也不会终止。

如果Tomcat需要一直运行以接收客户端请求,就必须让Tomcat内部的Daemon进程都存活,至少需要一个能阻止Tomcat进程停止的非Daemon进程,而这里创建的awaitThread进程,将其Daemon设置为false,就是要负责阻止Tomcat进程停止。

  • await方法
代码清单21StandardServer.javapublic void await() {// -2 时的处理 ...// 如果关闭Tomcat的端口是-1,代表是嵌入式Tomcatif (getPortWithOffset() == -1) {try {awaitThread = Thread.currentThread();while(!stopAwait) {try {Thread.sleep( 10000 );} catch( InterruptedException ex ) {// continue and check the flag}}} finally {awaitThread = null;}return;}// 退出端口的其他处理 ...
}

由 代码清单21 可知,如果关闭Tomcat的端口是-1,代表是嵌入式Tomcat(详见(3)启动Tomcat)。阻塞Tomcat进程结束的方式是每隔10s检查一次stopAwait的值,只要该值一直为false,Tomcat就不会退出。

······

经过上述一系列核心组件的初始化和启动,嵌入式Tomcat容器初始化完成。但此时Tomcat还不能提供服务,因为Connector在该阶段被移除,无法与客户端建立有效连接。

8.3.2 Web容器关闭相关的回调

由 代码清单6 可知,createWebServer方法中还有两个与生命周期回调相关的钩子。

8.3.2.1 WebServerGracefulShutdownLifecycle
代码清单22WebServerGracefulShutdownLifecycle.javaclass WebServerGracefulShutdownLifecycle implements SmartLifecycle {private final WebServer webServer;WebServerGracefulShutdownLifecycle(WebServer webServer) {this.webServer = webServer;}@Overridepublic void stop(Runnable callback) {this.running = false;this.webServer.shutDownGracefully((result) -> callback.run());}
}

由 代码清单22 可知,WebServerGracefulShutdownLifecycle用于触发嵌入式Web容器优雅停机的核心生命周期回调,它实现了SmartLifecycle接口,可以在IOC容器销毁阶段回调其stop方法以触发销毁逻辑。

stop方法会回调WebServer的shutDownGracefully方法实现优雅停机。

8.3.2.2 WebServerStartStopLifecycle
代码清单23WebServerStartStopLifecycle.javaclass WebServerStartStopLifecycle implements SmartLifecycle {private final WebServer webServer;WebServerStartStopLifecycle(WebServer webServer) {this.webServer = webServer;}@Overridepublic void start() {this.webServer.start();this.running = true;this.applicationContext.publishEvent(new ServletWebServerInitializedEvent(this.webServer, this.applicationContext));}@Overridepublic void stop() {this.webServer.stop();}
}

由 代码清单23 可知,WebServerStartStopLifecycle的作用启动和关闭嵌入式Web容器,它会在IOC容器刷新即将完成/销毁时被回调,从而回调WebServer的start/stop方法,真正启动/关闭嵌入式Web容器。

8.4 嵌入式Tomcat的启动

onRefresh方法执行完毕,后续的finishBeanFactoryInitialization方法执行完毕,IOC容器中所有非延迟加载的单实例bean对象均初始化完毕,此时会执行IOC容器刷新的第12步finishRefresh方法,该方法中会回调所有的SmartLifeCycle,其中就包括 8.3.2.2 中的WebServerStartStopLifecycle,它会在该阶段回调嵌入式Web容器的start方法,从而真正启动Web容器。

代码清单24WebServerManager.javavoid start() {this.handler.initializeHandler();this.webServer.start();this.applicationContext.publishEvent(new ReactiveWebServerInitializedEvent(this.webServer, this.applicationContext));
}
代码清单25TomcatWebServer.javapublic void start() throws WebServerException {synchronized (this.monitor) {if (this.started) {return;}try {// 还原、启动ConnectoraddPreviouslyRemovedConnectors();Connector connector = this.tomcat.getConnector();if (connector != null && this.autoStart) {performDeferredLoadOnStartup();}checkThatConnectorsHaveStarted();this.started = true;logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '"+ getContextPath() + "'");} // catch ...}
}private void addPreviouslyRemovedConnectors() {Service[] services = this.tomcat.getServer().findServices();for (Service service : services) {// 之前移除的Connector在serviceConnectors中Connector[] connectors = this.serviceConnectors.get(service);if (connectors != null) {for (Connector connector : connectors) {// 添加并启动Connectorservice.addConnector(connector);if (!this.autoStart) {stopProtocolHandler(connector);}}this.serviceConnectors.remove(service);}}
}

由 代码清单24、25 可知,进入TomcatWebServer的start方法后,会调用addPreviouslyRemovedConnectors方法以还原并启动之前被移除掉的Connector(启动逻辑也在addConnector方法中)。

至此,嵌入式Tomcat完整启动。

8.5 小结

第8章到此就梳理完毕了,本章的主题是:嵌入式Web容器。回顾一下本章的梳理的内容:

(二十七)嵌入式Tomcat容器

更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

第9章主要梳理:AOP模块的生命周期。主要内容包括:

  • AOP的核心后置处理器AnnotationAwareAspectJAutoProxyCreator;
  • AOP底层收集切面类的机制;
  • Bean被AOP代理的过程原理;
  • 代理对象执行的全流程分析。

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

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

相关文章

【Java程序设计】【C00291】基于Springboot的网上图书商城(有论文)

基于Springboot的网上图书商城&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的网上图书商城 本系统分为系统功能模块、管理员功能模块以及卖家功能模块。 系统功能模块&#xff1a;在系统首页可以查看首页、图书…

Window部署ElasticSearch

下载参考文档&#xff1a;C# ElasticSearch环境搭建与使用_c# elasticsearch helper-CSDN博客 版本&#xff1a; ElasticSearch&#xff1a;7.17.5 jdk&#xff1a;11.0.10 1、下载后解压缩目录结构 2、修改config目录文件elasticsearch.yml 取消注释下面节点 cluster.name…

《基于ICEEMDAN 和分布熵的SS-Y伸缩仪信号随机噪声压制方法》论文笔记

吴林斌&#xff0e;基于ICEEMDAN 和分布熵的SS-Y 伸缩仪信号随机噪声压制方法[J/OL]&#xff0e;大地测量与地球动力学. https://doi.org/10.14075/j.jgg.2023.07.103 CEEMDAN和ICEEMDAN性质差不多&#xff0c;只是改良了一下 这篇文章相较于上级篇文章&#xff0c;没有用方差…

window: C++ 获取自己写的dll的地址

我自己用C写了一个插件,插件是dll形式的,我的插件式在dll的目录下有个config文件夹,里面是我用json写的插件配置文件,当插件运行的时候我需要读取到json配置文件,所有最重要的就是如何获取dll的路径. 大概就是这么个结构, 我自己封装了一个函数.只适用于window编程,因为里面用…

mac m1调试aarch64 android kernel最终方案

问题 这是之前的&#xff0c;调试android kernel的方案还是太笨重了 完美调试android-goldfish(linux kernel) aarch64的方法 然后&#xff0c;看GeekCon AVSS 2023 Qualifier&#xff0c;通过 sdk-repo-linux_aarch64-emulator-8632828.zip 进行启动 完整编译的aosp kernnl…

PyTorch使用Tricks:学习率衰减 !!

文章目录 前言 1、指数衰减 2、固定步长衰减 3、多步长衰减 4、余弦退火衰减 5、自适应学习率衰减 6、自定义函数实现学习率调整&#xff1a;不同层不同的学习率 前言 在训练神经网络时&#xff0c;如果学习率过大&#xff0c;优化算法可能会在最优解附近震荡而无法收敛&#x…

Stable Diffusion 3 Early Preview发布

2月22日&#xff0c;Stability AI 发布了 Stable Diffusion 3 early preview&#xff0c;这是一种开放权重的下一代图像合成模型。据报道&#xff0c;它继承了其前身&#xff0c;生成了详细的多主题图像&#xff0c;并提高了文本生成的质量和准确性。这一简短的公告并未附带公开…

Vue+SpringBoot打造快递管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 数据中心模块2.2 快递类型模块2.3 快递区域模块2.4 快递货架模块2.5 快递档案模块 三、界面展示3.1 登录注册3.2 快递类型3.3 快递区域3.4 快递货架3.5 快递档案3.6 系统基础模块 四、免责说明 一、摘要 1.1 项目介绍 …

Android 内存优化内存泄漏处理

一:匿名内部类/非静态内部类 匿名内部类的泄漏原因&#xff1a;匿名内部类会隐式地持有外部类的引用.当外部类被销毁时&#xff0c;内部类并不会自动销毁&#xff0c;因为内部类并不是外部类的成员变量&#xff0c; 它们只是在外部类的作用域内创建的对象&#xff0c;所以内部…

深究 DevOps 与平台工程的区别

今天&#xff0c;我们将讨论平台工程和 DevOps 的关系。尽管这两个概念有一些共同点&#xff0c;但它们仍然是截然不同的&#xff0c;我们将具体了解它们之间的区别。本文旨在解释当代软件工程中的这两个基本概念。通过实际案例&#xff0c;我们将分别说明这两个方法如何塑造了…

【LeetCode】升级打怪之路 Day 01:二分法

今日题目&#xff1a; 704. 二分查找35. 搜索插入位置34. 在排序数组中查找元素的第一个和最后一个位置 目录 今日总结Problem 1: 二分法LeetCode 704. 二分查找 【easy】LeetCode 35. 搜索插入位置 ⭐⭐⭐⭐⭐LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置 【medi…

【力扣 - 搜索插入位置】

题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 题解1 int searchInsert(int* nums, int numsSize, in…

数据结构之堆

什么是堆 在计算机科学中&#xff0c;堆&#xff08;Heap&#xff09;是一种特殊的数据结构&#xff0c;它是一种完全二叉树&#xff08;或者近似完全二叉树&#xff09;&#xff0c;并且满足堆属性。堆有两种常见的类型&#xff1a;最大堆&#xff08;Max Heap&#xff09;和…

K线实战分析系列之三:吞没形态

K线实战分析系列之三&#xff1a;吞没形态 一、吞没形态二、看涨吞没形态三、看跌吞没形态四、吞没形态判别标准 一、吞没形态 两根或两根以上的K线形成的组合形态&#xff0c;吞没形态就是一种主要的反转形态。 这个形态由两根K线组成&#xff0c;前短后长&#xff0c;一阴一…

【STM32】软件SPI读写W25Q64芯片

目录 W25Q64模块 W25Q64芯片简介 硬件电路 W25Q64框图 Flash操作注意事项 状态寄存器 ​编辑 指令集 INSTRUCTIONS​编辑 ​编辑 SPI读写W25Q64代码 硬件接线图 MySPI.c MySPI.h W25Q64 W25Q64.c W25Q64.h W25Q64_Ins.h main.c 测试 SPI通信&#xff08;W25…

Android加载富文本

直接用webview加载&#xff1a; package com.example.testcsdnproject;import androidx.appcompat.app.AppCompatActivity;import android.annotation.SuppressLint; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.webk…

软件设计师软考题目解析02 --每日五题

想说的话&#xff1a;要准备软考了。0.0&#xff0c;其实我是不想考的&#xff0c;但是吧&#xff0c;由于本人已经学完所有知识了&#xff0c;只是被学校的课程给锁在那里了&#xff0c;不然早找工作去了。寻思着反正也无聊&#xff0c;就考个证玩玩。 本人github地址&#xf…

大公司跨域文件交换,如何兼顾安全效率和经济性?

现如今&#xff0c;随着我国经济的不断发展向前&#xff0c;许许多多的企业其规模也在不断的壮大&#xff0c;大型企业在全国、甚至全球范围的重要地区都设有自己的分支机构&#xff0c;总部与分支机构间&#xff0c;各分支机构间均存在数据交换需求&#xff0c;同时&#xff0…

【Unity】提示No valid Unity Editor liscense found.Please active your liscense.

有两个软件&#xff0c;如果只有一个&#xff0c;点黑的不会有效果、、、、&#xff08;楼主是这个原因&#xff0c;可以对号入座一下&#xff09; 简而言之&#xff0c;就是去下载Unity Hub&#xff0c;再里面激活管理通行证 问题情境&#xff1a; 点击unity出现以下弹窗&a…

设计模式学习笔记 - 面向对象 - 7.为什么要多用组合少用继承?如何决定该用组合还是继承?

前言 在面向对象编程中&#xff0c;有一条非常经典的设计原则&#xff1a;组合优于继承&#xff0c;多用组合少用继承。 为什么不推荐使用继承&#xff1f; 组合比继承有哪些优势&#xff1f; 如何判断该用组合还是继承&#xff1f; 为什么不推荐使用继承&#xff1f; 继承…
推荐文章