Spring源码:调度框架EnableSchedulingScheduled源码解析

news2024/11/24 15:50:51

目录

1.开启调度框架

2.ScheduledAnnotationBeanPostProcessor Bean后处理器分析

2.1 调度框架支持的Task类型

2.2 对Task进行调度执行

3.任务调度器

3.1 任务调度器获取

3.2 框架内提供的任务调度器

3.3 任务调度器执行逻辑 


在实际项目开发中,有时会遇到定时调度的开发需要,这部分的功能在Spring框架中给出了较好的支持,即@EnableScheduling&Scheduled定时调度框架,本着不仅知其然还要知其所以然的指导思想,下面对该调度框架进行源码解析,以便更好的理解其执行过程;

1.开启调度框架

Spring框架中,为了开启调度框架功能,需要在配置类上标注@EnableScheduling注解,这也是Spring中Enable*模式的典型应用,下面看一下@EnableScheduling的具体实现:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

这里通过@Import注解,导入了配置类SchedulingConfiguration,进一步看下SchedulingConfiguration配置类的源码,如下:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

	@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
		return new ScheduledAnnotationBeanPostProcessor();
	}

}

可以看到,这里定义了一个Bean后处理器ScheduledAnnotationBeanPostProcessor,调度框架的解析逻辑也是定义在ScheduledAnnotationBeanPostProcessor中的,下面着重对该部分进行具体分析;

2.ScheduledAnnotationBeanPostProcessor Bean后处理器分析

Bean后处理器中,主要分析下后处理器的拦截方法,如下:

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
				bean instanceof ScheduledExecutorService) {
			// Ignore AOP infrastructure such as scoped proxies.
			return bean;
		}

		Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
		if (!this.nonAnnotatedClasses.contains(targetClass) &&
				AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
			Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
					(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
						Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
								method, Scheduled.class, Schedules.class);
						return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
					});
			if (annotatedMethods.isEmpty()) {
				this.nonAnnotatedClasses.add(targetClass);
				if (logger.isTraceEnabled()) {
					logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
				}
			}
			else {
				// Non-empty set of methods
				annotatedMethods.forEach((method, scheduledAnnotations) ->
						scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
				if (logger.isTraceEnabled()) {
					logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
							"': " + annotatedMethods);
				}
			}
		}
		return bean;
	}

如上,postProcessAfterInitialization方法中,主要对标注@Scheduled和聚合注解@Schedules的类成员方法进行处理,主要分为2步:

1)识别标注@Scheduled和聚合注解@Schedules的方法;

2)对注解方法调用processScheduled方法进行处理;

方法processScheduled处理过程如下:

	/**
	 * Process the given {@code @Scheduled} method declaration on the given bean.
	 * @param scheduled the {@code @Scheduled} annotation
	 * @param method the method that the annotation has been declared on
	 * @param bean the target bean instance
	 * @see #createRunnable(Object, Method)
	 */
	protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
		try {
			Runnable runnable = createRunnable(bean, method);
			boolean processedSchedule = false;
			String errorMessage =
					"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

			Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

			// Determine initial delay
			long initialDelay = convertToMillis(scheduled.initialDelay(), scheduled.timeUnit());
			String initialDelayString = scheduled.initialDelayString();
			if (StringUtils.hasText(initialDelayString)) {
				Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
				if (this.embeddedValueResolver != null) {
					initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
				}
				if (StringUtils.hasLength(initialDelayString)) {
					try {
						initialDelay = convertToMillis(initialDelayString, scheduled.timeUnit());
					}
					catch (RuntimeException ex) {
						throw new IllegalArgumentException(
								"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
					}
				}
			}

			// Check cron expression
			String cron = scheduled.cron();
			if (StringUtils.hasText(cron)) {
				String zone = scheduled.zone();
				if (this.embeddedValueResolver != null) {
					cron = this.embeddedValueResolver.resolveStringValue(cron);
					zone = this.embeddedValueResolver.resolveStringValue(zone);
				}
				if (StringUtils.hasLength(cron)) {
					Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
					processedSchedule = true;
					if (!Scheduled.CRON_DISABLED.equals(cron)) {
						TimeZone timeZone;
						if (StringUtils.hasText(zone)) {
							timeZone = StringUtils.parseTimeZoneString(zone);
						}
						else {
							timeZone = TimeZone.getDefault();
						}
						tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
					}
				}
			}

			// At this point we don't need to differentiate between initial delay set or not anymore
			if (initialDelay < 0) {
				initialDelay = 0;
			}

			// Check fixed delay
			long fixedDelay = convertToMillis(scheduled.fixedDelay(), scheduled.timeUnit());
			if (fixedDelay >= 0) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
			}

			String fixedDelayString = scheduled.fixedDelayString();
			if (StringUtils.hasText(fixedDelayString)) {
				if (this.embeddedValueResolver != null) {
					fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
				}
				if (StringUtils.hasLength(fixedDelayString)) {
					Assert.isTrue(!processedSchedule, errorMessage);
					processedSchedule = true;
					try {
						fixedDelay = convertToMillis(fixedDelayString, scheduled.timeUnit());
					}
					catch (RuntimeException ex) {
						throw new IllegalArgumentException(
								"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
					}
					tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
				}
			}

			// Check fixed rate
			long fixedRate = convertToMillis(scheduled.fixedRate(), scheduled.timeUnit());
			if (fixedRate >= 0) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
			}
			String fixedRateString = scheduled.fixedRateString();
			if (StringUtils.hasText(fixedRateString)) {
				if (this.embeddedValueResolver != null) {
					fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
				}
				if (StringUtils.hasLength(fixedRateString)) {
					Assert.isTrue(!processedSchedule, errorMessage);
					processedSchedule = true;
					try {
						fixedRate = convertToMillis(fixedRateString, scheduled.timeUnit());
					}
					catch (RuntimeException ex) {
						throw new IllegalArgumentException(
								"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
					}
					tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
				}
			}

			// Check whether we had any attribute set
			Assert.isTrue(processedSchedule, errorMessage);

			// Finally register the scheduled tasks
			synchronized (this.scheduledTasks) {
				Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
				regTasks.addAll(tasks);
			}
		}
		catch (IllegalArgumentException ex) {
			throw new IllegalStateException(
					"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
		}
	}

上述处理过程主要包含以下几步:

1)将调用目标方法的过程包装为ScheduledMethodRunnable类

2)构造CronTask并进行调度

3)构造FixedDelayTask并进行调度

4)构造FixedRateTask并进行调度

下面主要说明下调度任务的类型以及具体的调度方法;

2.1 调度框架支持的Task类型

 Spring调度框架中重要支持3种调度任务类型(继承结构如上图),具体说明如下:

1)CronTask:cron表达式调度的任务

2)FixedDelayTask:固定延迟时间执行的任务

3)FixedRateTask:固定速率执行的任务

2.2 对Task进行调度执行

上述3种的调度执行实现近似,下面以FixedDelayTask进行说明,该任务的调度方法为scheduleFixedDelayTask,具体实现如下:

	/**
	 * Schedule the specified fixed-delay task, either right away if possible
	 * or on initialization of the scheduler.
	 * @return a handle to the scheduled task, allowing to cancel it
	 * (or {@code null} if processing a previously registered task)
	 * @since 5.0.2
	 */
	@Nullable
	public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) {
		ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
		boolean newTask = false;
		if (scheduledTask == null) {
			scheduledTask = new ScheduledTask(task);
			newTask = true;
		}
		if (this.taskScheduler != null) {
			if (task.getInitialDelay() > 0) {
				Date startTime = new Date(this.taskScheduler.getClock().millis() + task.getInitialDelay());
				scheduledTask.future =
						this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), startTime, task.getInterval());
			}
			else {
				scheduledTask.future =
						this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), task.getInterval());
			}
		}
		else {
			addFixedDelayTask(task);
			this.unresolvedTasks.put(task, scheduledTask);
		}
		return (newTask ? scheduledTask : null);
	}

这里主要包含以下几步:

1)将调度任务包装为ScheduledTask类型,其中封装了执行结果ScheduledFuture

2)存在任务调度器(taskScheduler)时,直接进行调度执行

3)不存在任务调度器(taskScheduler)时,将任务暂存到fixedDelayTasks中,待调用afterPropertiesSet方法时再进行调度执行

3.任务调度器

3.1 任务调度器获取

任务调度器支持自定义,当无自定义调度器时,调度框架提供了默认的任务调度器;

自定义任务调度器的处理逻辑在方法finishRegistration中,如下:

	private void finishRegistration() {
		if (this.scheduler != null) {
			this.registrar.setScheduler(this.scheduler);
		}

		if (this.beanFactory instanceof ListableBeanFactory) {
			Map<String, SchedulingConfigurer> beans =
					((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
			List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
			AnnotationAwareOrderComparator.sort(configurers);
			for (SchedulingConfigurer configurer : configurers) {
				configurer.configureTasks(this.registrar);
			}
		}

		if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
			Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type");
			try {
				// Search for TaskScheduler bean...
				this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false));
			}
			catch (NoUniqueBeanDefinitionException ex) {
				if (logger.isTraceEnabled()) {
					logger.trace("Could not find unique TaskScheduler bean - attempting to resolve by name: " +
							ex.getMessage());
				}
				try {
					this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true));
				}
				catch (NoSuchBeanDefinitionException ex2) {
					if (logger.isInfoEnabled()) {
						logger.info("More than one TaskScheduler bean exists within the context, and " +
								"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
								"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
								"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
								ex.getBeanNamesFound());
					}
				}
			}
			catch (NoSuchBeanDefinitionException ex) {
				if (logger.isTraceEnabled()) {
					logger.trace("Could not find default TaskScheduler bean - attempting to find ScheduledExecutorService: " +
							ex.getMessage());
				}
				// Search for ScheduledExecutorService bean next...
				try {
					this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false));
				}
				catch (NoUniqueBeanDefinitionException ex2) {
					if (logger.isTraceEnabled()) {
						logger.trace("Could not find unique ScheduledExecutorService bean - attempting to resolve by name: " +
								ex2.getMessage());
					}
					try {
						this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true));
					}
					catch (NoSuchBeanDefinitionException ex3) {
						if (logger.isInfoEnabled()) {
							logger.info("More than one ScheduledExecutorService bean exists within the context, and " +
									"none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " +
									"(possibly as an alias); or implement the SchedulingConfigurer interface and call " +
									"ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " +
									ex2.getBeanNamesFound());
						}
					}
				}
				catch (NoSuchBeanDefinitionException ex2) {
					if (logger.isTraceEnabled()) {
						logger.trace("Could not find default ScheduledExecutorService bean - falling back to default: " +
								ex2.getMessage());
					}
					// Giving up -> falling back to default scheduler within the registrar...
					logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing");
				}
			}
		}

		this.registrar.afterPropertiesSet();
	}

上述获取任务调度器的优先级顺序为:

1)当Bean后处理器中定义了任务调度器时,优先取Bean后处理器的任务调度器

2)在BeanFactory中获取Bean类型为SchedulingConfigurer的实例,在其方法configureTasks中可以自定义任务调度器

3)获取BeanFactory中TaskScheduler类型的bean(如有)

4)获取BeanFactory中ScheduledExecutorService类型的bean(如有)

5)当上述方式获取的任务调度器都不存在时,会使用框架中默认的任务调度器,如下:

		if (this.taskScheduler == null) {
			this.localExecutor = Executors.newSingleThreadScheduledExecutor();
			this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
		}

3.2 框架内提供的任务调度器

框架内提供的任务调度器主要包括:

1)ConcurrentTaskExecutor

2)ThreadPoolTaskScheduler

继承结构如下:

3.3 任务调度器执行逻辑 

以上述框架默认的ConcurrentTaskScheduler进行说明,在调用调度器方法scheduleWithFixedDelay执行时,具体执行逻辑为:

	@Override
	public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
		long initialDelay = startTime.getTime() - this.clock.millis();
		try {
			return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), initialDelay, delay, TimeUnit.MILLISECONDS);
		}
		catch (RejectedExecutionException ex) {
			throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
		}
	}

这里主要包含2部分:

1)首先把task任务包装为DelegatingErrorHandlingRunnable类型(支持嵌入错误处理器逻辑),具体是在方法decorateTask中实现的,如下:

	private Runnable decorateTask(Runnable task, boolean isRepeatingTask) {
		Runnable result = TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask);
		if (this.enterpriseConcurrentScheduler) {
			result = ManagedTaskBuilder.buildManagedTask(result, task.toString());
		}
		return result;
	}

	public static DelegatingErrorHandlingRunnable decorateTaskWithErrorHandler(
			Runnable task, @Nullable ErrorHandler errorHandler, boolean isRepeatingTask) {

		if (task instanceof DelegatingErrorHandlingRunnable) {
			return (DelegatingErrorHandlingRunnable) task;
		}
		ErrorHandler eh = (errorHandler != null ? errorHandler : getDefaultErrorHandler(isRepeatingTask));
		return new DelegatingErrorHandlingRunnable(task, eh);
	}

2)调用线程池方法scheduleWithFixedDelay进行调度执行

至此,Spring调度框架整体的处理过程总结如下:

  1. 开启调度框架(@EnableScheduling)
  2. 利用bean后处理器识别@Scheduled注解,并包装为Task任务
  3. 利用任务调度器(TaskScheduler,自定义或框架默认)进行调度执行

Over~~

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

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

相关文章

1、动手学深度学习——线性神经网络:线性回归的实现(从零实现+内置函数实现)

1、线性回归基本概念 回归&#xff08;regression&#xff09;是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域&#xff0c;回归经常用来表示输入和输出之间的关系。 给定一个数据集&#xff0c;我们的目标是寻找模型的权重和偏置&#xf…

selenium测试框架快速搭建(UI自动化测试)

一、介绍 selenium目前主流的web自动化测试框架&#xff1b;支持多种编程语言Java、pythan、go、js等&#xff1b;selenium 提供一系列的api 供我们使用&#xff0c;因此在web测试时我们要点页面中的某一个按钮&#xff0c;那么我们只需要获取页面&#xff0c;然后根据…

【力扣刷题 | 第十五天】

目录 前言&#xff1a; ​​​​​​​63. 不同路径 II - 力扣&#xff08;LeetCode&#xff09; 343. 整数拆分 - 力扣&#xff08;LeetCode&#xff09; 总结&#xff1a; 前言&#xff1a; 本篇我们主要刷动态规划的题&#xff0c;解题还是严格按照我们在【夜深人静写算法…

Linux查看文件大小

1、Linux下查看文件和文件夹大小 当磁盘大小超过标准时会有报警提示&#xff0c;这时如果掌握df和du命令是非常明智的选择。 df可以查看一级文件夹大小、使用比例、档案系统及其挂入点&#xff0c;但对文件却无能为力。 du可以查看文件及文件夹大小。 两者配合使用&#xf…

UE中创建可脚本化编辑器工具(Scriptable Tools)

UE5.2中提供了可脚本化工具编辑模式&#xff0c;该模式下用户可以编写蓝图节点自定义界面操作模式下的逻辑&#xff0c;例如重写鼠标点击事件&#xff0c;制作自定义画刷等。 如果你不太了解UE编辑器工具&#xff0c;可以参考这篇文章&#xff1a; https://blog.csdn.net/gray…

chatgpt赋能python:Python编程计算一元二次方程——最简单的方法实现

Python编程计算一元二次方程——最简单的方法实现 前言 Python编程语言是一种优秀的开源编程语言&#xff0c;具有易于学习、代码简洁明了、易于维护等优点&#xff0c;因此在近年来得到了广泛的应用。 本文将介绍如何使用Python编写一个简单而又实用的计算一元二次方程的程…

华为认证哪个方向考的人多?考试费用是多少?

从学校毕业后&#xff0c;就要走上社会了&#xff0c;很多人就要面临就业困难&#xff0c;对于信息通信行业的人来说&#xff0c;考一份技术证书&#xff0c;是可以帮助自己提高职业竞争力、获得好工作的方法。华为云认证是华为云旗下的认证&#xff0c;其设立时间长&#xff0…

多线程爬虫实战-思路

前言 最近有很多小伙伴找到我&#xff0c;说想要王者荣耀所有英雄皮肤的照片&#xff0c;但是又不想自己去下载&#xff0c;正好借这个机会给大家讲解一下多线程的爬虫实战 由于线程爬虫会对任何服务器都有一定的影响&#xff0c;本文仅供学习交流使用&#xff0c;切勿拿去做什…

PID原理及仿真程序套用(matlab仿真与图像处理系列第3期)

PID控制器模拟器 概述: PID控制器是一种常用的反馈控制算法,用于实现系统输出与期望值之间的精确调节。PID控制器模拟器是一个工具,可以模拟和测试PID控制器的性能,并对系统进行调整和优化。 输入参数: setpoint:期望值或目标值process_variable:过程变量或实际测量…

go中map哈希表使用总结

作者&#xff1a;程序员CKeen 博客&#xff1a;http://ckeen.cn​​​​​​​ 长期坚持做有价值的事&#xff01;积累沉淀&#xff0c;持续成长&#xff0c;升维思考&#xff01;希望把编码做为长期爱好&#x1f604; 在 Go 语言中&#xff0c;map是一种基于哈希表实现的数据结…

设计模式之解释器模式笔记

设计模式之解释器模式笔记 说明Interpreter(解释器)目录解释器模式示例类图抽象表达式类环境角色类封装变量的类加法表达式类减法表达式类测试类 说明 记录下学习设计模式-解释器模式的写法。JDK使用版本为1.8版本。 Interpreter(解释器) 意图:定义一个语言&#xff0c;定义…

chatgpt赋能python:Python编程:如何将程序变成软件

Python编程&#xff1a;如何将程序变成软件 Python是一种非常流行的编程语言&#xff0c;因为它具有简单易学、高效和灵活等优点。许多人使用Python编写程序&#xff0c;但他们可能不知道如何将这些程序变成可执行的软件。在本文中&#xff0c;我们将介绍Python程序如何成为软…

【C/C++数据结构】链表与快慢指针

目录 一、单链表 二、双向循环链表 三、判断链表是否带环 四、链表的回文结构判断 五、复制带随机指针的链表 一、单链表 优点&#xff1a;头部增删效率高&#xff0c;动态存储无空间浪费 缺点&#xff1a;尾部增删、遍历效率低&#xff0c;不支持随机访问节点 头结点&…

【Linux | Shell】bash shell 基础命令

目录 一、概述二、启动shell2.1 用户的默认 Shell 程序2.2 Shell 提示符 三、 基础命令3.1、man 命令3.2、cd 命令3.3、ls命令 一、概述 很多 Linux 发行版的默认 shell 是 GNU bash shell。本文将介绍 bash shell 的基本特性&#xff0c;比如 bash 手册、命令行补全以及如何显…

JavaWeb 第一个Servlet程序

1.Servlet Servlet是Java编写的用于Web应用程序的服务器端程序。 它可以接收来自Web浏览器的HTTP请求并生成响应。 Servlet通常用于创建动态Web内容&#xff0c;例如网页、表单处理、登录和数据库访问等。 Servlet是Java EE&#xff08;Enterprise Edition&#xff09;规范…

[补充]托福口语21天——day2 课堂内容

day1 课堂&#xff1a;http://t.csdn.cn/cyvZm day1 语料&#xff1a;http://t.csdn.cn/syTBy 目录 1 时间准备 2 例题 3 答题步骤 3.1 范例 3.2 范例 4 连接词 5 完整回答 5.1 范例 5.2 范例 6 总结 背背背&#xff01; 1. 如今的生活成本非常高。人们要付…

学习系统编程No.25【核心转储实战】

引言&#xff1a; 北京时间&#xff1a;2023/6/16/8:39&#xff0c;实训课中&#xff0c;大一下学期最后有课的一天&#xff0c;还有两天就要期末考啦&#xff01;目前什么都还没有复习&#xff0c;不到星期天晚上&#xff0c;咱不慌&#xff0c;小小挂科&#xff0c;岂能拦得…

postgresql_internals-14 学习笔记(六)—— 统计信息

不完全来自这本书&#xff0c;把查到的和之前的文章重新汇总整理了一把。 一、 统计信息的收集 1. 主要参数 其中最主要的是track_counts&#xff0c;开启才会收集统计信息。 postgres# select name,setting,short_desc,context from pg_settings where name like track%; na…

LeetCode 周赛 351(2023/06/25)T2 有点意思

本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。 往期回顾&#xff1a;LeetCode 单周赛第 348 场 数位 DP 模版学会了吗&#xff1f; T1. 美丽下标对的数目&#xff08;Easy&#xff09; 标签&am…

python爬虫并做可视化分析--前程无忧

一.数据采集 1.采集逻辑 2.数据schema 招聘信息Schema { "岗位名称": "财务会计主管", "薪资":"1.3-2万", "地址": "*******", "经验要求": "5-7年", "公司名": "***…