没有需求,就没有折腾。
首先,阐述一下背景。
早上迷迷糊糊地开始了春节后的一天上班日程,脑袋还在噼里啪啦放烟花,项目管理部和SEO小伙伴就提了一桶凉水过来,往我头上一浇,瞬间烟花都湮灭了。
“给官网加个定时发布文章的功能吧。”
“啥?”
“我们的官网,每次新增文章都是立即执行静态化并进行发布,现在周末也需要发布文章,SEO周末是不上班哒,所以,请给我们开发一个定时发布文章的功能吧。”
“啊?”
“评估一下时间,越快越好。”
“...”
“需求了解了吧,那就这样,尽快产出哦。”
“...”
脑袋还在宕机中。
喂喂喂,那你们浇灭了我的烟花,都不用赔一下吗,真不厚道。
虽然我不喜欢频繁需求变动,但是我爱折腾。
不过这么简单的功能,貌似也算不上折腾,但是记录下来也许能帮助到别人呢,Hard to say。
环境说明
1、centOS 服务器一台
2、基于SSM + 一些没必要在这里提到的第三方控件
3、Bootstrap前端框架
4、最最重要的是:帅比码农一枚
其实,上面前三点都没必要提及,主要是基于Java环境来实现定时任务。所以最重要的,请记住第四点,强调,是第四点。
思路
SEO通过管理后台新增文章,但是并不是立即发布,而是可以手动选择发布方式,包括立即发布和定时发布,定时发布可以指定一个时间,交由系统自动实现发布功能。
说了跟没说似的,原谅我,帅比码农说话都比较高(zhuang)深(shen)莫(nong)测(gui)。
1、前端通过bootstrap-datepicker插件,在文章表单中新增一个发布时间的选择控件。具体使用方式请参考官网API或留言。
<!-- 页面元素 -->
<div class="input-append date form_datetime">
<input id="pubTime" name="pubTime" size="16" type="text" value="" readonly>
<span class="add-on"><i class="icon-th"></i></span>
</div>
<!-- Javascript -->
<script type="text/javascript">
$(".form_datetime").datetimepicker({
language:"zh-CN",
showMeridian: true,
todayBtn:true,
startDate:new Date(),
format: "yyyy-mm-dd hh:ii:ss"
}).on('changeDate', function(ev){
$('#pubTiming').attr('checked',true);//通过事件,实现[定时发布]单选按钮的联动选择
});
</script>
2、后台新增文章的方法,新增入参[发布方式-pubType]和[发布时间-pubTime]来接收表单传递过来的值,当用户选择发布方式为定时发布时,要求发布时间必须选择。
由于这里是以实体的方式来接收表单的,只需要在Article实体中新增pubType和pubTime两个属性,并生成getter和setter即可接收表单值。
部分代码如下
/**
* 新增文章
*
* @param article 文章实体
* @param request 请求
* @return ResponseBean 响应实体
*/
@RequestMapping("/add")
@ResponseBody
public ResponseBean add(Article article, HttpServletRequest request) {
boolean success = articleService.add(article, request);
...
}
/**
* 文章
*
* @author zoro
* @version 1.0
* @since 2018/02/23
*/
public class Article implements Serializable {
...
private Integer pubType;//发布方式,1立即,2定时
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date pubTime;//定时发布的时间
public Integer getPubType() {
return pubType;
}
public void setPubType(Integer pubType) {
this.pubType = pubType;
}
public Date getPubTime() {
return pubTime;
}
public void setPubTime(Date pubTime) {
this.pubTime = pubTime;
}
...
}
3、将文章内容和发布状态保存到数据库,如果是立即发布,则执行文章渲染,通过模板渲染成html文件,以供访问。
articleRender.rendering();
4、如果是定时发布的话,就需要建立定时任务。
这里有几种情况需要说明:
- 新增文章
直接保存文章,并建立定时任务。 - 修改文章
修改文章会存在不同时间点重复发布任务的可能性,所以需要特殊处理。
针对修改文章,每次新建定时任务的时候,先判断是否存在同一篇文章的定时任务,如果有,则标识该任务为取消状态(取消状态下的任务,任务体不会执行任何操作),并从id映射和缓存中移除
文章任务部分代码如下
package com.andatech.admin.service;
...
import com.andatech.tools.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 文章发布任务
*
* @author Zoro
* @date 2018/2/23
* @since 1.0
*/
public class ArticlePublishJob implements Runnable {
/**logger*/
private static final Logger LOGGER = LoggerFactory.getLogger(ArticlePublishJob.class);
/**id映射*/
private static volatile ConcurrentHashMap<String, String> idMapping = new ConcurrentHashMap<>();
/**任务缓存*/
private static volatile ConcurrentHashMap<String, ArticlePublishJob> cache = new ConcurrentHashMap<>();
private volatile AtomicBoolean canceled = new AtomicBoolean(false);//任务取消状态
private String jobId;//任务id
private String articleUuid;//文章id
private ArticleRender articleRender;//文章渲染器
public ArticlePublishJob(ArticleRender articleRender) {
this.jobId = IdGenerator.plainJdkUUID();
this.articleRender = articleRender;
this.articleUuid = articleRender.getArticle().getUuid();
//取消并清除上一次任务
cancelAndClearLastJobIfExist();
//缓存本次任务
cacheThisJob();
}
/**
* 取消并清除上一次任务
*/
private void cancelAndClearLastJobIfExist(){
if (StringUtil.isNotEmpty(idMapping.get(articleUuid))) {
ArticlePublishJob lastJob = cache.get(idMapping.get(articleUuid));
if (null != lastJob) {
lastJob.cancelJob();
cache.remove(idMapping.get(articleUuid));
idMapping.remove(articleUuid);
}
}
}
/**
* 缓存本次任务
*/
private void cacheThisJob(){
//id映射
idMapping.put(this.articleUuid, this.jobId);
//文章发布任务缓存
cache.put(this.jobId, this);
}
@Override
public void run() {
//判断任务是否被取消
if (!canceled.get()) {
ArticleService articleService = (ArticleService) SpringContextHolder.getBean("articleService");
//渲染
try {
articleRender.rendering();
} catch (IOException e) {
LOGGER.error("render log err:" + e.getMessage(), e);
}
//更新文章状态
Article updArticle = new Article();
updArticle.setUuid(articleRender.getArticle().getUuid());
updArticle.setStatus(Article.STATUS_NORMAL);
articleService.edit(updArticle);
//从缓存中清理本任务
clear();
}
}
/**
* 取消任务
*/
public void cancelJob() {
this.canceled.set(true);
}
/**
* 清理缓存
*/
public void clear() {
idMapping.remove(articleUuid);
cache.remove(this.jobId);
}
}
5、具体定时任务方式,包括以下几种
- Thread方式:线程等待,不安全。
- timer方式:线程资源没有复用。
- 任务调度框架,比如Quartz等:需要继承框架。
- ScheduledExecutorService方式:被相中了。
综上分析,选择了最后一种,也是较好的选择之一,下面给出最简单的用法,如有深入需要,建议查看JavaAPI。
部分代码如下
/**创建线程池*/
public static ScheduledExecutorService service = Executors.newScheduledThreadPool(50);
/**新建任务,并设定执行时间*/
ArticlePublishJob job = new ArticlePublishJob(articleRender);
long delay = article.getPubTime().getTime() - System.currentTimeMillis();
service.schedule(job, delay, TimeUnit.MILLISECONDS);
测试,大功告成。
总结
不结合业务来说,定时任务的创建无非就"第5点"中的几种方式,熟悉API并熟练使用即可。
结合业务情况下,需要考虑任务是否会重复,重复了怎么处理等问题。