Spring 定时任务

个人专题目录

ActiviMQ专题

链路追踪

Dubbo专题

Docker专题

Git专题

Idea专题

Java阿里P6+必会专题

Java工具类

Kafka专题

Linux专题

Maven专题

Markdown专题

Mysql专题

Netty专题

Nginx专题

Openstack专题

Redis专题

Spring专题

SpringBoot专题

SpringCloud专题

Zookeeper专题

个人随笔专题

数据结构专题

单点登录专题

设计模式专题

架构优化专题


一、实现方法

spring中实现的定时任务,大致有四种方法:

  • web接口,使用crontab调用
  • 使用@Scheduled注解
  • 使用Quartz
  • java类继承TimerTask

这四类的比较如下:

方法 配置项 集群模式 使用场景
web接口 较少 集群模式下需要申请域名,通过域名调用 web项目
@Scheduled注解 不支持集群模式,集群模式下每个节点都会调用注解标示的任务 单节点项目
TimerTask 不支持集群模式,集群模式下每个节点都会调用注解标示的任务 单节点项目
Quartz 需要额外数据库来支持集群模式 分布式项目

下面详细介绍下基于注解和quartz的使用方法。

二、@Scheduled注解

2.1 依赖包

Scheduled依赖包

<dependency> 
        <groupId>org.springframework</groupId> 
        <artifactId>spring-context</artifactId> 
        <version>${spring.version}</version> 
</dependency>

2.2 配置文件

Schedule配置文件

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns:task="http://www.springframework.org/schema/task" 
    xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context" 
    xsi:schemaLocation=" 
http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
http://www.springframework.org/schema/context 
http://www.springframework.org/schema/context/spring-context-3.0.xsd 
http://www.springframework.org/schema/task 
http://www.springframework.org/schema/task/spring-task-3.1.xsd"> 
   
    <!-- 开启定时任务 --> 
    <task:annotation-driven />
    <!-- 配置线程池,若不配置所有的定时任务会串行执行 --> 
        <task:annotation-driven scheduler="myScheduler"/>  
    <task:scheduler id="myScheduler" pool-size="5"/> 
   
    <!-- 开启注解 --> 
    <context:annotation-config /> 
    <!-- 指定相关的包路径 --> 
    <context:component-scan base-package="com.spring.task"/>   <!-- MyTask中使用注解 -->
  
    <!-- 当然你也可以不在java类中使用注解,此时需要如下配置 -->        
        <bean id="myTask" class="com.spring.task.MyTask2"></bean>  <!-- MyTask2中不使用注解,在配置文件配置执行信息 --> 
    <task:scheduled-tasks>
      <!-- 这里表示的是每隔五秒执行一次 -->
      <task:scheduled ref="myTask2" method="show" cron="*/5 * * * * ?" />
      <task:scheduled ref="myTask2" method="print" cron="*/10 * * * * ?"/>
    </task:scheduled-tasks>
</beans>

2.3 Job类实现

Scheduled任务类

package com.spring.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
  
/**
 * 基于注解的定时器任务
 */
@Component
public class MyTask {
  
  /**
   * 定时计算。每天凌晨 01:00 执行一次
   */
  @Scheduled(cron = "0 0 1 * * *")
  public void show() {
    System.out.println("show method 2");
  }
  
  /**
   * 启动时执行一次,之后每隔2秒执行一次
   */
  @Scheduled(fixedRate = 1000*2) 
  public void print() {
    System.out.println("print method 2");
  }
}
非注解任务  展开原码
package com.spring.task;
  
/**
 * 不使用注解,自定义任务,此时就是普通的java类
 */
public class MyTask2 {
  
  public void show() {
    System.out.println("show method 1");
  }
  
  public void print() {
    System.out.println("print method 1");
  }
}

三、Quartz

3.1 Quartz任务调度的基本实现原理

Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于Java实现。作为一个优秀的开源调度框架,Quartz具有以下特点:

(1)强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;

(2)灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;

(3)分布式和集群能力,Terracotta收购后在原来功能基础上作了进一步提升。本文将对该部分相加阐述。

3.1.1 Quartz 核心元素

Quartz任务调度的核心元素为:Scheduler——任务调度器、Trigger——触发器、Job——任务。其中trigger和job是任务调度的元数据,scheduler是实际执行调度的控制器。

Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。这四种trigger可以满足企业应用中的绝大部分需求。

Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:requestsRecovery和durability,其中requestsRecovery表示任务是否在发生故障的时候在其他节点执行,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。

Scheduler由scheduler工厂创建:DirectSchedulerFactory或者StdSchedulerFactory。第二种工厂StdSchedulerFactory使用较多,因为DirectSchedulerFactory使用起来不够方便,需要作许多详细的手工编码设置。Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。

Quartz核心元素之间的关系如图所示:

01.png

​ 核心元素关系图

3.1.2 Quartz 线程视图

在Quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。

02.png

​ Quartz线程视图

Scheduler调度线程主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。

3.1.3 Quartz Job数据存储

Quartz中的trigger和job需要存储下来才能被使用。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。

03.png

3.2 Quartz集群原理

3.2.1 Quartz 集群架构

一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的,如图所示。

04.png

​ Quartz集群架构

3.2.2 Quartz集群相关数据库表

因为Quartz集群依赖于数据库,所以必须首先创建Quartz数据库表,Quartz发布包中包括了所有被支持的数据库平台的SQL脚本。这些SQL脚本存放于<quartz_home>/docs/dbTables 目录下。这些表的简要介绍如和创建语句如下。

数据表说明

# 数据库所需表
# QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 信息
# QRTZ_CRON_TRIGGERS 存储 Cron Trigger,包括Cron表达式和时区信息
# QRTZ_FIRED_TRIGGERS 存储与已触发的 Trigger 相关的状态信息,以及相联 Job的执行信息QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的 Trigger组的信息
# QRTZ_SCHEDULER_STATE 存储少量的有关 Scheduler 的状态信息,和别的Scheduler实例(假如是用于一个集群中)
# QRTZ_LOCKS 存储程序的悲观锁的信息(假如使用了悲观锁)
# QRTZ_JOB_DETAILS 存储每一个已配置的 Job 的详细信息
# QRTZ_JOB_LISTENERS 存储有关已配置的 JobListener的信息
# QRTZ_SIMPLE_TRIGGERS存储简单的Trigger,包括重复次数,间隔,以及已触的次数
# QRTZ_BLOG_TRIGGERS Trigger 作为 Blob 类型存储(用于 Quartz 用户用JDBC创建他们自己定制的 Trigger 类型,JobStore并不知道如何存储实例的时候)
# QRTZ_TRIGGER_LISTENERS 存储已配置的 TriggerListener的信息
# QRTZ_TRIGGERS 存储已配置的 Trigger 的信息

重要表说明:

调度器状态表(QRTZ_SCHEDULER_STATE)

说明:集群中节点实例信息,Quartz定时读取该表的信息判断集群中每个实例的当前状态。

instance_name:配置文件中org.quartz.scheduler.instanceId配置的名字,如果设置为AUTO,quartz会根据物理机名和当前时间产生一个名字。

last_checkin_time:上次检入时间

checkin_interval:检入间隔时间

触发器与任务关联表(qrtz_fired_triggers)

存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息。

触发器信息表(qrtz_triggers)

trigger_name:trigger的名字,该名字用户自己可以随意定制,无强行要求

trigger_group:trigger所属组的名字,该名字用户自己随意定制,无强行要求

job_name:qrtz_job_details表job_name的外键

job_group:qrtz_job_details表job_group的外键

trigger_state:当前trigger状态设置为ACQUIRED,如果设为WAITING,则job不会触发

trigger_cron:触发器类型,使用cron表达式

任务详细信息表(qrtz_job_details)

说明:保存job详细信息,该表需要用户根据实际情况初始化

job_name:集群中job的名字,该名字用户自己可以随意定制,无强行要求。

job_group:集群中job的所属组的名字,该名字用户自己随意定制,无强行要求。

job_class_name:集群中job实现类的完全包名,quartz就是根据这个路径到classpath找到该job类的。

is_durable:是否持久化,把该属性设置为1,quartz会把job持久化到数据库中

job_data:一个blob字段,存放持久化job对象。

权限信息表(qrtz_locks)

3.2.3 Quartz Scheduler在集群中的启动流程

Quartz Scheduler自身是察觉不到被集群的,只有配置给Scheduler的JDBC JobStore才知道。当Quartz Scheduler启动时,它调用JobStore的schedulerStarted()方法,它告诉JobStore Scheduler已经启动了。schedulerStarted() 方法是在JobStoreSupport类中实现的。JobStoreSupport类会根据quartz.properties文件中的设置来确定Scheduler实例是否参与到集群中。假如配置了集群,一个新的ClusterManager类的实例就被创建、初始化并启动。ClusterManager是在JobStoreSupport类中的一个内嵌类,继承了java.lang.Thread,它会定期运行,并对Scheduler实例执行检入的功能。Scheduler也要查看是否有任何一个别的集群节点失败了。检入操作执行周期在quartz.properties中配置。

3.2.4 侦测失败的Scheduler节点

当一个Scheduler实例执行检入时,它会查看是否有其他的Scheduler实例在到达他们所预期的时间还未检入。这是通过检查SCHEDULER_STATE表中Scheduler记录在LAST_CHEDK_TIME列的值是否早于org.quartz.jobStore.clusterCheckinInterval来确定的。如果一个或多个节点到了预定时间还没有检入,那么运行中的Scheduler就假定它(们) 失败了。

3.2.5 从故障实例中恢复Job

当一个Sheduler实例在执行某个Job时失败了,有可能由另一正常工作的Scheduler实例接过这个Job重新运行。要实现这种行为,配置给JobDetail对象的Job可恢复属性必须设置为true(job.setRequestsRecovery(true))。如果可恢复属性被设置为false(默认为false),当某个Scheduler在运行该job失败时,它将不会重新运行;而是由另一个Scheduler实例在下一次触发时间触发。Scheduler实例出现故障后多快能被侦测到取决于每个Scheduler的检入间隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。

3.3 Quartz集群实例(Quartz+Spring)

3.3.1 依赖包

Quartz依赖

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>${quartz.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${spring.version}</version>
</dependency>

3.3.2 配置文件

有三类配置文件:数据源配置、quartz基本配置、quartz任务配置

3.3.2.1 数据源配置

1.jdbc配置

jdbc配置

quartz.url=jdbc:mysql://10.111.17.78:3306/kec_scheduler?useUnicode=true&characterEncoding=utf8
quartz.username=root
quartz.password=root
quartz.driverClassName=com.mysql.jdbc.Driver

2.数据源定义

数据源配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  
    <!-- 属性文件读入 -->
    <bean id="propertyConfigurer"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:database.properties</value>
            </list>
        </property>
    </bean>
  
    <!-- 数据源定义,使用c3p0 连接池 -->
    <bean id="quartzDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
          destroy-method="close">
        <property name="driverClass" value="${quartz.driverClassName}" />
        <property name="jdbcUrl" value="${quartz.url}" />
        <property name="user" value="${quartz.username}" />
        <property name="password" value="${quartz.password}" />
        <property name="initialPoolSize" value="2" />
        <property name="minPoolSize" value="10" />
        <property name="maxPoolSize" value="20" />
        <property name="acquireIncrement" value="2" />
        <property name="maxIdleTime" value="1800" />
    </bean>
  
    <!-- 使用jdbc访问数据库 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="quartzDataSource" />
    </bean>
</beans>

3.3.2.2 quartz基本配置

quartz基本配置

#==============================================================   
#Configure Main Scheduler Properties   
#============================================================== 
org.quartz.scheduler.instanceName = DimServerQuartz     # 可为任何值,用在 JDBC JobStore 中来唯一标识实例,但是所有集群节点中必须相同
org.quartz.scheduler.instanceId = AUTO                  # 基于主机名和时间戳来产生实例ID
  
#==============================================================   
#Configure ThreadPool   
#============================================================== 
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
  
  
#==============================================================   
#Configure JobStore   
#============================================================== 
org.quartz.jobStore.misfireThreshold = 60000
# JobStoreTX,将任务持久化到数据中。因为集群中节点依赖于数据库来传播 Scheduler 实例的状态,你只能在使用 JDBC JobStore 时应用 Quartz 集群。
# 这意味着你必须使用 JobStoreTX 或是 JobStoreCMT 作为 Job 存储;你不能在集群中使用 RAMJobStore。
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# 根据选择的数据库类型不同而不同,我这里的是mysql,所以是org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_               # 表前缀
org.quartz.jobStore.maxMisfiresToHandleAtATime=10
org.quartz.jobStore.isClustered = true                # 属性为 true,你就告诉了 Scheduler 实例要它参与到一个集群当中
org.quartz.jobStore.clusterCheckinInterval = 3600000  # 调度实例失效的检查时间间隔,检查间隔3600s
  
#============================================================== 
#Configure DataSource  我通过配置文件引入数据源信息,如2.1中的配置
#==============================================================
# org.quartz.jobStore.dataSource = myDS     # 别名
# org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
# org.quartz.dataSource.myDS.URL = jdbc:mysql://192.168.31.18:3306/test?useUnicode=true&amp;characterEncoding=UTF-8
# org.quartz.dataSource.myDS.user = root
# org.quartz.dataSource.myDS.password = 123456
# org.quartz.dataSource.myDS.maxConnections = 30
  
org.quartz.scheduler.skipUpdateCheck = true                                                 # 不检查版本更新
# org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin # 打印步骤信息,可在调试时使用
org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin     # 停止时的清理插件
org.quartz.plugin.shutdownhook.cleanShutdown = true

3.3.2.3 quartz任务配置

quartz任务配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  
    <!-- 数据库配置文件 -->
    <import resource="classpath:datasource.xml"></import>
         
        <!-- 为了支持使用注解所写的类 -->
    <bean id="jobFactory" lazy-init="false" autowire="no" class="com.test.scheduler.api.AutoWiredJobFactory"/>  
  
    <bean name="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="jobFactory" ref="jobFactory"/>         <!-- 正常情况下QuartzJobBean是无法使用注解的,若想使用需引入配置,该类的实现下面会展示 -->
        <property name="overwriteExistingJobs" value="true"/>  <!-- 是否覆盖已有job,防止启动时重复执行停止时的任务 -->
        <property name="dataSource">                           <!-- 数据源配置 -->
            <ref bean="quartzDataSource" />
        </property>
        <property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
        <property name="configLocation" value="classpath:quartz/quartz.properties" />   <!-- quartz基本配置 -->
  
        <property name="triggers">                             <!-- 在此添加自定义的trigger -->
            <list>
                <ref bean="SyncTrigger" />
            </list>
        </property>
    </bean>
                
        <!-- 自定义的trigger -->
    <bean id="SyncTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="jobDetail" ref="SyncJob" />
        <property name="cronExpression" value="0/10 * * * * ?" />   <!-- 每隔10s执行一次 -->
    </bean>
  
    <!-- 自定义的job -->
    <bean id="SyncJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
        <property name="durability" value="true"></property>       <!-- 是否持久化,集群模式时需为true -->
        <property name="requestsRecovery" value="true"></property> <!-- 失败或者重启时是否在其他节点恢复 -->
        <property name="jobClass"                                  <!-- 继承QuartzJobBean的自定义任务实现类 -->
                  value="com.scheduler.api.SyncScheduler.SyncJob">
        </property>
    </bean>
 
</beans>

3.3.3 Job类实现

自定义的任务(job),需要继承QuartzJobBean并实现其中的executeInternal函数,另外为了支持使用注解需要一个额外的AutoWiredJobFactory类。

3.3.3.1 Job类

自定义Job

package com.scheduler.api.SyncScheduler;
  
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Service;
  
@Service
@DisallowConcurrentExecution  // 不允许并发执行
public class SyncJob extends QuartzJobBean {
  
    @Autowired
    private JdbcTemplate jdbcTemplate;
  
    private Logger logger = LoggerFactory.getLogger(SyncJob.class);
  
    @Override
    public void executeInternal(JobExecutionContext jobexecutioncontext) throws JobExecutionException {
  
        //这里执行定时调度业务
        logger.info("testMethod.......1");
        System.out.println("2--testMethod......."+System.currentTimeMillis()/1000);
    }
}

3.3.3.2 AutoWiredJobFactory类

AutoWiredJobFactory类

package com.scheduler.api;
  
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
  
/**
 * 供 quartz scheduler 使用,使其支持注解
 */
public class AutoWiredJobFactory extends AdaptableJobFactory {
  
    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;
  
    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        //调用父类方法
        Object jobInstance = super.createJobInstance(bundle);
        //进行注入
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

3.4 注意事项

3.4.1 时间同步问题

Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。

节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。

3.4.2 节点争抢Job问题

因为Quartz使用了一个随机的负载均衡算法, Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。

3.4.3 从集群获取Job列表问题

当前,如果不直接进到数据库查询的话,还没有一个简单的方式来得到集群中所有正在执行的Job列表。请求一个Scheduler实例,将只能得到在那个实例上正运行Job的列表。Quartz官网建议可以通过写一些访问数据库JDBC代码来从相应的表中获取全部的Job信息。

3.5 参考文档

http://www.cnblogs.com/zhenyuyaodidiao/p/4755649.html (Quartz集群原理及配置应用)

http://veiking.iteye.com/blog/2372284 (Quartz在集群、分布式系统中的应用)

http://www.jianshu.com/p/14f86c6efe22 (分布式定时任务(二))

http://www.jianshu.com/p/a518dd3229de (分布式定时任务(三))

推荐阅读更多精彩内容

  • 概述 了解Quartz体系结构 Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个...
    张晨辉Allen阅读 1,138评论 2 11
  • 本文参考自 Spring官方文档 34. Task Execution and Scheduling 。在程序中常...
    大爽兔阅读 399评论 0 41
  • pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 ...
    王震阳阅读 78,829评论 25 511
  • 本文参考自Spring官方文档 34. Task Execution and Scheduling。 在程序中常常...
    乐百川阅读 4,258评论 0 11
  • 美好的一天,早安![太阳] 向日葵说,只要你朝着阳光努力向上,生活便会因此变得单纯而美好。美好的一天开始,愿你能向...
    蜜思小琳阅读 51评论 0 0