1、任务调度背景

在业务系统中有很多这样的场景:
1、账单日或者还款日上午 10 点,给每个信用卡客户发送账单通知,还款通知。如何判断客户的账单日、还款日,完成通知的发送?
2、银行业务系统,夜间要完成跑批的一系列流程,清理数据,下载文件,解析文件,对账清算、切换结算日期等等,如何触发一系列流程的执行?
3、金融机构跟人民银行二代支付系统对接,人民银行要求低于 5W的金额(小额支付)半个小时打一次包发送,以缓解并发压力。所以,银行的跨行转账分成了多个流程: 录入、复核、发送。如何把半个小时以内的所有数据一次性发送?

类似于这种基于准确的时刻或者固定的时间间隔触发的任务,或者有批量数据需要处理,再或者要实现两个动作解耦的场景,都可以用任务调度来实现。

任务调度的实现方式有很多,如果要实现调度需求,那么工具应该要有什么样的基本要求呢?
1)可以定义触发的规则。比如基于时刻、时间间隔、表达式;
2)可以定义需要执行的任务。比如执行一个脚本或者一段代码,任务和规则是分开的;
3)集中管理配置,持久配置。不用把规则写在代码里面,可以看到所有的任务配置,方便维护,重启之后任务可以再次调度——配置文件或者配置中心;
4)支持任务的串行执行。例如执行 A 任务后再执行 B 任务再执行 C 任务;
5)支持多个任务并发执行,互不干扰(例如 ScheduledThreadPoolExecutor);
6)有自己的调度器,可以启动、中断、停止任务;
7)容易集成到 Spring

任务调度工具对比:

层次 举例 特点
操作系统 Linux crontab、Windows 计划任务 只能执行简单脚本或者命令
数据库 MySQL、Oracle 可以操作数据,但不能执行 Java 代码
工具 Kettle 可以操作数据,执行脚本,但没有集中配置
开发语言 JDK Timer、ScheduledThreadPool Timer:单线程,JDK1.5 之后:ScheduledThreadPool(Cache、Fiexed、Single):没有集中配置,日程管理不够灵活
容器 Spring Task、@Scheduled 不支持集群
分布式框架 XXL-JOB,Elastic-Job

@Scheduled 也是用 JUC 的 ScheduledExecutorService 实现的 
Scheduled(cron = “0 15 10 15 * ?”)**
1、 ScheduledAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法将@Scheduled 的方法包装为指定的 task添加到 ScheduledTaskRegistrar 中;
2、 ScheduledAnnotationBeanPostProcessor 会监听 Spring 的容器初始化事件,在 Spring 容器初始化完成后进行TaskScheduler 实现类实例的查找,若发现有 SchedulingConfigurer 的实现类实例,则跳过 3;
3、 查找 TaskScheduler 的实现类实例默认是通过类型查找,若有多个实现则会查找名字为"taskScheduler"的实现 Bean,若没有找到则在 ScheduledTaskRegistrar 调度任务的时候会创建一个 newSingleThreadScheduledExecutor,将TaskScheduler 的实现类实例设置到 ScheduledTaskRegistrar 属性中;
4、 ScheduledTaskRegistrar 的 scheduleTasks 方法触发任务调度;
5、 真正调度任务的类是 TaskScheduler 实现类中的 ScheduledExecutorService,由JUC提供;

2、Quartz是什么

老规矩,没有什么比官网更适合去学习一门新的知识:http://www.quartz-scheduler.org/

Quartz 的意思是石英,像石英表一样精确。 Quartz 的目的就是让任务调度更加简单,开发人员只需要关注业务即可。他是用 Java 语言编写的(也有.NET 的版本)。Java 代码能做的任何事情,Quartz 都可以调度。

而官网吹起来也是毫不吝啬:Quartz是一个功能丰富的开源作业调度库,可以集成到几乎所有的Java应用中--从最小的独立应用到最大的电子商务系统。Quartz可用于创建简单或复杂的时间表,以执行几十、几百、甚至几万个作业;这些作业的任务被定义为标准的Java组件,几乎可以执行任何你可以编程让它们做的事情。Quartz Scheduler包括许多企业级功能,如支持JTA事务和集群。

不过说句实在话,Quartz是一个非常老牌的任务调度系统,从1998 年构思,于 2001 年发布到 sourceforge,到现在也风风雨雨熬了20来年,目前整体更新比较慢,因为已经非常成熟了:

1、精确到毫秒级别的调度;

2、可以独立运行,也可以集成到容器中;

3、支持事务(JobStoreCMT );

4、支持集群;

5、支持持久化;

目前GitHub上最新版还是三年前的版本

3、Quartz初尝

引入maven依赖

<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

默认配置文件

在org.quartz.core 包下,有一个默认的配置文件:quartz.properties。如果没有定义一个同名的配置文件的时候,就会使用默认配置文件里面的配置。

创建任务Job

实现Quartz提供的Job接口,实现接口中的execute方法,也就是任务执行的内容:

执行任务

可以看到,任务每2s执行一次,封装在JobDetail中的信息也可以取出

所以,使用Quartz就是这么简单,拢共就4步,依赖、任务、触发器、调度器,完事儿。值得注意的是,调度器一定是单例的,在构建的时候,Quartz也做了一个单例的判断和保证。

4、Quartz基本架构

Trigger的四大类型

而刚刚上面Demo中使用的Trigger是SimpleTrigger,听名字就知道肯定干不了多复杂的事儿,它是基于某种固定时刻进行任务触发。而在Quartz中一共提供了4中不同类型的Trigger:

1、SimpleTrigger:简单触发器。固定时刻或时间间隔,精度可以做到毫秒级。例如:每天 9 点钟运行;每隔 30 分钟运行一次。

2、CalendarIntervalTrigger:基于日历的触发器。比简单触发器更多时间单位,支持非固定时间的触发,例如一年可能 365/366,一个月可能 28/29/30/31。例如:每年、每个月、每周、每天、每小时、每分钟、每秒。即使每年的月数和每个月的天数不是固定的也适用;

3、DailyTimeIntervalTrigger:基于日期的触发器。每天的某个时间段。例如:每天早上 9 点到晚上 9 点,每隔半个小时执行一次,并且只在周一到周六执行。

4、CronTrigger:基于 Cron 表达式的触发器,是最常用的触发器类型;Cron表达式是基于Linux的crontab基础上移植出的表达式,用来定义时间维度的调度规则。虽然是移植出来的,但是一点优化都舍不得做,可读性还么那么差。

Cron表达式

位置 时间域 特殊值
1 0-59 , - * /
2 分钟 0-59 , - * /
3 小时 0-23 , - * /
4 日期 1-31 , - * ? / L W C
5 月份 1-12 , - * /
6 星期 1-7 , - * ? / L W C
7 年份(可选) 1-31 , - * /

Cron 表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。好了,下面对于Cron表达式的介绍感兴趣的就看一眼,不感兴趣的直接跳过既可。反正在真正开发的时候,会写的顺手就写了,不会写的度娘查一下也顺手就复制了,没人真的会去自己研究规则。

星号(*):可用在所有字段中,表示对应时间域的每一个时刻,例如,在分钟字段时,表示“每分钟”;

问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;

减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从 10 到 12 点,即 10,11,12;

逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;

斜杠(/):x/y 表达一个等步长序列,x 为起始值,y 为增量步长值。如在分钟字段中使用 0/15,则表示为 0,15,30 和 45 秒,而 5/15 在分钟字段中表示 5,20,35,50,你也可以使用*/y,它等同于 0/y;

L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L 在日期字段中,表示 这个月份的最后一天,如一月的 31 号,非闰年二月的 28 号;如果 L 用在星期中,则表示星期六,等同于 7。但是,如果 L 出现在星期字段里,而且在前面有一个数值 X,则表示“这个月的最后 X 天”,例如,6L 表示该月的最后星期五;

W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如 15W 表示离该月 15号最近的工作日,如果该月 15 号是星期六,则匹配 14 号星期五;如果 15 日是星期日,则匹配 16 号星期一;如果 15号是星期二,那结果就是 15 号星期二。但必须注意关联的匹配日期不能够跨月,如你指定 1W,如果 1 号是星期六,结果匹配的是 3 号星期一,而非上个月最后的那天。W 字符串只能指定单一日期,而不能指定日期范围;

LW 组合:在日期字段可以组合使用 LW,它的意思是当月的最后一个工作日;

井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如 6#3 表示当月的第三个星期五(6 表示星期五,\#3 表示当前的第三个),而 4#5 表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;

C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如 5C 在日期字段中就相当于日历 5 日以后的第一天。1C 在星期字段中相当于星期日后的第一天。

基于Calendar的排除规则

上面定义的都是在什么时间执行,但是有一些在什么时间不执行的需求。比如:理财周末和法定假日购买不计息;证券公司周末和法定假日休市。是不是要把日期写在数据库中,然后读取基于当前时间判断呢?

如果要在触发器的基础上,排除一些时间区间不执行任务,就要用到Quartz的Calendar类(注意不是JDK的Calendar)。可以按年、月、周、日、特定日期、Cron表达式排除。

Calendar名称 用法
BaseCalendar 为高级的 Calendar 实现了基本的功能,实现了 org.quartz.Calendar 接口
AnnualCalendar 排除年中一天或多天
CronCalendar 日历的这种实现排除了由给定的CronExpression表达的时间集合。 例如:可以使用此日历使用表达式“* * 0-7,18-23?* *”每天排除所有营业时间(上午8点至下午5点)。 如果CronTrigger具有给定的cron表达式并且与具有相同表达式的CronCalendar相关联,则日历将排除触发器包含的所有时间,并且它们将彼此抵消。
DailyCalendar 可以使用此日历来排除营业时间(上午8点 - 5点)每天。 每个DailyCalendar仅允许指定单个时间范围,并且该时间范围可能不会跨越每日边界(即,您不能指定从上午8点至凌晨5点的时间范围)。 如果属性invertTimeRange为false(默认),则时间范围定义触发器不允许触发的时间范围。 如果invertTimeRange为true,则时间范围被反转 - 也就是排除在定义的时间范围之外的所有时间。
HolidayCalendar 特别的用于从 Trigger 中排除节假日
MonthlyCalendar 排除月份中的指定数天,例如,可用于排除每月的最后一天
WeeklyCalendar 排除星期中的任意周几,例如,可用于排除周末,默认周六和周日

以AnnualCalendar为例,其实使用非常简单,只要指定好想要排除的日期,然后设置进去调度器中即可,调度器在执行的时候自然会将指定的日期排除掉:

Listener

有这么一种需求,在每个任务运行结束之后发送通知给运维管理员,那是不是要在每个任务的最后添加一行代码呢。这种方式对原来的代码造成了入侵,不利于维护。如果代码不是写在任务代码的最后一行,怎么知道任务执行完了呢,或者说,怎么监测到任务的生命周期呢?

Quartz中提供了三种Listener,监听Scheduler的,监听Trigger的,监听Job的。 使用观察者模式,来定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并自动更新。 只需要创建类实现相应的接口,并在Scheduler上注册Listener,便可实现对核心对象的监听。

JobListener

ListenerManager,主要用于添加、获取、移除监听器;而Matcher,主要是基于groupName和keyName进行匹配。

可以看到,当Job执行的时候,通过JobListener监听可以取到Job的名称及相关的执行状态:

JobListener中提供的4个方法:

getName():返回JobListener的名称;

jobToBeExecuted():Scheduler 在 JobDetail 将要被执行时调用这个方法;

jobExecutionVetoed():Scheduler 在 JobDetail 即将被执行,但又被 TriggerListener 否决了时调用这个方法;

jobWasExecuted():Scheduler 在 JobDetail 被执行之后调用这个方法;

TriggerListener

TriggerListener中提供的5个方法:

getName():返回监听器的名称;

triggerFired():Trigger 被触发,Job 上的 execute() 方法将要被执行时,Scheduler 就调用这个方法;

vetoJobExecution():在 Trigger 触发后,Job 将要被执行时由 Scheduler 调用这个方法。TriggerListener 给了一个选择去否决 Job 的执行。假如这个方法返回 true,这个 Job 将不会为此次 Trigger 触发而得到执行;

triggerMisfired():Trigger 错过触发时调用;

triggerComplete():Trigger 被触发并且完成了 Job 的执行时,Scheduler 调用这个方法;

SchedulerListener

这个监听器提供的方法更多,大体上和上面的两个监听器差不多,感兴趣的自己去实现SchedulerListener接口的方法,一看便知。

JobStore

使用Quartz提供的这些监听器可以很方便的获取任务执行的进度,那么问题来了:如果任务执行到一半的时候,调度器服务重启,原来任务中运行的信息全部丢失。如果想要基于原先的任务进度做进度恢复的话,是无法实现的,因为Quartz默认的这些数据都是存储在内存中:

RAMJobStore

Quartz默认的JobStore是RAMJobstore,也就是把任务和触发器信息运行的信息存储在内存中,用到了HashMap、TreeSet、HashSet等等数据结构。如果程序崩溃或重启,所有存储在内存中的数据都会丢失,如果想要进行数据的恢复,就需要把这些数据持久化到磁盘。

JDBCJobStore

如果要将数据保存到磁盘中,必不可少需要使用到数据库进行数据的存储。Quartz提供的JDBCJobStore可以通过JDBC接口,将任务运行数据保存在数据库中。但是,数据库中的表结构应该要怎么去设计呢?表中应该有哪些字段,字段的名称、数据类型、长度等等属性是不是应该统一规范起来呢。

是的,在Quartz官网上已经将数据库的建表语句提供好了,从maven下载下来的源码包中就已经包含了SQL语句脚本,使用IDEA全局搜索就完事儿:org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar!\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql

Quartz提供的数据库脚本非常的全,我们这边需要用到的脚本文件就是tables_mysql_innodb.sql,将这个文件中的脚本复制到数据库总执行即可

这11张表的含义分别为:

qrtz_blob_triggers:Trigger作为Blob类型存储;
qrtz_calendars:存储Quartz的Calendar信息;
qrtz_cron_triggers:存储CronTrigger,包括Cron表达式和时区信息;
qrtz_fired_triggers:存储与已触发的Trigger相关的状态信息,以及相关Job的执行信息;
qrtz_job_details:存储每一个已配置的Job的详细信息;
qrtz_locks:存储程序的悲观锁的信息;
qrtz_paused_trigger_grps:存储已暂停的Trigger组的信息;
qrtz_scheduler_state:存储少量的有关Scheduler的状态信息,和别的Scheduler实例;
qrtz_simple_triggers:存储SimpleTrigger的信息,包括重复次数、间隔、以及已触的次数;
qrtz_simprop_triggers:存储CalendarIntervalTrigger和DailyTimeIntervalTrigger类型的触发器;
qrtz_triggers:存储已配置的Trigger的信息;

5、动态调度任务

传统的Spring方式集成,由于任务信息全部配置在xml文件中,如果需要操作任务或者修改任务运行频率,只能重新编译、打包、部署、重启,如果有紧急问题需要处理,会浪费很多的时间。有没有可以动态调度任务的方法,比如:停止一个Job,启动一个Job,或者修改Job的触发频率。读取配置文件、写入配置文件、重启Scheduler或重启应用明显是不可取的。对于这种频繁变更并且需要实时生效的配置信息,应该放在哪里是最好的?

有了上面介绍的那些铺垫,自然会想到将这些配置放入数据库中,顺便提供一个界面,实现对数据表的轻松操作。这样通过页面上简单的设置,便可动态进行任务的调度。

引入相关依赖

定义任务的执行内容

注意:这里实现的动态调度,是先将任务预计定义好,然后对已经定义好的任务进行动态的启动、停止、修改调度频率。而不是直接可以从0到1,添加全新的任务执行。如果要实现添加全新的任务执行的话,就需要在页面上编码执行的代码作为参数传递,这样的做法是违反生产规则的,非常不建议这么做。

写个Controller,模拟页面请求的各种动态调度定时任务

package com.feenix.quartz.controller;import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Map;@RestController
public class JobController {/*** 添加一个定时任务*/@PostMapping("/addJob")public void addJob(String jobClassPath, String jobName, String jobGroupName, String jobDataMap, String cronExpression)throws ClassNotFoundException, InstantiationException, IllegalAccessException, SchedulerException {// 封装JobDetailJob job = (Job) Class.forName(jobClassPath).newInstance();JobDetail jobDetail = JobBuilder.newJob(job.getClass()).withIdentity(jobName, jobGroupName).build();// 传递任务运行时的参数Map<String, String> map = JSON.parseObject(jobDataMap, Map.class);map.forEach((k, v) -> {jobDetail.getJobDataMap().put(k, v);});// 根据Cron表达式生成 调度器构建器CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);// 构建触发器TriggerCronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroupName).withSchedule(cronScheduleBuilder).startNow().build();// 构建并启动调度器Scheduler scheduler = new StdSchedulerFactory().getScheduler();scheduler.scheduleJob(jobDetail, trigger);scheduler.start();}/*** 暂停一个定时任务*/@PostMapping("/pauseJob")public void pauseJob(String jobName, String jobGroupName) throws SchedulerException {Scheduler scheduler = new StdSchedulerFactory().getScheduler();scheduler.pauseJob(JobKey.jobKey(jobName, jobGroupName));}/*** 启用一个定时任务*/@PostMapping("/resumeJob")public void resumeJob(String jobName, String jobGroupName) throws SchedulerException {Scheduler scheduler = new StdSchedulerFactory().getScheduler();scheduler.resumeJob(JobKey.jobKey(jobName, jobGroupName));}/*** 更新定时任务表达式*/@PostMapping("/rescheduleJob")public void rescheduleJob(String jobName, String jobGroupName, String cronExpression) throws SchedulerException {// 构建调度器SchedulerScheduler scheduler = new StdSchedulerFactory().getScheduler();// 获取触发器的KeyTriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);// 根据触发器Key从调度器中获取触发器CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);// 根据新的Cron表达式生成调度器构建器CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);// 将原先的触发器绑定新的调度器构建器trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).startNow().build();// 按新的trigger重新设置job执行scheduler.rescheduleJob(triggerKey, trigger);}/*** 删除定时任务*/@PostMapping("/deleteJob")public void deleteJob(String jobName, String jobGroupName) throws SchedulerException {// 构建调度器SchedulerScheduler scheduler = new StdSchedulerFactory().getScheduler();// 停止出发scheduler.pauseTrigger(TriggerKey.triggerKey(jobName, jobGroupName));// 取消调度器任务scheduler.unscheduleJob(TriggerKey.triggerKey(jobName, jobGroupName));// 删除任务scheduler.deleteJob(JobKey.jobKey(jobName, jobGroupName));}}

编写quartz.properties配置文件

启动服务,使用PostMan对接口进行请求:添加一个定时任务

jobClassPath的值给的是预先定义好的执行任务的类的全路径。前面自定义任务的时候,定义了两个任务,分别是TaskJob1和TaskJob2。现在需要将任务TaskJob1加入调度器中执行起来,所以jobClassPath需要给TaskJob1的全路径。

cronExpression定义任务执行的频率,0/2 * * * * ? 的意思是每2秒执行一次,看效果:

任务已经成功调度执行起来。从控制台打印的信息中可以看到,Quartz已经连接到配置中的MySQL数据库

查看数据库中的表,刚刚执行的任务信息已经储存其中:

使用PostMan对接口进行请求:暂停一个定时任务

使用PostMan对接口进行请求:启用一个定时任务

使用PostMan对接口进行请求:更新定时任务执行频率

使用PostMan对接口进行请求:删除定时任务

【手把手】分布式定时任务调度解析之Quartz相关推荐

  1. 【手把手】分布式定时任务调度解析之Elastic-Job

    1.这货怎么没怎么听过 经常使用Quartz或者Spring Task的小伙伴们,或多或少都会遇到几个痛点,比如: 1.不敢轻易跟着应用服务多节点部署,可能会重复多次执行而引发系统逻辑的错误: 2.Q ...

  2. ElasticJob分布式定时任务调度框架以及生产遇到的问题

    ElasticJob分布式定时任务调度框架 1:maven配置 2:客户端xml配置 3:测试类 4:重点: 5:总结 1:maven配置 <!--elastic-job--><de ...

  3. 遇见未知的Saturn |准备篇:分布式定时任务调度系统技术解决方案(xxl-job、Elastic-job、Saturn)

    1.业务场景 保险人管系统每月工资结算,平安有150万代理人,如何快速的进行工资结算(数据运算型) 保险短信开门红/电商双十一 1000w+短信发送(短时汇聚型) 工作中业务场景非常多,所涉及到的场景 ...

  4. quarts集群 运维_分布式定时任务调度系统技术解决方案(xxl-job、Elastic-job、Saturn)...

    1.业务场景 保险人管系统每月工资结算,平安有150万代理人,如何快速的进行工资结算(数据运算型) 保险短信开门红/电商双十一 1000w+短信发送(短时汇聚型) 工作中业务场景非常多,所涉及到的场景 ...

  5. C# Task 循环任务_taroco-scheduler 分布式定时任务调度

    taroco-scheduler 分布式定时任务调度 基于 Spring Boot 2.0 + Spring Task + Zookeeper 的分布式任务调度组件,非常轻量级,使用简单,只需要引入j ...

  6. 分布式定时任务调度系统技术解决方案(xxl-job、Elastic-job、Saturn)

    分布式定时任务调度系统技术解决方案(xxl-job.Elastic-job.Saturn) 参考文章: (1)分布式定时任务调度系统技术解决方案(xxl-job.Elastic-job.Saturn) ...

  7. 分布式定时任务调度中心

    分布式定时任务调度中心选型 目前主流的开源分布式定时任务调度中心据我了解主要是XXL-JOB和ELASTIC-JOB. 对比: 以上框架实现的功能大体都差不多,下面说下我选择XXL-JOB的原因: 1 ...

  8. 分布式定时任务调度框架Quartz

    文章目录 一.Quartz引言 二.Quartz使用 2.1 导入依赖 2.2 定义Job 2.3 API测试 2.3.1 细节 2.4 配置 2.5 核心类说明 三.Trigger触发器 3.1 S ...

  9. 分布式定时任务调度实战

    目录 1.为什么需要定时任务 2.定时任务调度框架 2.1 单机 2.2 分布 3.xxl-job和elastic-job对比 3.1 支持集群部署方式 3.2 多节点部署任务执行方式 3.3 日志可 ...

最新文章

  1. hadoop集群安装
  2. DDoS deflate的安装使用
  3. Docker安装部署RabbitMQ
  4. tf.nn.conv2d 与tf.layers.conv2d的区别
  5. 【Beta阶段】M2事后分析
  6. python学习-defaultdict
  7. java addlast_Java中的LinkedList addLast()方法: java.util.LinkedList.addLast() - Break易站
  8. Git:add多个文件或者目录的方式
  9. python课程报告模板_Python制作WORD报告
  10. 《Android框架揭秘》——1.2节通过启动过程分析Android Framework
  11. 交叉熵和相对熵(KL散度)
  12. 吃鸡显示连接服务器超时,吃鸡 怎么显示连接超时 | 手游网游页游攻略大全
  13. UI控件--时间选择(日期拾取器)
  14. HTML表格边框空隙
  15. Cb Vc 经典大讨论(很长的一篇文章!)?
  16. 基础光照-Phong 光照模型
  17. 常用生物信息 ID 及转换方法
  18. 计算机无法关闭密码保护共享,xp系统怎么关闭密码保护共享
  19. web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentia
  20. MDA核心之MOF原理和实现

热门文章

  1. 预科知识整理【计算机、硬件、软件、快捷键、Dos命令】
  2. 苹果 App Store账号申请和证书申请发布app等知识
  3. NLP学习笔记(二)
  4. 文本分析合集,文本向量处理的方法jieba,对文本的特征工程之TfidfVectorizer以及结合TruncatedSVD,WordCloud词云图展示
  5. pcep协议什么意思_协议是什么意思??
  6. 核心频率个加速频率_RTX 3090液氮超频可将核心频率推高到2580MHz
  7. 将oracle中的逗号转成全角,ORACLE SQL半角全角转换
  8. [Java Base] 类,接口,枚举,静态常量到底应该放在哪?
  9. CSDN的markdown语法
  10. 测试恋爱值的软件,全国恋爱等级测试