【SpringBoot】SpringBoot核心启动流程源码解析

news2024/11/13 9:23:21

SpringBoot总体流程

当我们启动一个SpringBoot程序的时候,只需要一个main方法就可以启动,但是对于其中流程时如何执行的,以及如何调用spring的IOC和AOP机制,本篇带着这个问题来整体体系化的梳理下流程。

@SpringBootApplication
public class SpringBootApp {

	public static void main(String[] args) {
		//  test
		SpringApplication.run(SpringBootApp.class);
	}
}

实际调用的是如下,也就是run方法

	public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
		// 调用重载的run方法,将传递的Class对象封装为了一个数组
		return run(new Class<?>[] { primarySource }, args);
	}
	
	public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		// 创建了一个SpringApplication对象,并调用其run方法
		// 1.先看下构造方法中的逻辑
		// 2.然后再看run方法的逻辑
		return new SpringApplication(primarySources).run(args);
	}

基本可以分为两块,一个是构造方法,一个是run。

SpringBootApplication 构造方法

构造方法中,其实主要完成的工作,设置主启动类 primarySources,判断当前应用类型 servlet类型。通过公共方法,getSpringFactoriesInstances 进行获取目标类。然后设置到对应的初始化器以及监听器对象中。
然后通过堆栈信息获取主启动类

	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		// 传递的resourceLoader为null
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		// 记录主方法的配置类名称 用set进行去重复
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		// 记录当前项目的类型 servlet类型
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		// 加载配置在spring.factories文件中的ApplicationContextInitializer对应的类型并实例化
		// 并将加载的数据存储在了 initializers 成员变量中。
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 初始化监听器 并将加载的监听器实例对象存储在了listeners成员变量中
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 反推main方法所在的Class对象 并记录在了mainApplicationClass对象中
		this.mainApplicationClass = deduceMainApplicationClass();
	}

其他的比较简单,主要分析下getSpringFactoriesInstances()方法

getSpringFactoriesInstances

整体流程,其实就是根据入参,从spring.factorties中 根据key获取对应的权限定类集合。
在这里插入图片描述

getSpringFactoriesInstances(ApplicationContextInitializer.class));

    // 根据入参类型 返回一个集合对象
	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
		// 获取当前上下文类加载器 默认是app类加载
		ClassLoader classLoader = getClassLoader();
		// 获取到的扩展类名存入set集合中防止重复
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		// 创建扩展点实例
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
		// 进行排序
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

SpringFactoriesLoader.loadFactoryNames

通过调用loadSpringFactories 从META-INF/spring.factories处读取相应配置文件,key是对应的 factoryTypeName value是多个类名。

	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
		// 根据Class获取名称
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
	}

	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		// 先查询缓存是否有 第一次进来是空
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
			// 判断classLoader是否为空 
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryTypeName, factoryImplementationName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}

在这里插入图片描述
在这里插入图片描述

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

前面获取到所有类,然后进行反射进行实例化,添加到instances 集合中 返回。

	private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
			ClassLoader classLoader, Object[] args, Set<String> names) {
		// 创建实例的集合容器
		List<T> instances = new ArrayList<>(names.size());
		for (String name : names) {
			try {
				// 通过反射将扩展点实例实例化
				Class<?> instanceClass = ClassUtils.forName(name, classLoader);
				Assert.isAssignable(type, instanceClass);
				Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
				T instance = (T) BeanUtils.instantiateClass(constructor, args);
				instances.add(instance);
			}
			catch (Throwable ex) {
				throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
			}
		}
		return instances;
	}

所以这个过程有设置了两个 ApplicationContextInitializer、ApplicationListener 会对配置中的类进行初始化。

构造方法就执行如下方法。
在这里插入图片描述

run方法

run方法比较核心,其中主要包含初始化环境变量、事件发布等,应用参数设置、打印banner、创建上下文对象,异常处理器、刷新前操作、刷新操作、刷新后操作、监听器运行等。好了,我们分析下主要的流程。

	public ConfigurableApplicationContext run(String... args) {
		// 创建一个任务执行观察器
		StopWatch stopWatch = new StopWatch();
		// 开始执行记录执行时间
		stopWatch.start();
		// 声明 ConfigurableApplicationContext 对象
		ConfigurableApplicationContext context = null;
		// 声明集合容器用来存储 SpringBootExceptionReporter 启动错误的回调接口
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		// 设置了一个名为java.awt.headless的系统属性
		// 其实是想设置该应用程序,即使没有检测到显示器,也允许其启动.
		//对于服务器来说,是不需要显示器的,所以要这样设置.
		configureHeadlessProperty();
		// 获取 SpringApplicationRunListener 加载的是 EventPublishingRunListener
		// 获取启动时的监听器---》 事件发布器  发布相关事件的  11个监听器 谁去发布事件?
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 触发启动事件  发布 starting 事件 --》 那么监听starting事件的监听器就会触发
		listeners.starting();
		try {
			// 构造一个应用程序的参数持有类 java -jar xx.jar a
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 创建并配置环境
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			// 配置需要忽略的BeanInfo信息
			configureIgnoreBeanInfo(environment);
			// 输出的Banner信息
			Banner printedBanner = printBanner(environment);
			// 创建应用上下文对象 AnnotationConfigServletWebServerApplicationContext
			context = createApplicationContext();
			// 加载配置的启动异常处理器
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			// 刷新前操作
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 刷新应用上下文 完成Spring容器的初始化
			refreshContext(context);
			// 刷新后操作 功能拓展进行使用
			afterRefresh(context, applicationArguments);
			// 结束记录启动时间
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			// 事件广播 启动完成了
			listeners.started(context);
			callRunners(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);
		}
		// 返回上下文对象--> Spring容器对象
		return context;
	}

getRunListeners 获取监听器

这里会获取spring.factories 的中的SpringApplicationRunListener 类,然后实例化。并且调用start()进行启动执行。这里以 LoggingApplicationListener 为例子,执行了 日志的操作。

	private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger,
				// getSpringFactoriesInstances 读取spring.factories 文件中key 为 SpringApplicationRunListener 类型的
				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
	}

	void starting() {
		// 发布器 EventPulishingRunListener
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.starting();
		}
	}

	public void starting() {
		// System.out.println("EventPublishingRunListener ----》starting ");
		this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
	}

	@Override
	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		Executor executor = getTaskExecutor();
		for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			if (executor != null) {
				executor.execute(() -> invokeListener(listener, event));
			}
			else {
				invokeListener(listener, event);
			}
		}
	}

	doInvokeListener(listener, event);	

	private void onApplicationStartingEvent(ApplicationStartingEvent event) {
		this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
		this.loggingSystem.beforeInitialize();
	}
		

prepareEnvironment 环境准备

	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		// 创建并且配置 Environment servlet context config \ systemt config等信息
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		// 配置PropertySources和activeProfiles
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		// 加载 configurationProperties 配置信息
		ConfigurationPropertySources.attach(environment);
		// 在配置环境信息之前发布事件 配置相关 这里会在执行监听器的 environmentPrepared
		listeners.environmentPrepared(environment);
		// 把相关的配置信息绑定到Spring容器中
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		// 配置PropertySources对它自己的递归依赖
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

会再次执行

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		// 加载系统提供的环境配置的后置处理器
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		// 添加自身即 ConfigFileApplicationListener 为后置处理器
		postProcessors.add(this);
		// 原来有4个 现在加了一个需要重新排序
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			// 系统提供那4个不是重点,重点是 ConfigFileApplicationListener 中的这个方法处理
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}

在这里插入图片描述

createApplicationContext 创建容器

这里比较简单根据webApplicationType的类型,选择创建对应的容器对象,这里是 AnnotationConfigServletWebServerApplicationContext 并且进行实例化。调用默认构造器进行初始化了两个对象,AnnotatedBeanDefinitionReader、ClassPathBeanDefinitionScanner 后续会使用到

	protected ConfigurableApplicationContext createApplicationContext() {
		Class<?> contextClass = this.applicationContextClass;
		if (contextClass == null) {
			try {
				switch (this.webApplicationType) {
				case SERVLET: // AnnotationConfigServletWebServerApplicationContext
					contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
				}
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
			}
		}
		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

	// 在实例化的时候 调用构造方法 创建两个对象 reader和scanner
	public AnnotationConfigServletWebServerApplicationContext() {
		//
		this.reader = new AnnotatedBeanDefinitionReader(this);
		//
		this.scanner = new ClassPathBeanDefinitionScanner(this);
	}

Spring容器前置处理

将启动类注入容器,为后续开启自动化配置奠定基础。
这里其实主要做的两件事,一个是执行吃书画方法,将容器中的Initializer执行初始化操作,以及将主类,通过注解的方式添加到spring的beanDefinitionMap中。

	// 1.设置环境属性
	// 2.设置postprocess
	// 3.启动类config信息 准备上下文环境工作
	private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
			SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
		// 设置环境变量属性
		context.setEnvironment(environment);
		// 转换方法
		postProcessApplicationContext(context);
		// 应用初始化器 核心⭐️ 执行容器中的ApplicationContextInitializer(包括 spring.factories和自定义的实例)
		applyInitializers(context);
		listeners.contextPrepared(context);
		if (this.logStartupInfo) {
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);
		}
		// Add boot specific singleton beans
		// 获取bean工厂
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		// 注册 springApplicationArguments 参数对象
		beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
		if (printedBanner != null) {
			// 注册banner
			beanFactory.registerSingleton("springBootBanner", printedBanner);
		}
		// spring底层使用就是这个
		if (beanFactory instanceof DefaultListableBeanFactory) {
			((DefaultListableBeanFactory) beanFactory)
					.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
		}
		if (this.lazyInitialization) {
			context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
		}
		// Load the sources
		Set<Object> sources = getAllSources();
		Assert.notEmpty(sources, "Sources must not be empty");
		// 核心⭐️
		load(context, sources.toArray(new Object[0]));
		listeners.contextLoaded(context);
	}
	protected void applyInitializers(ConfigurableApplicationContext context) {
		// 遍历
		for (ApplicationContextInitializer initializer : getInitializers()) {
			Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
					ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
			// 执行初始化方法
			initializer.initialize(context);
		}
	}
 
 	// ConfigurationWarningsApplicationContextInitializer 执行了将ConfigurationWarningsPostProcessor 添加到容器中 BFPP 等待容器进行刷新的BFPP的时候执行。
	public void initialize(ConfigurableApplicationContext context) {
		// 加入对象
		context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
	}

	loader.load();

	int load() {
		int count = 0;
		for (Object source : this.sources) {
			count += load(source);
		}
		return count;
	}

	// 加载
	private int load(Object source) {
		Assert.notNull(source, "Source must not be null");
		//类
		if (source instanceof Class<?>) {
			return load((Class<?>) source);
		}
		//资源
		if (source instanceof Resource) {
			return load((Resource) source);
		}
		//包
		if (source instanceof Package) {
			return load((Package) source);
		}
		//
		if (source instanceof CharSequence) {
			return load((CharSequence) source);
		}
		throw new IllegalArgumentException("Invalid source type " + source.getClass());
	}

		if (isComponent(source)) { // 是否包含compoent注解
			// 注册SpringBootApp类到ioc 容器中
			this.annotatedReader.register(source);
			return 1;
		}

				this.beanDefinitionMap.put(beanName, beanDefinition);
				this.beanDefinitionNames.add(beanName);

在这里插入图片描述

在这里插入图片描述

refreshContext(context);

这里其实就是调用spring的refresh() 由于整体过于庞大,先不讲解。后续在补坑。

小总结

在这里插入图片描述

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

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

相关文章

哥斯拉短视频:成都柏煜文化传媒有限公司

哥斯拉短视频&#xff1a;巨兽传奇的视听盛宴 在短视频的海洋中&#xff0c;成都柏煜文化传媒有限公司 有一种特殊的存在总能吸引人们的目光&#xff0c;那就是以哥斯拉为主题的短视频。这些视频以震撼的视觉效果、扣人 ​心弦的剧情和独特的怪兽文化&#xff0c;为我们呈现了…

mysql5.7安装使用

mysql5.7安装包&#xff1a;百度网盘 提取码: 0000 一、 安装步骤 双击安装文件 选择我接受许可条款–Next 选择自定义安装&#xff0c;下一步 选择电脑对应的系统版本后(我的系统是64位)&#xff0c;点击中间的右箭头&#xff0c;选择Next 选择安装路径–Next 执行…

Android隐藏状态栏和修改状态栏颜色_亲测有效

本文记录了隐藏状态栏和修改状态栏颜色以及电量、WiFi标志等内容的模式显示&#xff0c;亲测有效。 1、隐藏屏幕状态栏 public void hideStatusBar(BaseActivity activity) {Window window activity.getWindow();//没有这一行无效window.addFlags(WindowManager.LayoutParam…

瑞数(rs6)接口以及源码

测试代码截图如下&#xff1a;调用接口即可直接用 需要dd 有想要学习教程的也能够找我。 如有需求&#xff0c;欢迎&#xff0b;我绿泡泡。 期待你的加入&#xff01;

Java | Leetcode Java题解之第189题轮转数组

题目&#xff1a; 题解&#xff1a; class Solution {public void rotate(int[] nums, int k) {k % nums.length;reverse(nums, 0, nums.length - 1);reverse(nums, 0, k - 1);reverse(nums, k, nums.length - 1);}public void reverse(int[] nums, int start, int end) {whil…

ZYNQ MPSOC烧写问题记录

1、如果带有ARM代码&#xff0c;则ZYNQ MPSOC烧写的烧写是通过ViTIS进行的&#xff0c;通过JTAG模式进行烧写&#xff0c;如下图的PS_MODEx配置成0000&#xff0c;这个只与硬件相关&#xff0c;硬件拉高拉低。 2、如果不带ARM代码&#xff0c;则烧写过程与前版本一致。

PaddleOCR的快速使用

一、简介 PaddleOCR 旨在打造一套丰富、领先、且实用的 OCR 工具库&#xff0c;助力开发者训练出更好的模型&#xff0c;并应用落地。 支持多种 OCR 相关前沿算法&#xff0c;在此基础上打造产业级特色模型PP-OCR、PP-Structure和PP-ChatOCRv2&#xff0c;并打通数据生产、模型…

vue-cli 搭建项目

创建 router 目录 在一个.js文件中添加 打开外部命令 打开外部命令后&#xff0c;在指令栏输入npm i vue-router3.5.3 &#xff0c;等待下载 下载完成后 在 main.js 中配置路由 输入这些后&#xff0c;基本的配置就实现了 最后进行测试&#xff0c;验证是否配置 或者打开外部命…

解决指南:如何应对错误代码 0x80070643

在使用Windows操作系统过程中&#xff0c;用户可能会遭遇各种错误代码&#xff0c;其中错误 0x80070643是比较常见的一种。这个错误通常在安装更新或某些软件时发生&#xff0c;尤其是在微软的Windows Defender或其他Microsoft安全产品以及.NET Framework更新过程中更为常见。本…

Linux的fwrite函数

函数原型: 向文件fp中写入writeBuff里面的内容 int fwrite(void*buffer&#xff0c;intsize&#xff0c;intcount&#xff0c;FILE*fp) /* * description : 对已打开的流进行写入数据块 * param ‐ ptr &#xff1a;指向 数据块的指针 * param ‐ size &#xff1a;指定…

19.《C语言》——【如何理解static和extern?】

&#x1f387;开场语 亲爱的读者&#xff0c;大家好&#xff01;我是一名正在学习编程的高校生。在这个博客里&#xff0c;我将和大家一起探讨编程技巧、分享实用工具&#xff0c;并交流学习心得。希望通过我的博客&#xff0c;你能学到有用的知识&#xff0c;提高自己的技能&a…

操作系统速成笔记五

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、前言&#x1f680;&#x1f680;&#x1f680;二、正文☀️☀️☀️1.内存映射文件&#xff08;1&#xff09;方便程序员访问文件数据&#xff08;2&#xff09…

小红书多账号管理平台哪个好用?可以快速监测多个小红书账号的数据吗?

随着品牌营销战线的不断扩展&#xff0c;小红书已经成为企业和个人品牌竞相展示的舞台。但是&#xff0c;随之而来的多账号管理问题也让众多运营者头疼不已。一个优秀的多账号管理平台&#xff0c;能让你事半功倍&#xff0c;轻松监控和分析账号数据。 如今&#xff0c;市面上出…

【proteus 51单片机入门】8*8led点阵

文章目录 前言如何点亮led点阵仿真图代码点亮led核心代码解析 爱心代码 滚动总结 前言 在嵌入式系统的开发中&#xff0c;LED点阵显示器是一种常见的显示设备&#xff0c;它可以用来显示各种图形和文字&#xff0c;为用户提供直观的信息反馈。本文将介绍如何使用Proteus软件和…

亿发微商城:从社交流量到生意增量,拓客+变现双收益

亿发微商城提供私域经营必备的系统 、服务和技术支持等全套解决方案。省去开发成本&#xff0c;还原品牌调性&#xff0c;打通社交渠道&#xff0c;经营私域会员&#xff0c;带来传统电商和线下门店之外的生意增量。

【linux/shell案例实战】解决Linux和Windows的换行符CRLF和LF问题

目录 一.什么是Linux 和 Windows 的换行符 CRLF 和 LF 二.使用Linux 中命令 dos2unix 和 unix2dos 实现CRLF 和LF的转换 三.使用 windows 中的代码编辑器实现 CRLF 和 LF 的转换&#xff08;Notepad&#xff09; 一.什么是Linux 和 Windows 的换行符 CRLF 和 LF CR是Carria…

<sa8650>QCX ID16_UsecaseRawLiteAuto 使用详解

<sa8650>QCX ID16_UsecaseRawLiteAuto 使用详解 一、前言二、ID16_UsecaseRawLiteAuto拓扑图三、UsecaseRawLiteAuto拓扑图 解析3.1 camxUsecaseRawLiteAuto.xml3.2 camxRawLiteAuto.xml四、测试一、前言 我们在使用QCX时,如果由于使用的摄像头自带了ISP,那么可能不需要使…

iOS开发中用到的自定义UI库

文章目录 前言cell 左右滑动菜单日历组件仿QQ 侧滑抽屉仿探探、陌陌的卡牌滑动库头部缩放视图自定义UITabbar刮刮乐广告横幅 前言 本文中的UI组件&#xff0c;是作者在移动应用开发中都用到过的。 确实&#xff0c;找到对的三方库可以快速帮助我们构建App, 极大程度上提高了生…

吉利银河L6(官方小订送的3M) 对比 威固vk70+ks15

吉利送的号称价值2000的3M效果 撕膜重贴 威固vk70ks15 之后的效果 // 忘记测反射的热量了 可以验证金属膜是反射热而不是吸热 金属膜 手机GPS还能用吗 亲测 能用 太阳能总阻隔率 3M貌似20%出头 威固前档55% 侧后挡高一点不超过60% 夏天真实太阳发热能量 即阻隔率55%到60% …

[leetcode hot 150]第一百二十二题,买卖股票的最佳时机Ⅱ

题目&#xff1a; 给你一个整数数组 prices &#xff0c;其中 prices[i] 表示某支股票第 i 天的价格。 在每一天&#xff0c;你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买&#xff0c;然后在 同一天 出售。 返回 你能获得的 最大…