Servlet與JSP項(xiàng)目實(shí)戰(zhàn) — 博客系統(tǒng)(下)

前面兩篇文章已經(jīng)介紹了這個(gè)博客項(xiàng)目的主要功能。本文將討論余下的一些高級(jí)功能。作為這個(gè)項(xiàng)目系列的終結(jié)包券,在這里也要感謝原作者的慷慨分享濒旦,讓我們有機(jī)會(huì)得到這么具體實(shí)用的鍛煉株旷。另外寫完這個(gè)系列的感受就是,它確實(shí)大大地幫助了我去深入思考和挖掘尔邓,教是最好的學(xué)習(xí)晾剖。今天是元旦锉矢,新年快樂!

網(wǎng)頁靜態(tài)化

JSP齿尽、ASP.NET等動(dòng)態(tài)頁面是互聯(lián)網(wǎng)技術(shù)的一次飛躍沽损。但它們也有缺陷。觀察一下淘寶雕什、京東等訪問量巨大的網(wǎng)站缠俺,可以發(fā)現(xiàn)它們大多都是靜態(tài)的HTML頁面。

網(wǎng)頁靜態(tài)化就是指在功能不變的前提下贷岸,把這些動(dòng)態(tài)頁面變成靜態(tài)的HTML頁面壹士。

靜態(tài)化一些好處:

  • 提高打開速度。動(dòng)態(tài)頁面需要容器的很多操作偿警,很消耗時(shí)間躏救;而靜態(tài)頁面只需要HTTP服務(wù)器就能夠處理了,可以大幅提高響應(yīng)能力螟蒸。這也是網(wǎng)頁靜態(tài)化的主要?jiǎng)恿Α?/li>
  • 有利于被搜索引擎收錄盒使。搜索引擎的爬蟲更容易解析靜態(tài)頁面。
  • 更簡(jiǎn)單七嫌,更安全少办。不容易被黑客發(fā)現(xiàn)漏洞;數(shù)據(jù)庫出故障照樣能打開頁面诵原。

那么本項(xiàng)目是怎么實(shí)現(xiàn)靜態(tài)化的呢英妓?

setting.properties中的environment.product改為true。應(yīng)用加載起來后绍赛,訪問http://localhost:8080/蔓纠,你看到的就是一個(gè)靜態(tài)頁面。它就是web.xml中指定的歡迎頁面html/index.html吗蚌。這個(gè)頁面中的大部分鏈接也都是靜態(tài)頁面腿倚。比如點(diǎn)擊“全部文章”得到的是html/article_list_create_date_1.html,點(diǎn)擊第一篇文章打開的是html/article_1.html蚯妇;點(diǎn)擊右邊欄的“點(diǎn)擊排行”打開的是html/article_list_access_times_1.html敷燎。這些都是靜態(tài)頁面。

從這里就能看出靜態(tài)化的好處:大部分用戶只是上來看看侮措,切換幾個(gè)頁面懈叹,瀏覽幾篇文章——他們看到的都是靜態(tài)頁面,消耗的資源極少分扎,從而大大減輕了服務(wù)器的壓力澄成。

下面來看看實(shí)現(xiàn)原理。打開com.zuoxiaolong.listener包下ConfigurationListener的代碼:

public class ConfigurationListener implements ServletContextListener {
    
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        
        ...
        if (Configuration.isProductEnv()) {
            ...
            Executor.executeTask(new FetchTask());
            ...
            Executor.executeTask(new BaiduPushTask());
            ...
        }
    }
    ...

這個(gè)方法在容器加載應(yīng)用時(shí)被調(diào)用。Executor.executeTask()接受一個(gè)Runnable的實(shí)現(xiàn)類墨状,就是啟動(dòng)一個(gè)新線程來執(zhí)行任務(wù)卫漫。

FetchTask類的實(shí)現(xiàn)如下:

public class FetchTask implements Runnable {
    
    private static final int THREAD_SLEEP_DAYS = Integer.valueOf(Configuration.get("fetch.thread.sleep.days"));

    @Override
    public void run() {
        while (true) {
            try {
                ImageUtil.loadArticleImages();
                if (Configuration.isProductEnv()) {
                    Cnblogs.fetchArticlesAfterLogin();
                } else {
                    Cnblogs.fetchArticlesCommon();
                }
                LuceneHelper.generateIndex();
                Generators.generate();
                Thread.sleep(1000L * 60L * 60L * 24L * Long.valueOf(THREAD_SLEEP_DAYS));
            } catch (Exception e) {
                logger.warn("fetch and generate failed ...", e);
                break;
            }
        }
...

方法中的循環(huán)表明任務(wù)將會(huì)定期運(yùn)行,默認(rèn)間隔是一天肾砂。其他代碼我們后面再探討列赎,先來看Generators.generate()

為了弄清楚這個(gè)函數(shù)镐确,先來看看com.zuoxiaolong.generator這個(gè)包包吝。這個(gè)包下所有類都繼承自接口Generator

public interface Generator {

    ViewMode VIEW_MODE = ViewMode.STATIC;
    int order();
    void generate();
}

可以猜到,這個(gè)接口就定義了生成靜態(tài)頁面的接口源葫。
Generators類在被調(diào)用之前先把包下面所有的靜態(tài)頁面生成類找到并存放到數(shù)組中诗越。Generators.generate()就是依次調(diào)用這些類的generate()方法。

ArticleGenerator類為例:

public class ArticleGenerator implements Generator {

    ...
    @Override
    public void generate() {
        List<Map<String, String>> articles = DaoFactory.getDao(ArticleDao.class).getArticles("create_date", Status.published, VIEW_MODE);
        for (int i = 0; i < articles.size(); i++) {
            generateArticle(Integer.valueOf(articles.get(i).get("id")));
        }
    }

    void generateArticle(Integer id) {
        Writer writer = null;
        try {
            Map<String, Object> data = FreemarkerHelper.buildCommonDataMap(VIEW_MODE);
            ArticleHelper.putDataMap(data, VIEW_MODE, id);
            String htmlPath = Configuration.getContextPath(ArticleHelper.generateStaticPath(id));
            writer = new FileWriter(htmlPath);
            FreemarkerHelper.generate("article", writer, data);
        } catch (IOException e) {
            ...

}

它的generate()方法就是對(duì)每篇文章調(diào)用generateArticle()息堂。由于VIEW_MODE的取值始終是接口中的賦值ViewMode.STATIC嚷狞,因此生成的結(jié)果中含有的鏈接都是靜態(tài)地址。而通過計(jì)算得到的靜態(tài)頁面地址htmlPath將會(huì)是html/article_id.html荣堰。

得到靜態(tài)的文章地址床未,這沒問題。但是更上層的靜態(tài)頁面中的鏈接(比如首頁中的文章列表)應(yīng)該指向這些靜態(tài)頁面振坚,這樣才有意義薇搁。

我們來看看怎么實(shí)現(xiàn)。以ArticleListGenerator類為例渡八,它負(fù)責(zé)生成靜態(tài)的最新文章列表等頁面只酥。其生成方法中調(diào)用了ArticleListHelper.putDataMap()方法,后者又調(diào)用了ArticleDao.getPageArticles()方法呀狼。最終這個(gè)方法調(diào)用了transfer()來把從數(shù)據(jù)庫中查詢到的變量轉(zhuǎn)換成用于模板的Map變量。來看看它的代碼:

public Map<String, String> transfer(ResultSet resultSet, ViewMode viewMode) {
        Map<String, String> article = new HashMap<String, String>();
        try {
            String id = resultSet.getString("id");
            article.put("id", id);
            if (viewMode == ViewMode.DYNAMIC) {
                article.put("url", ArticleHelper.generateDynamicPath(Integer.valueOf(id)));
            } else {
                article.put("url", ArticleHelper.generateStaticPath(Integer.valueOf(id)));
            }
            ...

看到了嗎损离?由于開始傳入的VIEW_MODE始終是靜態(tài)的哥艇,url的值將會(huì)是文章的靜態(tài)頁面的地址∑欤看到這里你應(yīng)該就能徹底理解VIEW_MODE的用意了貌踏。

以此類推,從最外層的歡迎頁面窟勃,到文章列表頁面祖乳,再到具體的文章頁面,這些靜態(tài)頁面含有的始終都是靜態(tài)頁面的鏈接秉氧。除非用戶點(diǎn)擊頂欄菜單中的“主頁”鏈接(這個(gè)鏈接指向的是動(dòng)態(tài)地址)眷昆,繞來繞去他都是在訪問靜態(tài)頁面!

最后,靜態(tài)頁面不是定期才刷新的亚斋。否則會(huì)出現(xiàn)問題——假如有人提交了新的評(píng)論作媚,其他人仍然看不到這個(gè)評(píng)論,只能等到一天后刷新帅刊。觀察Generators類纸泡,它還含有一些靜態(tài)方法,比如generateArticle()赖瞒。這些方法會(huì)在需要時(shí)被調(diào)用女揭,而不用被動(dòng)的等待任務(wù)定期刷新。Ctrl+H查看引用就能發(fā)現(xiàn)方法的調(diào)用情況栏饮。

緩存

把一些常常被訪問的數(shù)據(jù)保存到內(nèi)存中吧兔,需要時(shí)直接獲取而不用進(jìn)行磁盤IO,這便是常見的緩存技術(shù)抡爹。作者自己實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的緩存機(jī)制掩驱,代碼在com.zuoxiaolong.cache包中。

緩存的數(shù)據(jù)是用ConcurrentHashMap來存放的冬竟,并且用另一個(gè)ConcurrentHashMap來追蹤數(shù)據(jù)的生命周期欧穴。讀取數(shù)據(jù)時(shí),先檢查數(shù)據(jù)有沒有過期泵殴,如果有則刪除數(shù)據(jù)涮帘,返回null。

查看CacheManager的所有引用可以看出緩存功能的使用情況笑诅。它主要用在兩方面:

  • 用戶訪問記錄调缨。由于調(diào)用次數(shù)多,且邏輯非常簡(jiǎn)單吆你,使用緩存可以提高性能弦叶。
  • 文章顯示在文章列表中的隨機(jī)配圖。由于這些圖都是事先準(zhǔn)備好的妇多,而且常常用到伤哺,所以用緩存進(jìn)行優(yōu)化很合理。

Lucene搜索

系統(tǒng)使用了大名鼎鼎的Apache Lucene作為全文搜索引擎者祖。這里是它的官方網(wǎng)站立莉。關(guān)于它的原理,如果你用過Everything這個(gè)文件搜索工具七问,或者諸如DT Search這樣的代碼搜索工具蜓耻,就會(huì)很容易理解。簡(jiǎn)單來說械巡,它們都會(huì)事先掃描所有文件的內(nèi)容刹淌,然后把每個(gè)單詞建立索引(可以類比為Hash存儲(chǔ))饶氏,這樣在搜索時(shí)將會(huì)非常快芦鳍。這里有一篇較為詳細(xì)的講解嚷往。

具體的實(shí)現(xiàn)大部分在com.zuoxiaolong.search.LuceneHelper類中。

  • generateIndex()方法被FetchTask任務(wù)定期調(diào)用柠衅,掃描文章生成索引皮仁。
  • search()方法調(diào)用Lucene引擎得到結(jié)果,并把結(jié)果用高亮標(biāo)注菲宴。
  • common.js中的searchArticles()方法將搜索事件轉(zhuǎn)發(fā)給article_list.ftl頁面贷祈,后者的動(dòng)態(tài)數(shù)據(jù)類最終調(diào)用LuceneHelper的方法得到結(jié)果。

爬蟲

這個(gè)系統(tǒng)中引入的爬蟲只是為了將作者以前在CnBlogs的博客搬運(yùn)過來喝峦。代碼全部在com.zuoxiaolong.reptile.Cnblogs這一個(gè)類中势誊。

爬蟲的原理是使用Jsoup這個(gè)HTML解析器,后者可以讓HTML解析變得非常簡(jiǎn)單谣蠢。具體可以參考其官網(wǎng)粟耻。這里不做更多探討。

RSS訂閱和百度主動(dòng)推送

博客網(wǎng)站往往都支持RSS訂閱眉踱,方便用戶在一個(gè)地方閱讀不同來源的內(nèi)容挤忙。只不過無私一點(diǎn)的就把內(nèi)容也放在Feed中;自私一點(diǎn)就只放文章鏈接谈喳,這樣用戶還得來訪問自己的網(wǎng)站册烈;最自私的就是不提供訂閱…

RSS的原理很簡(jiǎn)單,就是網(wǎng)站發(fā)布一個(gè)Url婿禽,這個(gè)地址是一個(gè)XML文本赏僧,里面用RSS格式描述網(wǎng)站的最新內(nèi)容。如這個(gè)鏈接是阮一峰博客的Feed扭倾〉砹悖客戶端軟件保存這個(gè)Url,然后定期地刷新以獲得XML文本的最新內(nèi)容膛壹,再通過比較就能夠得知網(wǎng)站是否存在更新窑滞,如果有就通知用戶。

點(diǎn)擊主頁右邊欄的"RSS訂閱"按鈕恢筝,發(fā)現(xiàn)它打開的網(wǎng)址是http://localhost:8080/blog/feed.xml 。根據(jù)web.xml的配置巨坊,.XML文件也是跟.FTL一樣處理的撬槽。也就是說,也會(huì)有一個(gè)FreeMarker模板趾撵,與動(dòng)態(tài)數(shù)據(jù)合并后生成內(nèi)容侄柔。只不過最后輸出的XML文檔共啃。

那么就來看看它分別對(duì)應(yīng)的動(dòng)態(tài)數(shù)據(jù)類Feed和模板blog/feed.ftl:

@Namespace
public class Feed implements DataMap {

    @Override
    public void putCustomData(Map<String, Object> data, HttpServletRequest request, HttpServletResponse response) {
        response.addHeader("Content-Type","text/xml; charset=utf-8");
        Map<String, Integer> pager = new HashMap<>();
        pager.put("current", 1);
        data.put("articles", DaoFactory.getDao(ArticleDao.class).getPageArticles(pager, Status.published, "create_date", ViewMode.STATIC));
        data.put("lastBuildDate", DateUtil.rfc822(new Date()));
    }
}

可見它就是把最新的文章從數(shù)據(jù)訪問層中取出,然后放到FreeMarker的變量中暂题。注意getPageArticles()用的參數(shù)是ViewMode.STATIC移剪,所以得到的都是靜態(tài)頁面。

再來看FreeMarker模板:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>左瀟龍個(gè)人博客</title>
        <atom:link  rel="self" type="application/rss+xml"/>
        <link>http://www.zuoxiaolong.com</link>
        <description>一起走在編程的路上</description>
        <lastBuildDate>${lastBuildDate}</lastBuildDate>
        <language>zh-CN</language>
        <#list articles as article>
            <#if article_index gt 9>
                <#break />
            </#if>
            <item>
                <title>${article.subject}</title>
                <link>${contextPath}${article.url}</link>
                <pubDate>${article.us_create_date}</pubDate>
                <description>${article.summary}...</description>
            </item>
        </#list>
    </channel>
</rss>

一目了然薪者,把文章的標(biāo)題纵苛、鏈接、摘要等放入合適的RSS元素中言津。這里也說明FreeMarker不是只用來生成HTML的攻人,它可以生成任何內(nèi)容。

最后一部分內(nèi)容是關(guān)于百度的主動(dòng)推送悬槽。

關(guān)于它的解釋可以參考這個(gè)鏈接怀吻,以及官方文檔。大致意思是初婆,使用主動(dòng)鏈接推送可以第一時(shí)間把內(nèi)容更新告知百度蓬坡,而不用等待百度的蜘蛛爬蟲來解析你的網(wǎng)站。這樣做的一個(gè)好處就是保護(hù)原創(chuàng)磅叛,使內(nèi)容可以在轉(zhuǎn)發(fā)之前被百度發(fā)現(xiàn)屑咳。

其實(shí)現(xiàn)在類BaiduPushTask中,也是作為一個(gè)單獨(dú)的線程被Executor啟動(dòng)宪躯。來看代碼:

@Override
    public void run() {
        boolean first = true;
        while (true) {
            try {
                if (first) {
                    first = false;
                    Thread.sleep(1000 * 60 * Integer.valueOf(Configuration.get("baidu.push.thread.wait.minutes")));
                }
                DaoFactory.getDao(HtmlPageDao.class).flush();
                HttpApiHelper.baiduPush(1);
                Thread.sleep(1000 * 60 * 60 * 24);
            } catch (Exception e) {
                logger.warn("baidu push failed ...", e);
                break;
            }
        }
    }

就是定期運(yùn)行乔宿。先調(diào)用DaoFactory.getDao(HtmlPageDao.class).flush();刷新要push的鏈接。再調(diào)用HttpApiHelper.baiduPush();將鏈接提交到百度访雪。

HttpApiHelper.baiduPush()方法很簡(jiǎn)單详瑞,就是把內(nèi)容以json方式發(fā)送到百度提供的接口上。當(dāng)然要提前在百度申請(qǐng)好API的Token臣缀,配置在setting.properties文件中坝橡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市精置,隨后出現(xiàn)的幾起案子计寇,更是在濱河造成了極大的恐慌,老刑警劉巖脂倦,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件番宁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡赖阻,警方通過查閱死者的電腦和手機(jī)蝶押,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來火欧,“玉大人棋电,你說我怎么就攤上這事茎截。” “怎么了赶盔?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵企锌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我于未,道長(zhǎng)撕攒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任沉眶,我火速辦了婚禮打却,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谎倔。我一直安慰自己柳击,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布片习。 她就那樣靜靜地躺著捌肴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪藕咏。 梳的紋絲不亂的頭發(fā)上状知,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音孽查,去河邊找鬼饥悴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盲再,可吹牛的內(nèi)容都是我干的西设。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼答朋,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼贷揽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起梦碗,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤禽绪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后洪规,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體印屁,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年斩例,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了库车。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡樱拴,死狀恐怖柠衍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晶乔,我是刑警寧澤珍坊,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站正罢,受9級(jí)特大地震影響阵漏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜翻具,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一履怯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧裆泳,春花似錦叹洲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至闻葵,卻和暖如春民泵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背槽畔。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工栈妆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厢钧。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓鳞尔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坏快。 傳聞我的和親對(duì)象是個(gè)殘疾皇子铅檩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容