前面两篇文章已经介绍了这个博客项目的主要功能。本文将讨论余下的一些高级功能。作为这个项目系列的终结,在这里也要感谢原作者的慷慨分享,让我们有机会得到这么具体实用的锻炼。另外写完这个系列的感受就是,它确实大大地帮助了我去深入思考和挖掘,教是最好的学习。今天是元旦,新年快乐!
网页静态化
JSP、ASP.NET等动态页面是互联网技术的一次飞跃。但它们也有缺陷。观察一下淘宝、京东等访问量巨大的网站,可以发现它们大多都是静态的HTML页面。
网页静态化就是指在功能不变的前提下,把这些动态页面变成静态的HTML页面。
静态化一些好处:
- 提高打开速度。动态页面需要容器的很多操作,很消耗时间;而静态页面只需要HTTP服务器就能够处理了,可以大幅提高响应能力。这也是网页静态化的主要动力。
- 有利于被搜索引擎收录。搜索引擎的爬虫更容易解析静态页面。
- 更简单,更安全。不容易被黑客发现漏洞;数据库出故障照样能打开页面。
那么本项目是怎么实现静态化的呢?
把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/feed.xml 。根据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文件中。