前面兩篇文章已經(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文件中坝橡。