Servlet与JSP项目实战 — 博客系统(下)

前面两篇文章已经介绍了这个博客项目的主要功能。本文将讨论余下的一些高级功能。作为这个项目系列的终结,在这里也要感谢原作者的慷慨分享,让我们有机会得到这么具体实用的锻炼。另外写完这个系列的感受就是,它确实大大地帮助了我去深入思考和挖掘,教是最好的学习。今天是元旦,新年快乐!

网页静态化

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文件中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,510评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • 十六岁那年遇见你也遇见了算命先生先生说,你命犯桃花我不信,十年寒窗浪迹天涯金榜题名又红烛高挂红盖头下你脸若桃花我信...
    昊水长天阅读 257评论 4 6
  • 今天二美的课堂请来一位嘉宾为《好报》写作群的群友做写作交流和分享,让人惊讶的是这位小老师竟然是96年的,从13岁开...
    宋月才河北阅读 271评论 1 2
  • 1.先思考,再行动? 今天好好想了想,不仅女人会骗人,道理也会骗人。 (她抱着无忌,低声道:“孩儿,你长大了之后,...
    神农堂朱家阅读 256评论 6 6