springboot之配置文件加载

news2024/11/27 2:43:30

  springboot启动流程参考。Springboot总结。本内容主要解析里面的配置文件的加载过程。

springboot资源加载

入口。SpringApplication#run

  我们知道,run方法是构建容器的过程。里面有一个方法:prepareEnvironment。用于构建环境组件Environment,发布环境准备事件,有相关监听器完成资源的加载。

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 看这里看这里。
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner 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);
			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);
		}
		return context;
	}

  prepareEnvironment的源码如下:
  1. 根据webApplicationType,实例化对应的environment对象。environment的父类AbstractEnvironment实例化会调用customizePropertySources方法完成一些资源文件的加载。customizePropertySources的落地实现在对应environment的实现类中
  2. main函数入参配置。增加一个name为configurationProperties的PropertySources。这个包含上面加载的所有propertySource。
  3. springApplicationRunListener发布environmentPrepared事件。处理事件的监听器有以下几种。
springboot处理environmentPrepared的事件

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment 创建一个Environment对象。
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

ConfigFileApplicationListener监听器对资源的加载

  ConfigFileApplicationListener主要完成对application相关的资源加载。我们重点看这个监听器。对其他几个感兴趣的,可以自己debug看看代码。

    // ConfigFileApplicationListener#onApplicationEvent
	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}
	
   List<EnvironmentPostProcessor> loadPostProcessors() {
		return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
	}

  由源码我们可以看出,对事件的处理是调用的onApplicationEnvironmentPreparedEvent方法。其中loadPostProcessors方法是通过spi机制加载spring.factories文件获取key为:EnvironmentPostProcessor.class的对象集合。这些对象都实现了EnvironmentPostProcessor接口的postProcessEnvironment接口。ConfigFileApplicationListener这个类本身也实现了EnvironmentPostProcessor接口。我们接着看ConfigFileApplicationListener#EnvironmentPostProcessor。
  跟进ConfigFileApplicationListener#EnvironmentPostProcessor。主要逻辑来着load()方法的执行。

void load() {
			FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
					(defaultProperties) -> {
						this.profiles = new LinkedList<>();
						this.processedProfiles = new LinkedList<>();
						this.activatedProfiles = false;
						this.loaded = new LinkedHashMap<>();
						initializeProfiles();
						while (!this.profiles.isEmpty()) {
							Profile profile = this.profiles.poll();
							if (isDefaultProfile(profile)) {
								addProfileToEnvironment(profile.getName());
							}
							// 看这里看这里
							load(profile, this::getPositiveProfileFilter,
									addToLoaded(MutablePropertySources::addLast, false));
							this.processedProfiles.add(profile);
						}
						// 看这里看这里
						load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
						addLoadedPropertySources();
						applyActiveProfiles(defaultProperties);
					});
		}
		private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
		    // getSearchLocations方法获取扫描文件的目录地址。getSearchNames获取查询文件的名称。
			getSearchLocations().forEach((location) -> {
				boolean isDirectory = location.endsWith("/");
				Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
				names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
			});
		}

  我们重点看getSearchLocations方法和getSearchNames方法。
  1. getSearchLocations方法主要获取查询资源文件的目录信息。这也是我们配置文件加载的先后顺序。
springboot资源记载目录
  2. getSearchNames获取需要加载的文件名称。当environment中不存在key:spring.config.name时,获取默认name的名称:application。如果environment中存在key:spring.config.name时,获取的name为对应的value。当前获取到的name为appplicaition。

private Set<String> getSearchNames() {
            // CONFIG_NAME_PROPERTY: spring.config.name
			if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
				String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
				Set<String> names = asResolvedSet(property, null);
				names.forEach(this::assertValidConfigName);
				return names;
			}
			// DEFAULT_NAMES: application
			return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
		}

  接着遍历propertySourceLoader,拼接文件名称,在扫描的目录下加载对应的文件。propertySourceLoader也是通过spi机制加载spring.factories文件获取key为:PropertySourceLoader.class的对象集合,我们可以看到有两个加载类。这个集合是一个List集合,所以根据顺序会先执行PropertiesPropertySourceLoader,然后执行YamlPropertySourceLoader。
  1. PropertiesPropertySourceLoader处理后缀名为"properties", “xml"的文件。
  2. YamlPropertySourceLoader处理后缀名为"yml”, "yaml"的文件。
在这里插入图片描述
  跟进debug,我们可以发现,加载对应的资源名称封装为:“applicationConfig: [” + getLocationName(location, resource) + “]”;
在这里插入图片描述
在这里插入图片描述
  加载到资源封装为document后,会调用consume函数接口处理。主要做的就是将解析获取到的资源文件封装为OriginTrackedMapPropertySource并放入到environment中。

private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
				boolean checkForExisting) {
			return (profile, document) -> {
				if (checkForExisting) {
					for (MutablePropertySources merged : this.loaded.values()) {
						if (merged.contains(document.getPropertySource().getName())) {
							return;
						}
					}
				}
				MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
						(k) -> new MutablePropertySources());
				addMethod.accept(merged, document.getPropertySource());
			};
		}
		
	/**
	 * Add the given property source object with lowest precedence.
	 * 这个是addMethod对应的处理。addMethod也是consume函数接口
	 */
	public void addLast(PropertySource<?> propertySource) {
		synchronized (this.propertySourceList) {
			removeIfPresent(propertySource);
			// 加入到environment中。
			this.propertySourceList.add(propertySource);
		}
	}

在这里插入图片描述
  两个配置文件中都有server.port配置。解析完的时候我们通过debug执行看看获取的配置是哪个。结果很明显,是获取到的applicaiton.properties的配置。getProperties查询是遍历propertySources。查询到返回。按照这个逻辑的话。配置文件优先级应该是(未验证):.properties > xml > yml > yaml。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

基于springboot的springcloud资源加载

  基于springboot的springcloud服务,在处理environmentPrepared事件(这个是方法名,事件名称为ApplicationEnvironmentPreparedEvent)增加了两个监听器在spring-cloud-context包中。我们重点看看第一个BootstrapApplicationListener。
在这里插入图片描述

BootstrapApplicationListener

  首先,BootstrapApplicationListener在处理ApplicationEnvironmentPreparedEvent事件的时候,首先从环境中判断key:spring.cloud.bootstrap.enabled对应的value是否为true,不配置的话默认值为true。其次,从环境中获取key:spring.cloud.bootstrap.name对应的value,不配置的话默认为bootstrap。
  构建一个bootstrapServiceContext对象。这个一个容器对象AnnotationConfigApplicationContext。
  对当前应用中增加监听器CloseContextOnFailureApplicationListener。
  对当前应用springapplication进行相关配置。增加一些容器初始化类。

// BootstrapApplicationListener#onApplicationEvent
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
		ConfigurableEnvironment environment = event.getEnvironment();
		// 校验是否开启bootstrap的引导类
		if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
				true)) {
			return;
		}
		// don't listen to events in a bootstrap context
		if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
			return;
		}
		ConfigurableApplicationContext context = null;
		String configName = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
		for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
				.getInitializers()) {
			if (initializer instanceof ParentContextApplicationContextInitializer) {
				context = findBootstrapContext(
						(ParentContextApplicationContextInitializer) initializer,
						configName);
			}
		}
		if (context == null) {
			// 构建容器对象AnnotationConfigApplicationContext。里面逻辑复杂,见下文分析。
			context = bootstrapServiceContext(environment, event.getSpringApplication(),
					configName);
			// 对当前应用中增加监听器CloseContextOnFailureApplicationListener。
			event.getSpringApplication()
					.addListeners(new CloseContextOnFailureApplicationListener(context));
		}
		
		// 根据新创建的容器对当前应用springapplication进行相关配置。增加一些容器初始化类。
		apply(context, event.getSpringApplication(), environment);
	}

构建bootstrapServiceContext对象(容器对象AnnotationConfigApplicationContext)

  构建容器对象就是创建一个新的AnnotationConfigApplicationContext。
  1. 构建一个空的environment。类型是StandardEnvironment
  2. 获取当前应用中key为:spring.cloud.bootstrap.location,spring.cloud.bootstrap.additional-location的路径。如果存在,下面代码会根据这个地址去加载文件。我们没有配置,默认是""。
  3. 构建Map对象,里面有spring.config.name的配置。上文讲解springboot解析的内容中,在遍历资源目录的时候,获取文件名,取得就是这个key的值。相当于这里配置了,那么就不会取applicaiton了,取的是bootstrap。将Map对象封装成MapPropertySource对象,添加到bootstrapServiceContext的环境对象中。
  4. 将当前环境对象中的propertySource对象,筛选添加到bootstrapServiceContext的环境对象中。这种类型StubPropertySource的不添加。StubPropertySource对应的是servlet相关的资源文件。
  5. 通过SpringApplicationBuild构建器构造SpringApplicaiton对象。webApplicaitonType为none。
  6. 设置BootstrapServiceContext容器id为bootstrap。给当前应用的SpringApplciaiton中添加容器初始化处理类AncestorInitializer。AncestorInitializer里面包含了bootstrap容器。会在后续当前应用的容器初始化的时候,设置容器的父容器为boootstrap容器。
  7. bootstrap容器中的环境中移除name为bootsratp的propertySource。里面存储的就是spring.config.name配置,还有其他配置的MapPropertySource。移除后,后面的资源加载获取的文件名默认就是application了。
  8. 合并bootstrap容器中的Environment的propertySource到当前应用的Environment中。name为:springCloudDefaultProperties。sources为bootstrap容器中Environment的propertySource集合。

	private ConfigurableApplicationContext bootstrapServiceContext(
	       // 构建一个空的environment
			ConfigurableEnvironment environment, final SpringApplication application,
			String configName) {
		StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
		MutablePropertySources bootstrapProperties = bootstrapEnvironment
				.getPropertySources();
		for (PropertySource<?> source : bootstrapProperties) {
			bootstrapProperties.remove(source.getName());
		}
		String configLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
		String configAdditionalLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
		// 构建Map对象,后面会放入到Envrionment中。传进来的configName为bootstrap。
		Map<String, Object> bootstrapMap = new HashMap<>();
		bootstrapMap.put("spring.config.name", configName);
		// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap
		// will fail
		// force the environment to use none, because if though it is set below in the
		// builder
		// the environment overrides it
		bootstrapMap.put("spring.main.web-application-type", "none");
		if (StringUtils.hasText(configLocation)) {
			bootstrapMap.put("spring.config.location", configLocation);
		}
		if (StringUtils.hasText(configAdditionalLocation)) {
			bootstrapMap.put("spring.config.additional-location",
					configAdditionalLocation);
		}
		bootstrapProperties.addFirst(
				new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
		// 将当前环境对象中的propertySource对象,筛选添加到bootstrapServiceContext的环境对象中。
		for (PropertySource<?> source : environment.getPropertySources()) {
			if (source instanceof StubPropertySource) {
				continue;
			}
			bootstrapProperties.addLast(source);
		}
		// TODO: is it possible or sensible to share a ResourceLoader?
		// 构建SpringApplication对象
		SpringApplicationBuilder builder = new SpringApplicationBuilder()
				.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
				.environment(bootstrapEnvironment)
				// Don't use the default properties in this builder
				.registerShutdownHook(false).logStartupInfo(false)
				.web(WebApplicationType.NONE);
		final SpringApplication builderApplication = builder.application();
		if (builderApplication.getMainApplicationClass() == null) {
			// gh_425:
			// SpringApplication cannot deduce the MainApplicationClass here
			// if it is booted from SpringBootServletInitializer due to the
			// absense of the "main" method in stackTraces.
			// But luckily this method's second parameter "application" here
			// carries the real MainApplicationClass which has been explicitly
			// set by SpringBootServletInitializer itself already.
			builder.main(application.getMainApplicationClass());
		}
		if (environment.getPropertySources().contains("refreshArgs")) {
			// If we are doing a context refresh, really we only want to refresh the
			// Environment, and there are some toxic listeners (like the
			// LoggingApplicationListener) that affect global static state, so we need a
			// way to switch those off.
			builderApplication
					.setListeners(filterListeners(builderApplication.getListeners()));
		}
		builder.sources(BootstrapImportSelectorConfiguration.class);
		final ConfigurableApplicationContext context = builder.run();
		// gh-214 using spring.application.name=bootstrap to set the context id via
		// `ContextIdApplicationContextInitializer` prevents apps from getting the actual
		// spring.application.name
		// during the bootstrap phase.
		// 设置容器的id为bootstrap
		context.setId("bootstrap");
		// Make the bootstrap context a parent of the app context
		// 给当前应用的SpringApplication中添加ApplicationContextInitializer监听器。AncestorInitializer
		addAncestorInitializer(application, context);
		// It only has properties in it now that we don't want in the parent so remove
		// it (and it will be added back later)
		// bootstrap容器中的环境中移除name为bootsratp的propertySource。里面存储的就是spring.config.name配置,还有其他配置的MapPropertySource。
		bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
		// 合并bootstrap容器中的Environment的propertySource到当前应用的Environment中。name为:springCloudDefaultProperties。sources为bootstrap容器中Environment的propertySource集合。
		mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
		return context;
	}

  以下是bootstrap容器解析后的环境。
在这里插入图片描述

当前应用继续解析

  后续监听器继续解析。再记载文件的时候,当前的配置文件名称就是applicaiton了。
  再初始化当前应用的容器的时候,会设置父容器为bootstrap的容器。新的环境内容为。里面顺序调整应该是容器初始化处理类里面做的操作(暂时没看)。
在这里插入图片描述

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

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

相关文章

【ArcGIS Pro二次开发】(48):三调土地利用现状分类面积汇总统计

之前做了一个三调三大类面积统计&#xff0c;有小伙伴反映太粗糙&#xff0c;想要一个完整的地类面积汇总表。 【ArcGIS Pro二次开发】(35)&#xff1a;三调三大类面积统计 本质上并没有多少难度&#xff0c;之前也做过类似的用地用海汇总表&#xff0c;于是拿出来改一改就好了…

3D开发工具HOOPS 2023 SP2更新:增加了SOLIDWORKS贴花支持!

HOOPS SDK是全球领先开发商TechSoft 3D旗下的原生产品&#xff0c;专注于Web端、桌面端、移动端3D工程应用程序的开发。长期以来&#xff0c;HOOPS通过卓越的3D技术&#xff0c;帮助全球600多家知名客户推动3D软件创新&#xff0c;这些客户包括SolidWorks、SIEMENS、Oracle、Ar…

几款好用软件,嗐,朋友推荐的,真香

1、git加速 官网&#xff1a;https://steampp.net/ 2、gif 截图 官网&#xff1a;https://www.screentogif.com/

scss 预处理器自定义ui框架(bem架构)

BEM架构 bem架构 它是一种css架构 oocss 实现的一种 &#xff08;面向对象css&#xff09; &#xff0c;BEM实际上是block、element、modifier的缩写&#xff0c;分别为块层、元素层、修饰符层&#xff0c;element UI 也使用的是这种架构 BEM 命名约定的模式是&#xff1a; …

kaggle,球员接触检测

比赛链接 比赛目标 检测球员在NFL橄榄球比赛中所经历的外部接触。你将使用视频和球员跟踪数据来识别与接触的时刻&#xff0c;以帮助提高球员的安全。 评价指标 马修斯相关系数&#xff08;Matthews Correlation Coefficient&#xff0c;简称MCC&#xff09;是一种常用的二…

图论算法笔记

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 第12章 最短路径算法12-1 有权图的最短路径问题最短路径问题-路径规划单源最短路径带权图的最短路径和无权图的最短路径带权图的最短路径算法-Dijkstra算法 12-2 Di…

检测到目标Strict-Transport-Security响应头缺失

详细描述 Web 服务器对于 HTTP 请求的响应头中缺少 Strict-Transport-Security&#xff0c;这将导致浏览器提供的安全特性失效。 当 Web 服务器的 HTTP 头中包含 Strict-Transport-Security 头时&#xff0c;浏览器将持续使用 HTTPS 来访问 Web 站点&#xff0c;可以用来对抗协…

Echart柱形图条纹设置

代码内容: option {xAxis: {type: category,data: [Mon, Tue, Wed, Thu, Fri, Sat, Sun]},yAxis: {type: value},series: [{data: [120, 200, 150, 80, 70, 110, 130],type: bar},{data: [20, 40, 90, 40, 30, 70, 120],type: bar},{data: [140, 230, 120, 50, 30, 150, 120]…

利用远程调试获取Chromium内核浏览器Cookie

前言 本文将介绍不依靠DPAPI的方式获取Chromium内核浏览器Cookie 远程调试 首先我们以edge为例。edge浏览器是基于Chromium的&#xff0c;而Chromium是可以开启远程调试的&#xff0c;开启远程调试的官方文档如下&#xff1a; https://blog.chromium.org/2011/05/remote-deb…

8.带你入门matlab 数据统计与分析——区间参数估计 均匀分布(matlab 程序 )

1.简述 本文将涉及到数理统计的最后一个模块——参数估计&#xff0c;后续将更新的模块是多项式计算、数据插值和曲线拟合。 在讲述使用matlab来实现参数估计之前&#xff0c;有必要去了解一些基本原理。 1.离散型随机变量的极大似然估计法: (1) 似然函数 若X为离散型, 似然函数…

NFTScan 与 Decert 达成合作伙伴,双方在 NFT 数据方面展开合作

近日&#xff0c;NFT 数据基础设施 NFTScan 与 Decert 达成合作伙伴关系&#xff0c;双方在多链 NFT 数据层面展开合作。在 Decert 产品中&#xff0c;由 NFTScan 为其提供专业的多链 NFT 数据支持&#xff0c;为用户带来优质的 NFT 搜索查询等相关交互功能&#xff0c;提升用户…

SQL 上升的温度

197 上升的温度 SQL架构 表&#xff1a; Weather ---------------------- | Column Name | Type | ---------------------- | id | int | | recordDate | date | | temperature | int | ---------------------- id 是这个表的主键 该表包含特定日期的温度信息 编写一个 SQL …

MySQL主从复制(一主一从)

文章目录 MySQL主从复制(一主一从)什么是主从复制主从复制的原理主从复制的优点准备工作配置主数据库Master配置从数据库Slave测试 MySQL主从复制(一主一从) 什么是主从复制 MySQL主从复制是指数据可以从一个MySQL数据库服务器的主节点复制到一个或多个从节点。主节点记录了所…

CAP原则的一致性、可用性、分区容错性

2000年&#xff0c;Eric Brewer在ACM PODC分布式计算原理专题讨论会上首次提出CAP原则。后来&#xff0c;麻省理工学院的两位科学家(赛斯吉尔伯特和南希林奇)证明了CAP原则的正确性。目前&#xff0c;CAP原则被大型公司广泛采纳&#xff0c;例如Amazon公司。 CAP原则又称CAP定…

Unity Obfuscator

官方仓库 学习日期&#xff1a;2023-07-13&#xff08;防止后续仓库特性或功能更新无对比时间&#xff09; 目标&#xff1a;本文介绍使用此github库&#xff0c;混淆unity项目的代码&#xff0c;在ILSpy中无法正确反编译。 一、说明 官方说明 配置界面 Features: ControlFlow…

Web 前端 Day 6

函数 <script>// parseInt(200px)// getSum(20, 30) ​ ​function sayHi() {console.log(hello,function!)} ​// 函数必须进行调用&#xff0c;才会执行sayHi()let age 21 ​// 函数要有返回值&#xff0c;一定要添加return关键字&#xff0c;否则返回值为undefinedfu…

五子棋小游戏 java版(代码+详细注释)

游戏展示 这周闲来无事&#xff0c;再来写个五子棋小游戏。基本功能都实现了&#xff0c;包括人人对战、人机对战。界面布局和功能都写的还行&#xff0c;没做到很优秀&#xff0c;但也不算差。如有需要&#xff0c;做个java初学者的课程设计或者自己写着玩玩也都是不错的&…

关于酒吧的八个大实话

1、酒吧风格定位取决于你酒吧面向的客户群体 2、酒吧要根据所在地区人流量开酒吧&#xff0c;大酒吧开在人流量少的可能死于客流量太小&#xff0c;合理的酒吧规模才能生存。3、酒吧客群不能只面向亲朋好友&#xff0c;再好的朋友也不可能天天来照顾你的生意。4、酒吧注重酒吧…

Task管理系统项目

Task管理系统项目 项目开发意义和目的 本项目所开发的系统为Task管理系统。管理信息系统是一个由人和计算机等组成的能够提供信息以支持一个组织机构内部的作业、管理、分析和决策职能的系统。管理信息系统利用计算机的硬件和软件&#xff0c;手工规程.分析、计划、控制和决策用…

浏览器视口

目录 css单位相对单位绝对单位 像素分类物理像素逻辑像素css像素 DPRPPI浏览器视口布局视口视觉视口理想视口 css单位 在css中我们会使用到许多单位&#xff0c;如px&#xff0c;em&#xff0c;rem&#xff0c;vw&#xff0c;vh等等 整体上&#xff0c;我们可以将它们分成两类…