Java定時(shí)發(fā)布文章簡單方案

沒有需求,就沒有折騰。

首先,闡述一下背景。

早上迷迷糊糊地開始了春節(jié)后的一天上班日程,腦袋還在噼里啪啦放煙花,項(xiàng)目管理部和SEO小伙伴就提了一桶涼水過來,往我頭上一澆,瞬間煙花都湮滅了。

“給官網(wǎng)加個(gè)定時(shí)發(fā)布文章的功能吧。”

“啥?”

“我們的官網(wǎng),每次新增文章都是立即執(zhí)行靜態(tài)化并進(jìn)行發(fā)布,現(xiàn)在周末也需要發(fā)布文章,SEO周末是不上班噠,所以,請(qǐng)給我們開發(fā)一個(gè)定時(shí)發(fā)布文章的功能吧?!?/p>

“???”

“評(píng)估一下時(shí)間,越快越好?!?/p>

“...”

“需求了解了吧,那就這樣,盡快產(chǎn)出哦。”

“...”

腦袋還在宕機(jī)中。

喂喂喂,那你們澆滅了我的煙花,都不用賠一下嗎,真不厚道。

雖然我不喜歡頻繁需求變動(dòng),但是我愛折騰。

不過這么簡單的功能,貌似也算不上折騰,但是記錄下來也許能幫助到別人呢,Hard to say。

環(huán)境說明

1、centOS 服務(wù)器一臺(tái)

2、基于SSM + 一些沒必要在這里提到的第三方控件

3、Bootstrap前端框架

4、最最重要的是:帥比碼農(nóng)一枚

其實(shí),上面前三點(diǎn)都沒必要提及,主要是基于Java環(huán)境來實(shí)現(xiàn)定時(shí)任務(wù)。所以最重要的,請(qǐng)記住第四點(diǎn),強(qiáng)調(diào),是第四點(diǎn)。

思路

SEO通過管理后臺(tái)新增文章,但是并不是立即發(fā)布,而是可以手動(dòng)選擇發(fā)布方式,包括立即發(fā)布定時(shí)發(fā)布,定時(shí)發(fā)布可以指定一個(gè)時(shí)間,交由系統(tǒng)自動(dòng)實(shí)現(xiàn)發(fā)布功能。

說了跟沒說似的,原諒我,帥比碼農(nóng)說話都比較高(zhuang)深(shen)莫(nong)測(gui)。

1、前端通過bootstrap-datepicker插件,在文章表單中新增一個(gè)發(fā)布時(shí)間的選擇控件。具體使用方式請(qǐng)參考官網(wǎng)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);//通過事件,實(shí)現(xiàn)[定時(shí)發(fā)布]單選按鈕的聯(lián)動(dòng)選擇
        });
</script> 
界面

2、后臺(tái)新增文章的方法,新增入?yún)發(fā)布方式-pubType]和[發(fā)布時(shí)間-pubTime]來接收表單傳遞過來的值,當(dāng)用戶選擇發(fā)布方式為定時(shí)發(fā)布時(shí),要求發(fā)布時(shí)間必須選擇。

由于這里是以實(shí)體的方式來接收表單的,只需要在Article實(shí)體中新增pubType和pubTime兩個(gè)屬性,并生成getter和setter即可接收表單值。

部分代碼如下

    /**
     * 新增文章
     *
     * @param article 文章實(shí)體
     * @param request 請(qǐng)求
     * @return ResponseBean 響應(yīng)實(shí)體
     */
    @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;//發(fā)布方式,1立即,2定時(shí)
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private Date pubTime;//定時(shí)發(fā)布的時(shí)間
        
        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、將文章內(nèi)容和發(fā)布狀態(tài)保存到數(shù)據(jù)庫,如果是立即發(fā)布,則執(zhí)行文章渲染,通過模板渲染成html文件,以供訪問。

articleRender.rendering();

4、如果是定時(shí)發(fā)布的話,就需要建立定時(shí)任務(wù)。
這里有幾種情況需要說明:

  • 新增文章
    直接保存文章,并建立定時(shí)任務(wù)。
  • 修改文章
    修改文章會(huì)存在不同時(shí)間點(diǎn)重復(fù)發(fā)布任務(wù)的可能性,所以需要特殊處理。

針對(duì)修改文章,每次新建定時(shí)任務(wù)的時(shí)候,先判斷是否存在同一篇文章的定時(shí)任務(wù),如果有,則標(biāo)識(shí)該任務(wù)為取消狀態(tài)(取消狀態(tài)下的任務(wù),任務(wù)體不會(huì)執(zhí)行任何操作),并從id映射和緩存中移除

文章任務(wù)部分代碼如下

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;

/**
 * 文章發(fā)布任務(wù)
 *
 * @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<>();
    /**任務(wù)緩存*/
    private static volatile ConcurrentHashMap<String, ArticlePublishJob> cache = new ConcurrentHashMap<>();
    
    private volatile AtomicBoolean canceled = new AtomicBoolean(false);//任務(wù)取消狀態(tài)
    private String jobId;//任務(wù)id
    private String articleUuid;//文章id
    private ArticleRender articleRender;//文章渲染器

    
    public ArticlePublishJob(ArticleRender articleRender) {
        this.jobId = IdGenerator.plainJdkUUID();
        this.articleRender = articleRender;
        this.articleUuid = articleRender.getArticle().getUuid();
        //取消并清除上一次任務(wù)
        cancelAndClearLastJobIfExist();
        //緩存本次任務(wù)
        cacheThisJob();
    }

    /**
     * 取消并清除上一次任務(wù)
     */
    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);
            }
        }
    }

    /**
     * 緩存本次任務(wù)
     */
    private void cacheThisJob(){
        //id映射
        idMapping.put(this.articleUuid, this.jobId);
        //文章發(fā)布任務(wù)緩存
        cache.put(this.jobId, this);
    }

    @Override
    public void run() {
        //判斷任務(wù)是否被取消
        if (!canceled.get()) {
            ArticleService articleService = (ArticleService) SpringContextHolder.getBean("articleService");
            //渲染
            try {
                articleRender.rendering();
            } catch (IOException e) {
                LOGGER.error("render log err:" + e.getMessage(), e);
            }
            //更新文章狀態(tài)
            Article updArticle = new Article();
            updArticle.setUuid(articleRender.getArticle().getUuid());
            updArticle.setStatus(Article.STATUS_NORMAL);
            articleService.edit(updArticle);

            //從緩存中清理本任務(wù)
            clear();
        }
    }

    /**
     * 取消任務(wù)
     */
    public void cancelJob() {
        this.canceled.set(true);
    }

    /**
     * 清理緩存
     */
    public void clear() {
        idMapping.remove(articleUuid);
        cache.remove(this.jobId);
    }

}

5、具體定時(shí)任務(wù)方式,包括以下幾種

  • Thread方式:線程等待,不安全。
  • timer方式:線程資源沒有復(fù)用。
  • 任務(wù)調(diào)度框架,比如Quartz等:需要繼承框架。
  • ScheduledExecutorService方式:被相中了。

綜上分析,選擇了最后一種,也是較好的選擇之一,下面給出最簡單的用法,如有深入需要,建議查看JavaAPI。

部分代碼如下

/**創(chuàng)建線程池*/
public static ScheduledExecutorService service = Executors.newScheduledThreadPool(50);


/**新建任務(wù),并設(shè)定執(zhí)行時(shí)間*/
ArticlePublishJob job = new ArticlePublishJob(articleRender);
long delay = article.getPubTime().getTime() - System.currentTimeMillis();
service.schedule(job, delay, TimeUnit.MILLISECONDS);

測試,大功告成。

總結(jié)
不結(jié)合業(yè)務(wù)來說,定時(shí)任務(wù)的創(chuàng)建無非就"第5點(diǎn)"中的幾種方式,熟悉API并熟練使用即可。
結(jié)合業(yè)務(wù)情況下,需要考慮任務(wù)是否會(huì)重復(fù),重復(fù)了怎么處理等問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容