Servlet與JSP項目實戰 — 博客系統(下)

前面兩篇文章已經介紹了這個博客項目的主要功能。本文將討論餘下的一些高級功能。做爲這個項目系列的終結,在這裏也要感謝原做者的慷慨分享,讓咱們有機會獲得這麼具體實用的鍛鍊。另外寫完這個系列的感覺就是,它確實大大地幫助了我去深刻思考和挖掘,教是最好的學習。今天是元旦,新年快樂!html

網頁靜態化

JSP、ASP.NET等動態頁面是互聯網技術的一次飛躍。但它們也有缺陷。觀察一下淘寶、京東等訪問量巨大的網站,能夠發現它們大多都是靜態的HTML頁面。java

網頁靜態化就是指在功能不變的前提下,把這些動態頁面變成靜態的HTML頁面。web

靜態化一些好處:數據庫

  • 提升打開速度。動態頁面須要容器的不少操做,很消耗時間;而靜態頁面只須要HTTP服務器就可以處理了,能夠大幅提升響應能力。這也是網頁靜態化的主要動力。apache

  • 有利於被搜索引擎收錄。搜索引擎的爬蟲更容易解析靜態頁面。編程

  • 更簡單,更安全。不容易被黑客發現漏洞;數據庫出故障照樣能打開頁面。json

那麼本項目是怎麼實現靜態化的呢?數組

setting.properties中的environment.product改成true。應用加載起來後,訪問http://localhost:8080/,你看到的就是一個靜態頁面。它就是web.xml中指定的歡迎頁面html/index.html。這個頁面中的大部分連接也都是靜態頁面。好比點擊「所有文章」獲得的是html/article_list_create_date_1.html,點擊第一篇文章打開的是html/article_1.html;點擊右邊欄的「點擊排行」打開的是html/article_list_access_times_1.html。這些都是靜態頁面。緩存

從這裏就能看出靜態化的好處:大部分用戶只是上來看看,切換幾個頁面,瀏覽幾篇文章——他們看到的都是靜態頁面,消耗的資源極少,從而大大減輕了服務器的壓力。安全

下面來看看實現原理。打開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());
            ...
        }
    }
    ...

這個方法在容器加載應用時被調用。Executor.executeTask()接受一個Runnable的實現類,就是啓動一個新線程來執行任務。

FetchTask類的實現以下:

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;
            }
        }
...

方法中的循環代表任務將會按期運行,默認間隔是一天。其餘代碼咱們後面再探討,先來看Generators.generate()

爲了弄清楚這個函數,先來看看com.zuoxiaolong.generator這個包。這個包下全部類都繼承自接口Generator

public interface Generator {

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

能夠猜到,這個接口就定義了生成靜態頁面的接口。
Generators類在被調用以前先把包下面全部的靜態頁面生成類找到並存放到數組中。Generators.generate()就是依次調用這些類的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()方法就是對每篇文章調用generateArticle()。因爲VIEW_MODE的取值始終是接口中的賦值ViewMode.STATIC,所以生成的結果中含有的連接都是靜態地址。而經過計算獲得的靜態頁面地址htmlPath將會是html/article_id.html

獲得靜態的文章地址,這沒問題。可是更上層的靜態頁面中的連接(好比首頁中的文章列表)應該指向這些靜態頁面,這樣纔有意義。

咱們來看看怎麼實現。以ArticleListGenerator類爲例,它負責生成靜態的最新文章列表等頁面。其生成方法中調用了ArticleListHelper.putDataMap()方法,後者又調用了ArticleDao.getPageArticles()方法。最終這個方法調用了transfer()來把從數據庫中查詢到的變量轉換成用於模板的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始終是靜態的,url的值將會是文章的靜態頁面的地址。看到這裏你應該就能完全理解VIEW_MODE的用意了。

以此類推,從最外層的歡迎頁面,到文章列表頁面,再到具體的文章頁面,這些靜態頁面含有的始終都是靜態頁面的連接。除非用戶點擊頂欄菜單中的「主頁」連接(這個連接指向的是動態地址),繞來繞去他都是在訪問靜態頁面!

最後,靜態頁面不是按期才刷新的。不然會出現問題——假若有人提交了新的評論,其餘人仍然看不到這個評論,只能等到一天後刷新。觀察Generators類,它還含有一些靜態方法,好比generateArticle()。這些方法會在須要時被調用,而不用被動的等待任務按期刷新。Ctrl+H查看引用就能發現方法的調用狀況。

緩存

把一些經常被訪問的數據保存到內存中,須要時直接獲取而不用進行磁盤IO,這即是常見的緩存技術。做者本身實現了一個簡單的緩存機制,代碼在com.zuoxiaolong.cache包中。

緩存的數據是用ConcurrentHashMap來存放的,而且用另外一個ConcurrentHashMap來追蹤數據的生命週期。讀取數據時,先檢查數據有沒有過時,若是有則刪除數據,返回null。

查看CacheManager的全部引用能夠看出緩存功能的使用狀況。它主要用在兩方面:

  • 用戶訪問記錄。因爲調用次數多,且邏輯很是簡單,使用緩存能夠提升性能。

  • 文章顯示在文章列表中的隨機配圖。因爲這些圖都是事先準備好的,並且經常用到,因此用緩存進行優化很合理。

Lucene搜索

系統使用了大名鼎鼎的Apache Lucene做爲全文搜索引擎。這裏是它的官方網站。關於它的原理,若是你用過Everything這個文件搜索工具,或者諸如DT Search這樣的代碼搜索工具,就會很容易理解。簡單來講,它們都會事先掃描全部文件的內容,而後把每一個單詞創建索引(能夠類比爲Hash存儲),這樣在搜索時將會很是快。這裏有一篇較爲詳細的講解。

具體的實現大部分在com.zuoxiaolong.search.LuceneHelper類中。

  • generateIndex()方法被FetchTask任務按期調用,掃描文章生成索引。

  • search()方法調用Lucene引擎獲得結果,並把結果用高亮標註。

  • common.js中的searchArticles()方法將搜索事件轉發給article_list.ftl頁面,後者的動態數據類最終調用LuceneHelper的方法獲得結果。

爬蟲

這個系統中引入的爬蟲只是爲了將做者之前在CnBlogs的博客搬運過來。代碼所有在com.zuoxiaolong.reptile.Cnblogs這一個類中。

爬蟲的原理是使用Jsoup這個HTML解析器,後者可讓HTML解析變得很是簡單。具體能夠參考其官網。這裏不作更多探討。

RSS訂閱和百度主動推送

博客網站每每都支持RSS訂閱,方便用戶在一個地方閱讀不一樣來源的內容。只不過無私一點的就把內容也放在Feed中;自私一點就只放文章連接,這樣用戶還得來訪問本身的網站;最自私的就是不提供訂閱…

RSS的原理很簡單,就是網站發佈一個Url,這個地址是一個XML文本,裏面用RSS格式描述網站的最新內容。如這個連接是阮一峯博客的Feed。客戶端軟件保存這個Url,而後按期地刷新以得到XML文本的最新內容,再經過比較就可以得知網站是否存在更新,若是有就通知用戶。

點擊主頁右邊欄的"RSS訂閱"按鈕,發現它打開的網址是http://localhost:8080/blog/fe... 。根據web.xml的配置,.XML文件也是跟.FTL同樣處理的。也就是說,也會有一個FreeMarker模板,與動態數據合併後生成內容。只不過最後輸出的XML文檔。

那麼就來看看它分別對應的動態數據類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()));
    }
}

可見它就是把最新的文章從數據訪問層中取出,而後放到FreeMarker的變量中。注意getPageArticles()用的參數是ViewMode.STATIC,因此獲得的都是靜態頁面。

再來看FreeMarker模板:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>左瀟龍我的博客</title>
        <atom:link href="http://www.zuoxiaolong.com/feed.xml" 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>

一目瞭然,把文章的標題、連接、摘要等放入合適的RSS元素中。這裏也說明FreeMarker不是隻用來生成HTML的,它能夠生成任何內容。

最後一部份內容是關於百度的主動推送

關於它的解釋能夠參考這個連接,以及官方文檔。大體意思是,使用主動連接推送能夠第一時間把內容更新告知百度,而不用等待百度的蜘蛛爬蟲來解析你的網站。這樣作的一個好處就是保護原創,使內容能夠在轉發以前被百度發現。

其實如今類BaiduPushTask中,也是做爲一個單獨的線程被Executor啓動。來看代碼:

@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;
            }
        }
    }

就是按期運行。先調用DaoFactory.getDao(HtmlPageDao.class).flush();刷新要push的連接。再調用HttpApiHelper.baiduPush();將連接提交到百度。

HttpApiHelper.baiduPush()方法很簡單,就是把內容以json方式發送到百度提供的接口上。固然要提早在百度申請好API的Token,配置在setting.properties文件中。

相關文章
相關標籤/搜索