【SpringBoot系列-01】Spring Boot 启动原理深度解析

【SpringBoot系列-01】Spring Boot 启动原理深度解析

大家好!今天咱们来好好聊聊Spring Boot的启动原理。估计不少人跟我一样,刚开始用Spring Boot的时候觉得这玩意儿真神奇,一个main方法跑起来就啥都有了。但时间长了总会好奇:这背后到底发生了啥?

1. 启动流程源码分析

咱们先从最熟悉的入口开始,就是那个带着@SpringBootApplication注解的main方法:

@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {// 这句就是启动的核心,咱们今天就围着它转SpringApplication.run(DemoApplication.class, args);}
}

就这么一行代码,背后却藏着大学问。咱们先来看个整体的流程图,有个宏观认识:

main方法
创建SpringApplication实例
调用run方法
准备环境Environment
创建ApplicationContext
预处理上下文
刷新上下文refresh
刷新后处理
触发运行时处理器
启动完成

run()方法里的关键步骤

咱们直接看SpringApplication.run()方法的源码(基于2.7.x版本):

public ConfigurableApplicationContext run(String... args) {// 计时器,记录启动时间,调试时很有用StopWatch stopWatch = new StopWatch();stopWatch.start();// 初始化应用上下文和异常报告器ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();// 配置headless模式,一般用于服务器环境,不需要显示器等外设configureHeadlessProperty();// 第一步:获取并启动监听器SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();try {// 第二步:准备应用参数ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 第三步:准备环境(重点!)ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);configureIgnoreBeanInfo(environment);// 打印Banner,就是启动时那个Spring的logoBanner printedBanner = printBanner(environment);// 第四步:创建应用上下文(重点!)context = createApplicationContext();// 第五步:准备异常报告器exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);// 第六步:预处理上下文(重点!)prepareContext(context, environment, listeners, applicationArguments, printedBanner);// 第七步:刷新上下文(最核心!)refreshContext(context);// 第八步:刷新后的处理afterRefresh(context, applicationArguments);// 停止计时器stopWatch.stop();if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 通知监听器启动完成listeners.started(context);// 第九步:执行 runnerscallRunners(context, applicationArguments);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}try {listeners.running(context);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, null);throw new IllegalStateException(ex);}// 返回上下文return context;
}

各步骤详细解析

Main方法SpringApplicationListenersEnvironmentApplicationContextBeansSpringApplication.run()创建SpringApplication实例获取并启动监听器starting事件prepareEnvironment()environmentPrepared事件createApplicationContext()prepareContext()contextPrepared事件refreshContext()扫描和加载BeancontextRefreshed事件started事件callRunners()running事件返回ApplicationContextMain方法SpringApplicationListenersEnvironmentApplicationContextBeans

这段代码虽然长,但逻辑很清晰。我给你们划几个重点:

  1. 环境准备(prepareEnvironment):这里会加载各种配置,包括application.properties、系统变量、命令行参数等。调试时可以看这里加载了哪些配置源。

  2. 创建应用上下文(createApplicationContext):根据应用类型(Servlet/Reactive/None)创建不同的上下文。这里有个小技巧,你调试时注意看ApplicationContext的具体实现类,Web应用一般是AnnotationConfigServletWebServerApplicationContext

  3. 预处理上下文(prepareContext):这里会加载咱们的主配置类(就是带@SpringBootApplication的那个类)。

  4. 刷新上下文(refreshContext):这是最核心的一步,里面会完成Bean的扫描、创建、依赖注入等一系列操作。Spring的IoC容器就是在这里真正工作的。

  5. 执行runners:这是启动完成前的最后一步,咱们可以在这里做一些初始化工作。

我踩过一个坑,就是在项目启动慢的时候,不知道哪里出了问题。后来就是在run()方法里打了断点,一步步看哪个阶段耗时最长,最后发现是某个配置类加载了太多不必要的Bean。所以说,熟悉这个流程对排查问题非常有帮助。

2. SpringApplication初始化过程

咱们刚才看了run()方法的流程,但在调用run()之前,SpringApplication实例的创建也很关键。咱们来看它的构造器:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;// 断言主源不能为null,否则启动不了Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 第一步:判断应用类型this.webApplicationType = WebApplicationType.deduceFromClasspath();// 第二步:加载初始化器this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 第三步:加载监听器setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 第四步:推断主应用类(就是咱们写main方法的那个类)this.mainApplicationClass = deduceMainApplicationClass();
}

SpringApplication初始化流程图

有Servlet类
有Reactive类
都没有
SpringApplication构造器
判断应用类型
类路径检查
SERVLET类型
REACTIVE类型
NONE类型
加载初始化器
从spring.factories加载
加载监听器
推断主应用类
初始化完成

为什么要判断webApplicationType?

这个判断太重要了!WebApplicationType.deduceFromClasspath()会根据类路径上的类来判断应用类型:

  • SERVLET:如果有Servlet相关类且没有WebFlux相关类,就是普通的Spring MVC应用
  • REACTIVE:如果有WebFlux相关类且没有Servlet相关类,就是响应式应用
  • NONE:都没有,就是普通的非Web应用

这直接决定了后面创建什么样的ApplicationContext和嵌入式服务器。比如Web应用会创建TomcatServletWebServerFactory,而非Web应用就不会。

实际开发中,有时候你明明想创建一个非Web应用,却因为引入了spring-boot-starter-web依赖,导致它变成了Web应用,启动时会自动启动Tomcat。这时候你就可以在启动类里手动设置:

public static void main(String[] args) {new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE) // 强制非Web应用.run(args);
}

初始化器和监听器是怎么被加载的

注意构造器里的getSpringFactoriesInstances()方法,这是Spring Boot的一个核心机制。它会去扫描所有jar包下的META-INF/spring.factories文件,加载里面配置的类。

比如ApplicationContextInitializer的加载,就是读取spring.factories中key为org.springframework.context.ApplicationContextInitializer的配置。

咱们自己写starter的时候,也经常用这招。比如想自动注册一些组件,就可以在自己的starter里放一个spring.factories文件,配置上需要自动加载的类。

给你们看个小demo,自定义一个初始化器:

// 自定义初始化器
public class MyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {System.out.println("自定义初始化器执行了!");// 可以在这里做一些早期的配置ConfigurableEnvironment environment = applicationContext.getEnvironment();environment.setActiveProfiles("dev"); // 比如强制设置激活的环境}
}

然后在resources下创建META-INF/spring.factories

org.springframework.context.ApplicationContextInitializer=\
com.example.demo.MyInitializer

这样启动的时候,咱们的初始化器就会被自动加载执行了。是不是很简单?这招在开发中间件或者通用组件时特别有用。

3. 事件监听机制与启动阶段划分

Spring Boot在启动过程中会触发一系列事件,这些事件可以帮助我们在不同阶段做一些自定义操作。咱们先来看一张表格,了解下主要的事件及其触发时机:

事件类型触发时机主要用途
ApplicationStartingEvent刚调用run()方法时,在任何处理之前最早的事件,可用于初始化一些非常早期的资源
ApplicationEnvironmentPreparedEvent环境准备完成,但上下文还没创建可以修改环境变量,比如添加额外的配置
ApplicationContextInitializedEvent上下文创建并初始化,但Bean定义还没加载可以对上下文做一些设置
ApplicationPreparedEvent上下文准备完成,但还没刷新可以在Bean加载前做一些操作
ApplicationStartedEvent上下文刷新完成,Bean已加载,但runner还没执行可以做一些启动后的准备工作,如缓存预热
ApplicationReadyEvent所有启动过程完成,应用已可以处理请求通知应用已就绪
ApplicationFailedEvent启动失败时处理启动失败的情况,如资源清理

事件触发流程图

run()方法调用
环境准备
上下文初始化
上下文准备
刷新上下文
启动完成
Runners执行完成
应用就绪
失败
失败
失败
失败
失败
失败
ApplicationStartingEvent
ApplicationEnvironmentPreparedEvent
ApplicationContextInitializedEvent
ApplicationPreparedEvent
ContextRefreshed
ApplicationStartedEvent
ApplicationReadyEvent
ApplicationFailedEvent

这些事件都是通过SpringApplicationRunListeners来传播的。咱们来写个监听器的demo,感受一下:

// 监听启动完成事件
@Component
public class MyStartupListener implements ApplicationListener<ApplicationStartedEvent> {@Overridepublic void onApplicationEvent(ApplicationStartedEvent event) {System.out.println("应用启动完成,开始预热缓存...");// 模拟缓存预热CacheManager cacheManager = event.getApplicationContext().getBean(CacheManager.class);Cache userCache = cacheManager.getCache("userCache");// 预热一些常用数据userCache.put(1L, new User(1L, "admin"));System.out.println("缓存预热完成!");}
}// 缓存配置
@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic CacheManager cacheManager() {return new ConcurrentMapCacheManager("userCache");}
}// User类
public class User {private Long id;private String name;public User(Long id, String name) {this.id = id;this.name = name;}// getter和setter方法public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}

这个例子中,我们在应用启动完成后,预热了用户缓存,这样用户第一次访问时就不用等数据库查询了。这在实际项目中是个很常见的优化手段。

另外,还有个小技巧:如果你的监听器需要排序执行,可以实现Ordered接口或者加上@Order注解。

4. Bean定义加载过程

Bean的加载可以说是Spring的灵魂了,咱们来看看Spring Boot是怎么加载Bean定义的。

Bean加载流程图

注解类型
注解类型
注解类型
注解类型
@SpringBootApplication
@EnableAutoConfiguration
@ComponentScan
@SpringBootConfiguration
扫描当前包及子包
查找组件注解
注册为BeanDefinition
AutoConfigurationImportSelector
加载spring.factories
获取所有自动配置类
根据@Conditional条件过滤
注册符合条件的配置类
@Component
@Service
@Repository
@Controller
BeanDefinitionRegistry
创建Bean实例
依赖注入
初始化完成

@ComponentScan的扫描逻辑

@SpringBootApplication注解里包含了@ComponentScan,它会扫描指定包下的类,把带有@Component@Service@Repository@Controller等注解的类注册为Bean。

咱们来看下它的核心逻辑(简化版):

// ComponentScanAnnotationParser的parse方法
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {// 创建扫描器ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);// ... 省略部分代码 ...// 配置包含过滤器for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {for (TypeFilter typeFilter : typeFiltersFor(filter)) {scanner.addIncludeFilter(typeFilter);}}// 配置排除过滤器(重点注意!)for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {for (TypeFilter typeFilter : typeFiltersFor(filter)) {scanner.addExcludeFilter(typeFilter);}}// 配置扫描的包Set<String> basePackages = new LinkedHashSet<>();String[] basePackagesArray = componentScan.getStringArray("basePackages");for (String pkg : basePackagesArray) {String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);Collections.addAll(basePackages, tokenized);}// 如果没有指定包,默认使用@ComponentScan所在类的包if (basePackages.isEmpty()) {basePackages.add(ClassUtils.getPackageName(declaringClass));}// 开始扫描并注册Bean定义return scanner.doScan(StringUtils.toStringArray(basePackages));
}

这里有个地方要特别注意:excludeFilters会过滤掉某些类。默认情况下,Spring Boot会排除一些特定的类,比如带有@ConditionalOnMissingBean等条件注解且条件不满足的类。

实际开发中,有时候你会发现明明加了@Service注解的类,却没有被注册为Bean,这时候就要检查:

  1. 是不是包扫描路径不对
  2. 是不是被某个过滤器排除了
  3. 是不是有条件注解没满足

可以在scanner.doScan()这里打个断点,看看扫描结果里有没有你的类。

自动配置类是怎么被加载的

Spring Boot的自动配置是它最强大的功能之一,这得益于@EnableAutoConfiguration注解。咱们来看它的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";Class<?>[] exclude() default {};String[] excludeName() default {};
}

关键就在@Import(AutoConfigurationImportSelector.class),这个类会帮我们导入所有符合条件的自动配置类。

自动配置加载流程

@SpringBootApplication@EnableAutoConfigurationAutoConfigurationImportSelectorspring.factories@ConditionalBeanDefinitionRegistry包含注解@Import导入加载META-INF/spring.factories返回所有自动配置类列表去重和排序检查@Conditional条件true注册配置类false跳过该配置类alt[条件满足][条件不满足]loop[每个自动配置类]自动配置完成@SpringBootApplication@EnableAutoConfigurationAutoConfigurationImportSelectorspring.factories@ConditionalBeanDefinitionRegistry

AutoConfigurationImportSelector的核心方法是selectImports(),它会从META-INF/spring.factories中加载所有配置的自动配置类(key为org.springframework.boot.autoconfigure.EnableAutoConfiguration),然后根据条件注解(@Conditional)筛选出符合条件的配置类。

咱们自己写starter的时候,就是通过这种方式来实现自动配置的。比如mybatis-spring-boot-starter里就有MybatisAutoConfiguration这个自动配置类。

为什么@Configuration注解不能少

为什么必须加这个注解呢?因为Spring在处理@Configuration注解的类时,会通过CGLIB为它创建一个代理对象,这个代理会负责处理@Bean方法之间的依赖关系,确保Bean的单例性。

举个例子:

// 正确的配置类
@Configuration
public class AppConfig {@Beanpublic ServiceA serviceA() {return new ServiceA();}@Beanpublic ServiceB serviceB() {// 这里会调用serviceA()方法return new ServiceB(serviceA());}
}// 如果不加@Configuration(错误示例)
public class AppConfig {@Beanpublic ServiceA serviceA() {return new ServiceA();}@Beanpublic ServiceB serviceB() {// 每次调用serviceA()都会创建新实例!return new ServiceB(serviceA());}
}

如果加了@Configuration,不管调用多少次serviceA(),返回的都是同一个实例(代理会从容器中获取)。但如果没加,每次调用都会创建一个新实例,这就违反了Spring的单例原则,可能会导致各种奇怪的问题。

所以记住,配置类一定要加@Configuration注解,别偷懒!

5. 启动扩展点详解

Spring Boot提供了很多扩展点,让我们可以在启动过程中插入自己的逻辑。咱们来讲几个常用的。

扩展点执行顺序图

ApplicationContextInitializer
BeanDefinitionRegistryPostProcessor
BeanFactoryPostProcessor
BeanPostProcessor-before
PostConstruct注解
InitializingBean
init-method
BeanPostProcessor-after
ApplicationListener
CommandLineRunner/ApplicationRunner

CommandLineRunner和ApplicationRunner的区别

这两个接口都可以用来在应用启动后执行一些操作,它们的区别主要在参数上:

// CommandLineRunner接收原始的命令行参数
@Component
@Order(2) // 执行顺序
public class MyCommandLineRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {System.out.println("CommandLineRunner执行,参数:" + Arrays.toString(args));// args就是main方法接收的参数数组}
}// ApplicationRunner接收解析后的命令行参数
@Component
@Order(1) // 可以指定执行顺序,数字越小越先执行
public class MyApplicationRunner implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {System.out.println("ApplicationRunner执行");System.out.println("选项参数:" + args.getOptionNames());System.out.println("非选项参数:" + args.getNonOptionArgs());// 获取特定选项的值if (args.containsOption("debug")) {System.out.println("Debug模式已开启");}}
}

使用场景建议:

  • 如果只是简单地需要命令行参数,用CommandLineRunner更简单
  • 如果需要处理复杂的命令行参数(特别是选项参数),用ApplicationRunner更方便
  • 可以通过@Order注解指定多个runner的执行顺序

BeanPostProcessor的作用

BeanPostProcessor是Spring中非常强大的一个扩展点,它可以在Bean初始化前后对Bean进行处理。咱们常用的@Autowired、@Value等注解,都是靠它来实现的。

来看个实用的例子:

@Component
public class PerformanceMonitorBeanPostProcessor implements BeanPostProcessor {private Map<String, Long> beanInitTimes = new HashMap<>();// Bean初始化前调用@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {// 记录初始化开始时间beanInitTimes.put(beanName, System.currentTimeMillis());return bean;}// Bean初始化后调用@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {Long startTime = beanInitTimes.get(beanName);if (startTime != null) {long initTime = System.currentTimeMillis() - startTime;if (initTime > 100) { // 超过100ms的打印警告System.out.println("警告:Bean [" + beanName + "] 初始化耗时:" + initTime + "ms");}beanInitTimes.remove(beanName);}return bean;}
}

这个例子展示了如何监控Bean的初始化耗时,对于排查启动慢的问题非常有用。

Spring中的AutowiredAnnotationBeanPostProcessor就是用来处理@Autowired注解的,它会在Bean初始化前扫描Bean中的@Autowired注解,然后自动注入依赖。

不过要注意,BeanPostProcessor本身也是Bean,所以定义它的时候不能依赖其他Bean的初始化,否则可能会导致循环依赖问题。

自定义ApplicationContextInitializer

ApplicationContextInitializer是在Spring上下文初始化之前执行的,它可以用来对上下文进行一些配置。在做中间件适配时特别有用,比如需要统一设置一些上下文属性。

实现方式很简单:

public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {// 设置一些系统属性System.setProperty("spring.profiles.default", "dev");// 添加一些自定义的环境变量ConfigurableEnvironment environment = applicationContext.getEnvironment();Map<String, Object> myProps = new HashMap<>();myProps.put("myapp.version", "1.0.0");myProps.put("myapp.name", "Demo Application");environment.getPropertySources().addLast(new MapPropertySource("myProps", myProps));// 注册一个BeanFactoryPostProcessorapplicationContext.addBeanFactoryPostProcessor(beanFactory -> {System.out.println("Bean定义数量:" + beanFactory.getBeanDefinitionCount());});System.out.println("自定义ApplicationContextInitializer执行完成");}
}

然后在spring.factories中注册:

org.springframework.context.ApplicationContextInitializer=\
com.example.demo.MyApplicationContextInitializer

或者在启动类中直接注册:

public static void main(String[] args) {new SpringApplicationBuilder(DemoApplication.class).initializers(new MyApplicationContextInitializer()).run(args);
}

这种方式比监听器更早执行,适合做一些最早期的配置工作。

6. 常见问题与调试技巧

启动慢的排查方法

启动慢问题
开启启动日志
logging.level.org.springframework=DEBUG
debug=true
查看自动配置报告
分析耗时点
Bean创建慢
数据源初始化慢
组件扫描慢
使用BeanPostProcessor监控
检查数据库连接
优化包扫描路径
  1. 开启DEBUG日志
# application.properties
logging.level.org.springframework=DEBUG
debug=true
  1. 使用启动分析工具
// 在main方法中添加
public static void main(String[] args) {System.setProperty("spring.startup.logfile", "startup.log");SpringApplication app = new SpringApplication(DemoApplication.class);app.setApplicationStartup(ApplicationStartup.buffering()); // Spring Boot 2.4+app.run(args);
}
  1. 常见的启动慢原因
  • 包扫描范围太大:缩小@ComponentScan的范围
  • 数据源初始化慢:检查数据库连接配置
  • 不必要的自动配置:使用exclude排除不需要的配置
  • Bean初始化慢:优化Bean的初始化逻辑

Bean加载失败的排查

当遇到Bean找不到或者依赖注入失败时,可以这样排查:

  1. 检查包扫描路径
@SpringBootApplication(scanBasePackages = {"com.example.demo", "com.example.common"})
  1. 检查条件注解
@Component
@ConditionalOnProperty(name = "myapp.feature.enabled", havingValue = "true")
public class MyService {// 如果配置不满足,这个Bean不会被创建
}
  1. 查看Bean定义
@Component
public class BeanChecker implements ApplicationContextAware {@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {String[] beanNames = applicationContext.getBeanDefinitionNames();System.out.println("已注册的Bean数量:" + beanNames.length);for (String beanName : beanNames) {System.out.println(beanName);}}
}

总结

好了,今天咱们把Spring Boot的启动原理从头到尾捋了一遍。从main方法开始,到SpringApplication的初始化,再到事件监听、Bean加载,最后讲了几个常用的扩展点。

其实Spring Boot的启动过程虽然复杂,但逻辑很清晰,每个阶段都有明确的职责。理解了这些原理,不仅能帮我们更好地使用Spring Boot,还能在遇到问题时快速定位原因。

最后给几个实战建议:

  1. 调试启动问题时,记得在SpringApplication.run()方法里打个断点,一步步看流程
  2. 想知道哪些自动配置生效了,可以开启debug=true,会打印自动配置报告
  3. 自定义扩展时,注意选择合适的扩展点,别在太早的阶段做太复杂的操作
  4. 生产环境中,尽量不要用反射等方式修改Spring的核心流程,容易出问题
  5. 性能优化时,可以通过BeanPostProcessor监控Bean初始化耗时,找出瓶颈

希望这篇文章能帮到大家,有什么问题欢迎在评论区交流,咱们下次再聊!

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

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

相关文章

windows环境下使用vscode以及相关插件搭建c/c++的编译,调试环境

windows下使用vscode搭建c/c的编译、运行、调试环境&#xff0c;需要注意的是生成的是xxx.exe可执行文件。另外使用的编译器是mingw&#xff0c;也就是windows环境下的GNU。 我参考的网址是&#xff1a;https://zhuanlan.zhihu.com/p/1936443912806962622 文章分为2种环境搭建…

标准瓦片层级0~20,在EPSG:4326坐标系下,每个像素点代表的度数

在 EPSG:4326&#xff08;WGS84经纬度坐标系&#xff09; 下&#xff0c;瓦片层级&#xff08;Zoom Level&#xff09;的分辨率以 度/像素 为单位&#xff0c;其计算遵循 TMS Global Geodetic 规范&#xff08;单位&#xff1a;度&#xff09;。以下是 标准层级 0 至 20 的分辨…

Unity高级剔除技术全解析

目录 ​编辑层级剔除&#xff08;Layer Culling&#xff09;原理详解 代码示例 业务应用场景 距离剔除&#xff08;Distance Culling&#xff09;技术细节 进阶实现 开放世界优化技巧 视口裁剪&#xff08;Viewport Culling&#xff09;多摄像机协作方案 高级应用场景 …

[Linux] Linux文件系统基本管理

目录 识别文件系统和设备 Linux 中设备 Linux 文件系统 查看设备和文件系统 lsblk命令 df命令 du命令 案例&#xff1a;查看根文件系统中哪个文件占用了最大空间 环境准备 查找过程 挂载和卸载文件系统 环境准备 挂载文件系统 卸载文件系统 卸载失败处理 lsof …

如何在 Ubuntu 24.04 Server 或 Desktop 上安装 XFCE

在 Ubuntu 24.04 上更改当前桌面环境或添加新的桌面环境并不是一项艰巨的任务。大多数流行的 Linux 桌面环境,包括 XFCE,都可以通过默认的 Ubuntu 24.04 LTS 系统仓库安装。在本教程中,我们将学习如何使用 Tasksel 工具在 Ubuntu Linux 上安装和配置 XFCE。 访问终端并运行…

linux下用c++11写一个UDP回显程序

需求&#xff1a;1&#xff09;从2个UDP端口接收数据&#xff0c;并在同样的端口回显。echo2&#xff09;多个处理线程&#xff0c;多个发送线程&#xff1b;3&#xff09;使用条件变量唤醒&#xff1b;#include <stack> #include <mutex> #include <atomic>…

MySQL 深分页优化与条件分页:把 OFFSET 换成“游标”,再用覆盖索引抄近路

MySQL 深分页优化与条件分页:把 OFFSET 换成“游标”,再用覆盖索引抄近路 这不是“玄学调优”,而是可复制的方案。本文用可复现的 DDL/造数脚本,演示为什么 OFFSET 越大越慢,如何用 条件游标(Keyset Pagination) 替换它,并配上 覆盖索引。还会教你看 EXPLAIN/EXPLAIN A…

Unity 绳子插件 ObjRope 使用简记

Unity 绳子插件&#xff0c;是一个基于物理的、高度逼真且可交互的绳索模拟解决方案。 其性能良好&#xff0c;能够运行在小游戏平台。 一、插件基本 插件资源商店地址&#xff1a; Obi Rope | Physics | Unity Asset Store 官方文档&#xff08;手册&#xff09;&#xff…

demo 通讯录 + 城市选择器 (字母索引左右联动 ListItemGroup+AlphabetIndexer)笔记

一、城市选择器实现笔记1. 双层 for 循环渲染数据结构interface BKCityContent {initial: string; // 字母索引cityNameList: string[]; // 城市列表 }核心实现// 外层循环&#xff1a;字母分组 - 遍历城市数据&#xff0c;按字母分组显示 ForEach(this.cityContentList, (item…

【总结型】c语言中的位运算

位运算包括 & | ^ ~ << >>按位与 将某些变量中的某些位清0同时保持其他位不变。也可以用来获取变量中的某一位。 例如&#xff1a;将int型变量n低8位全置为0&#xff0c;其余位保持不变。 n n & 0xffffff00 如何判断一个int型变量n的第七位。 n & 0x8…

如何在FastAPI中玩转APScheduler,实现动态定时任务的魔法?

url: /posts/4fb9e30bb20956319c783e21897a667a/ title: 如何在FastAPI中玩转APScheduler,实现动态定时任务的魔法? date: 2025-08-16T01:14:26+08:00 lastmod: 2025-08-16T01:14:26+08:00 author: cmdragon summary: APScheduler是Python中强大的任务调度库,支持任务持久化…

GitHub的简单使用方法----(5)

最后一篇简单讲讲git管理远程仓库 1.目的 备份&#xff0c;实现代码共享集中化管理 &#xff08;将本地仓库同步到git远程仓库中&#xff09; git clone 仓库地址 以下图为示例&#xff0c;我打开了一个别人的项目仓库&#xff0c;点击code能看到仓库地址 等待完成即可 如…

C++ STL-string类底层实现

摘要&#xff1a; 本文实现了一个简易的string类&#xff0c;主要包含以下功能&#xff1a; 1. 默认成员函数&#xff1a;构造函数&#xff08;默认/参数化&#xff09;、拷贝构造、赋值重载和析构函数&#xff0c;采用深拷贝避免内存问题&#xff1b; 2. 迭代器支持&#xff1…

【LeetCode每日一题】

每日一题3. 无重复字符的最长子串题目总体思路代码1.两数之和题目总体思路代码15. 三数之和题目总体思路代码2025.8.153. 无重复字符的最长子串 题目 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长 子串 的长度。 示例 1: 输入: s “abcabcbb” 输出: 3…

sharding-jdbc读写分离配置

一主两从&#xff0c;爆红是正常的&#xff0c;不知为啥 spring:shardingsphere:datasource:names: ds_master,ds_s1,ds_s2ds_master:type: com.zaxxer.hikari.HikariDataSourcedriverClassName: com.mysql.jdbc.DriverjdbcUrl: jdbc:mysql://192.168.135.100:3306/gmall_produ…

【大模型核心技术】Dify 入门教程

文章目录一、Dify 是什么二、安装与部署2.1 云端 SaaS 版&#xff08;快速入门&#xff09;2.2 私有化部署&#xff08;企业级方案&#xff09;三、界面导航与核心模块3.1 控制台概览3.2 核心功能模块详解3.2.1 知识库&#xff08;RAG 引擎&#xff09;3.2.2 工作流编排3.2.3 模…

homebrew 1

文章目录brew(1) – macOS&#xff08;或 Linux&#xff09;上缺失的包管理器概要描述术语表基本命令install *formula*uninstall *formula*listsearch \[*text*|/*text*/]命令alias \[--edit] \[*alias*|*alias**command*]analytics \[*subcommand*]autoremove \[--dry-run]bu…

设计索引的原则有哪些?

MySQL 索引设计的核心原则是 在查询性能与存储成本之间取得平衡。以下是经过实践验证的 10 大设计原则及具体实现策略&#xff1a;一、基础原则原则说明示例/反例1. 高频查询优先为 WHERE、JOIN、ORDER BY、GROUP BY 频繁出现的列建索引✅ SELECT * FROM orders WHERE user_id1…

使用影刀RPA实现快递信息抓取

最近公司项目有个需求&#xff0c;要求抓取快递单号快递信息&#xff0c;比如签收地点、签收日期等。该项目对应的快递查询网站是一个国外的网站&#xff0c;他们有专门的快递平台可以用于查询。该平台提供了快递接口进行查询&#xff0c;但需要付费。同时也提供了免费的查询窗…

蚁剑--安装、使用

用途限制声明&#xff0c;本文仅用于网络安全技术研究、教育与知识分享。文中涉及的渗透测试方法与工具&#xff0c;严禁用于未经授权的网络攻击、数据窃取或任何违法活动。任何因不当使用本文内容导致的法律后果&#xff0c;作者及发布平台不承担任何责任。渗透测试涉及复杂技…