spring(基础五) spring实现后台的任务调度TimerTask和Quartz
强烈推介IDEA2020.2破解激活,IntelliJ IDEA 注册码,2020.2 IDEA 激活码
最近整后台,涉及到两个后台调度的问题。
一是以时间间隔为条件的轮询调度;
运用场景:每隔5分钟抓取数据;
二是一某个时间点为条件的轮询调度;
运用场景:后台日志货报表生成上传,每个周一生成上一周的,每个月初生成上一月。
其实按周来执行调度,用前面一个场景也可以实现,但是按月生成,因为每月时间不固定,必须动态判断和执行。
后台实现调度的思路,我一开始考虑的是,web启动时通过一个入口方法,启动一个while线程(假设我需要每隔5分钟发送某个请求task)一直监控。
task执行一次后,sleep(5分钟),或是设置一个哨兵时间点latestTime,获取当前时间currentTime,相减判断是否大于5分钟来判断是否再次执行task。
后一种方法虽然只是不停的执行2-3条指令,理论上觉得sleep来挂起线程应该更加节省资源,猜测。
简短流程:
任务入口类executor,执行exe方法来初始化启动loopThread,然后一直跑。
即Executor——》LoopThread——》Task;
后来发现spring配合Quartz或是timetask可以更加简单的实现,可以把线程生命周期等一些问题都交给Spring来管理。
通过配置后,直接到上面的—》TASK
下面根据场景1,实现简单的后台任务调度。
第一步:定义一个任务类Task。
然后把Task注入到spring管理的bean中,一般会有几种方法,最简单的一般我们本身都会配置一个bean.xml来
<!-- 1:配置注解的自动扫描的范围 -->
代码语言:javascript复制<context:component-scanbase-package="com.yourpro"/>
就是只需在类上加上
@Repository @Component
等自动注入该类了.
或是:
<beanid="task"class="com.xx.xx.Task">
第二步配置:
代码语言:javascript复制<!-- step3 -->
<bean id="targetTask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject">
<ref bean="task"/><--!这个bean引用的就是我们前面注入的bean,Task累,id为task-->
</property>
<property name="targetMethod">
<value>taskExe</value><!--指点目标类中的目标方法,即,需要执行Task中额taskExe方法-->
</property>
</bean>
<bean id="task<span style="font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;">Trigger</span>" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail">
<ref bean="<span style="font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;">targetTask</span>"/><!--执行详情task为上面那个配置的bean-->
</property>
<property name="startDelay"><!--开始延时时间-->
<value>0</value>
</property>
<property name="repeatInterval">
<!-- 轮询时间间隔 -->
<value>300</value>
</property>
</bean>
<!--step1:下面这个可以比作web启动后调度的启动器-->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref local="<span style="font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;">task</span><span style="font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;">Trigger</span>"/>
</list>
</property>
</bean>
我们可以看到基本启动流程是 step1启动了一个定时器taskTrigger,定时器设置了轮询时间参数,以及指定执行的任务类,
最上面的bean申明了任务类和任务方法。
按时间间隔的后台任务调度基本就这样简单实现了。
场景二实现:根据某个时间点时间点
只是将第二个bean,也就四定时器设置改成可以配置时间点。
代码语言:javascript复制<bean id="<span style="font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;">task</span><span style="font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;">Trigger</span>" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail">
<ref bean="targetTask"/>
</property>
<property name="cronExpression">
<value>0 0 0 1 * ?</value><该设置表示每月1号0点0分0秒执行,具体可以CronTrigger获取更多资料>
</property>
</bean>
上面就可以简单实现固定时间的task任务。
对于task类,也就是step1中配置的那个任务类,还可以用一个更加简单的继承实现。
使用spring Quartz,task类继承QuartzJobBean;
然后step1配置成:
代码语言:javascript复制<bean id="targetTask" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass"> <p class="p1"><span class="s1"><</span><span class="s2">ref</span><span class="s3"> </span><span class="s4">bean</span><span class="s3">=</span>"task"<span class="s1">/></span></p> </property>
</bean>
注意注入内容,一般就这样,无需指定方法,应为继承了QuartzJobBean后默认会让你实现一个executeInternal(.....)方法;
最后说下如何实现一个多任务的线程,从上面我们也看到,实现的都是一个简单的任务;
当然其实实现多线程任务也很简单,就是在将原本直接启动Task那步改成,启动一个MultipTaskEngine;
该多重任务引擎在生成多个子线程来执行;直接一个for循环将Task改造成线程就行了,后来一作后台的同事Spring直接可以
配置一个多线程池来实现;好处还是Spring带我们管理了线程生命周期,线程性能优化等。
具体怎么实现:
如上面所说,现在需要两个类了,一个是MultipTaskEngine类,会被配置到bean.xml中作为启动引擎类;该类继承TimerTask
另一个类就是Task任务线程类;
配置文件:
代码语言:javascript复制<!-- 6: 异步线程池 -->
<bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="10" />
<!-- 最大线程数 -->
<property name="maxPoolSize" value="100" />
<!-- 队列最大长度 >=mainExecutor.maxSize -->
<property name="queueCapacity" value="1000" />
<!-- 线程池维护线程所允许的空闲时间 -->
<property name="keepAliveSeconds" value="300" />
<!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
<!-- 主任务 负责扫描任务 将任务分配给线程完成 -->
<bean id="multipTaskEngine" class="com.xxx.MultipTaskEngine">
<property name="threadPool" ref="threadPool" />
</bean>
<bean id="taskTrigger" class="org.springframework.scheduling.concurrent.ScheduledExecutorTask">
<property name="runnable" ref="multipTaskEngine" /><!-- 线程方法 -->
<!-- 容器加载10秒后开始执行 -->
<property name="delay" value="5000" />
<!-- 每次任务间隔 1分钟-->
<property name="period" value="60000"/>
<!-- 固定间隔,否则默认fixDelay会等到前一个任务完成后才开始计时. -->
<property name="fixedRate" value="true" />
</bean>
<!-- 多线程调度 -->
<bean id="springScheduledExecutorFactoryBean" class="org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean">
<property name="scheduledExecutorTasks" >
<list>
<ref bean="<span style="font-family: Arial, Helvetica, sans-serif;">taskTrigger</span><span style="font-family: Arial, Helvetica, sans-serif;">" /></span>
</list>
</property>
</bean>
在MutipTaskEngine类中手动注入
代码语言:javascript复制private ThreadPoolTaskExecutor threadPool;
public void setThreadPool(ThreadPoolTaskExecutor threadPool) {
this.threadPool = threadPool;
}
生成字线程Task直接
代码语言:javascript复制for (int i = 0; i < n; i ) {
threadPool.execute(new UserRegisterReportThread(i));
}
这边要注意一点的是,如果新起的task子线程中有注入的bean,会产生
spring 子线程中注入bean为空,一开始我就犯了这个问题
后来网上找到一个答案:
有时候需要启动一个后台守护线程,做一些别的事情。这时候怎么获取spring里的Service、Dao、Action等对象?
(注意自己new一个是不行的,因为脱离了spring的管理,其中IoC资源都没有被注入)。
一个解决办法是,重新弄一个Spring:
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource( "applicationContext.xml"));
// 从配置文件中获取对象 IService hello = (IService) factory.getBean("service"); hello.service("Helloween");
factory.destroySingletons();
这是最笨的办法,也是不科学的。这样的话,web中会有两个spring容器,两套service、dao等。
---------------------我是分割线----------------------------------
比较科学的办法,是用Spring的方法获取到当前的容器(也是当前应用下唯一的spring容器),并从中获取资源。代码为:
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); IService service = (IService) context.getBean("service");
--------------------我是分割线-------------------------
ContextLoader.getCurrentWebApplicationContext()方法 可以从spring的加载listener中追踪到:
<listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener>
我 用:WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); IService service = (IService) context.getBean("service");
类方法来获取注入的bean,解决了以上问题。