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

接前一篇博客。这篇文章将专注于分析系统的核心功能。

管理员界面

在这个系统中,普通用户只能发表评论,只有管理员才能够创建文章。项目的README中介绍了进入管理员界面的方法:访问地址/admin

首先来看登录检查

前文已经介绍到,web.xml指明所有/admin路径下的请求都要先经过AdminFilter。观察它的代码,就是把请求按以下逻辑处理:

  • 对于"/admin/login.ftl""/admin/login.do",交给下一级处理。这两个请求将分别被DynamicFilterAdminLogin类接管。

  • 对于"/admin",重定向到"/admin/admin_index.ftl"

  • 对于其它/admin路径下的请求,先调用AbstractServlet.isAdminLogin()检查有没有登录。如果有则交给下一级处理;否则重定向到"/admin/login.ftl"要求登录。

逻辑很清晰。AdminLogin类的逻辑也很简单,比对密码的哈希和配置是否一致。如果一致,就重定向到"/admin/admin_index.ftl",并且在这之前,在Session中设置admin属性;否则,就重定向到"/admin/login.ftl"要求重新登录。

可想而知AbstractServlet.isAdminLogin()的逻辑,就是检查Session中的admin属性。

管理后台

管理员主页非常简单,只有如下几个功能。

后台

点击不同的链接会跳转到不同的FTL页面。例如“最新回复”,它的地址是admin/new_comment.ftl

com.zuoxiaolong.dynamic包中找到上面地址对应的动态数据类NewComment,它的代码非常简单,就是从数据库中找到评论,然后交给模板去呈现:

@Namespace("admin")
public class NewComment implements DataMap {
    
    @Override
    public void putCustomData(Map<String, Object> data,HttpServletRequest request, HttpServletResponse response) {
        data.put("newComments", DaoFactory.getDao(CommentDao.class).getComments());
    }
}

编辑文章

在文章管理页面,通过新建文章可以进入admin/article_input.ftl页面。

这个页面使用了著名的在线文本编辑器TinyMCE。简单的说,它只是一个JS库,能够“所见即所得”地把在编辑框中输入的富文本翻译成HTML文本。

下面来分析这个FTL文件的代码。

在正文部分定义了一个textarea,作为编辑器的占位元素。

<textarea class="html_editor" style="width:100%"></textarea>

在正文结尾处有如下Javascript代码:

<script type="text/javascript">
    var settings = {width:900,height:400,content:''};
    <#if article?? && article.escapeHtml??>
    settings.content = '${article.escapeHtml}';
    </#if>
    tinymceInit(settings);
    $(document).ready(function(){
        $("#submitButton").click(function(){
            ...
            $.ajax({
                url:"${contextPath}/admin/updateArticle.do",
                data:{"id":$("input[name=id]").val(),"content":tinymce.activeEditor.getContent()
                    ,"subject":$("input[name=subject]").val(),"status":status,"type":$("select[name=type]").val()
                    ,"tags":$("input[name=tags]").val(),"categories":categories,"updateCreateTime":updateCreateTime},
                type:"POST",
                success:function(data){
                    if(data && data == 'success') {
                        alert("保存成功");
                        window.location.href="${contextPath}/admin/article_manager.ftl"
                    } else {
                        alert("保存失败");
                    }
                }
            });
        });
    });
</script>

其中tinymceInit(settings);这句话就是初始化TinyMCE的代码,把编辑器显示出来。settings.content = '${article.escapeHtml}'; 是把编辑器中的内容设置为已有的内容——因为更新文章也是用的这个模板页面。至于文章的HTML内容是从哪里得到的,我们后面再谈。

后面的这段AJAX代码意思是当点击保存按钮时,把文章的所有数据提交到"admin/updateArticle.do"这个Url上来,注意代码使用了tinymce.activeEditor.getContent()来获得编辑器产生的HTML文本。我们再来看看这个Url对应的处理类做的事情:

@RequestMapping("/admin/updateArticle.do")
public class AdminUpdateArticle extends AbstractServlet {

    @Override
    protected void service() throws ServletException, IOException {
        String id = getRequest().getParameter("id");
        String subject = getRequest().getParameter("subject");
        String html = getRequest().getParameter("content");
        String status = getRequest().getParameter("status");
        String type = getRequest().getParameter("type");
        String icon = getRequest().getParameter("icon");
        String updateCreateTime = getRequest().getParameter("updateCreateTime");
        String[] categories = getRequest().getParameter("categories").split(",");
        String[] tags = getRequest().getParameter("tags").split(",");
        html = handleQuote(html);
        
        StringBuffer stringBuffer = new StringBuffer();
        JsoupUtil.appendText(Jsoup.parse(html), stringBuffer);
        Integer articleId = 
                DaoFactory.getDao(ArticleDao.class).saveOrUpdate(id, 
                        subject, 
                        Status.valueOf(Integer.valueOf(status)), 
                        Type.valueOf(Integer.valueOf(type)), 
                        Integer.valueOf(updateCreateTime), 
                        "左潇龙", html, stringBuffer.toString(), icon);
...        

主要的逻辑就是把数据从请求中读出来,做一些检查和处理后存入数据库。

尽管创建新文章和编辑已有文章共享同一套逻辑,但是在DAO层保存到数据库时,前者是insert,后者是update。另外创建新文章的时候并不知道文章的Id,所以DAO层插入记录时将获得数据库自动生成的ID。

// ArticleDao 的代码
statement = connection.prepareStatement(insertSql,Statement.RETURN_GENERATED_KEYS);
更新文章时如何获得已有文章的内容

在admin主页中点击文章管理可以进入文章管理页面(admin/article_manager.ftl)。点击每篇文章的标题就可以进入编辑文章页面,但这个Url还带了一个参数:文章id

article_input.ftl页面作为一个FTL模板文件,自然也会受到DynamicFilter的过滤,与它对应的动态数据类是ArticleInput类。这个类的putCustomData()方法会从请求中拿到id参数,然后根据这个id读取数据库,把对应的文章的数据存放到FreeMarker变量"article"中去。模板文件再根据这个参数呈现内容。

上传图片

无论是编辑文章还是提交评论,编辑器都支持上传图片的功能。如何实现的呢?

在项目中使用的TinyMCE 4.1.10版本自身还不支持上传图片,只能借助一些插件实现。而这里的实现另辟蹊径。我们来看看位于webapp/resources/js/tinymce路径下的tinymce.init.js文件。

function tinymceInit(settings) {
    $(document).ready(function() {
        var defaultSettings = {width:600,height:400,content:'',skin:'lightgray'};
        $.extend(defaultSettings,settings);
        tinymce.init({
            selector: "textarea.html_editor",
            language: "zh_CN",
            menubar : false,
            skin: defaultSettings.skin,
            width: defaultSettings.width,
            height: defaultSettings.height,
            toolbar_items_size:'small',
            setup: function(editor) {
                editor.addButton('upload',
                {
                    icon: 'print',
                    title: '上传本地图片',
                    onclick: function() {
                        editor.windowManager.open({
                            title: "上传本地图片",
                            url: contextPath + "/html/upload_image.html",
                            width: 400,
                            height: 150
                        });
                    }
                });
                editor.addButton('insertcode',
                {
                ...

这个文件是我们自己建立的,函数tinymceInit()在前面讲到的article_input.ftl文件中被调用。它调用了真正的API tinymce.init()来完成初始化。可以看到函数中调用了editor.addButton()来添加一个上传图片的按钮,被点击时的行为则是弹出一个upload_image.html页面。

upload_image.html页面是在运行时根据模板文件common/upload_image.ftl生成的。至于如何生成,且听下回分解。它的主要内容如下:

<body>
    <table class="float_left" style="width: 340px;height: 90px;border: 1px solid #d5d5d5;margin: 5px;">
            <form id="upload_image_form" method="POST" action="http://localhost:8080/uploadImage.do" enctype="multipart/form-data">
                <tr>
                    <td class="form_input">
                        <a class="file_input_a" href="#">
                            选择图片
                            <input class="file_input" type="file" name="imageFile" />
                        </a>
                    </td>
                </tr>
                <tr>
                    <td class="form_input">
                        <input type="text" class="text_input" id="file_path" readonly="readonly" style="width:340px;max-width: 340px;"/>
                    </td>
                </tr>
                <tr>
                    <td class="form_input">
                        <input type="submit" class="form_button" value="上传"/>
                    </td>
                </tr>
            </form>
        </table>
...

<script type="application/javascript">
        $(document).ready(function(){
            $("input[name=imageFile]").change(function(){
                $("#file_path").val($(this).val());
            });
            $("#upload_image_form").ajaxForm({
                beforeSubmit:function(){
                    if(!$("input[name=imageFile]").val()) {
                        window.parent.alert("请选择图片");
                        return false;
                    }
                    return true;
                },
                success:function(url){
                    if (url && url == 'format_error') {
                        alert("只能上传png,jpg,gif格式的文件");
                        return;
                    }
                    if (url) {
                        top.tinymce.activeEditor.insertContent("<img src='" + url + "'/>");
                        top.tinymce.activeEditor.windowManager.close();
                    }
                }
            });
        });
</script>

就是一个上传文件的表单,目的地址是uploadImage.do。上传图片成功后,得到一个图片的Url。JS脚本再调用top.tinymce.activeEditor.insertContent()向编辑器中插入图片元素。

负责处理uploadImage.do的是UploadImage类。它使用commons-fileupload包来获取文件,保存到image/路径下,再返回动态生成后的真实Url。

整个上传过程到此就结束了。再看tinymce.init.js文件可以发现它还增加了一个插入代码的按钮,原理都是类似的。

评论、投票与统计

直接来看blog/article.ftl文件。文章正文和评论部分在article_list.ftl中。

在文章的尾部有一排投票的表情,只能选一个,数据最终提交到数据库中的article表里。如果看一下article表的设计,可以发现它很长。其中不仅包含内容、日期这种信息,还包含评论数量等等。

当访问article页面时,所有与文章相关的数据都被数据库中读取到,显示到页面中。点击页面中的评论、投票等按钮时,就会发送请求到相应的xxx.do地址,请求被合适的Servlet处理,最终反映到数据库中。这些都略去不表。

下面来看下如何跟踪文章的访问次数。可以注意到article表中有一个access_times字段,就是文章的访问次数。那么它是如何更新的呢?

article.ftl文件中有这么一段Javascript脚本:

<script type="text/javascript">
    // 建立评论区的编辑器
    tinymceInit({width:700,height:150,skin:'comment'});
    $(document).ready(function() {
        counter({"articleId":$("#articleId").val(),"type":1,"column":"access_times"});
        ...

这里的counter()函数像是跟访问计数有关。它在common/common.js中定义:

function counter(data) {
    $.ajax({
        url:contextPath + "/counter.do",
        type:"POST",
        data:data
    });
}

就是把数据发到"/counter.do"地址。来看它的处理类Counter:

public class Counter extends AbstractServlet {

    @Override
    protected void service() throws IOException {
        HttpServletRequest request = getRequest();
        Integer type = Integer.valueOf(request.getParameter("type"));
        if (type == 1) {
            updateArticle(request);
        } else if (type == 2) {
            updateQuestion(request);
        }  else if (type == 3) {
            updateRecord(request);
        } else {
            throw new RuntimeException("unknown type.");
        }
    }
    
...

    private void updateArticle(HttpServletRequest request) {
        Integer articleId = Integer.valueOf(request.getParameter("articleId"));
        String column = request.getParameter("column");
        if (logger.isInfoEnabled()) {
            logger.info("counter param : articleId = " + articleId + "   , column = " + column);
        }
        if (!column.equals("access_times")) {
            if (logger.isInfoEnabled()) {
                logger.info("there is someone remarking...");
            }
            String username = getUsername();
            String ip = HttpUtil.getVisitorIp(request);
            if (DaoFactory.getDao(ArticleIdVisitorIpDao.class).exists(articleId, ip, username)) {
                writeText("exists");
                if (logger.isInfoEnabled()) {
                    logger.info(ip + " has remarked...");
                }
                return ;
            } else {
                DaoFactory.getDao(ArticleIdVisitorIpDao.class).save(articleId, ip, username);
            }
        }
        boolean result = DaoFactory.getDao(ArticleDao.class).updateCount(articleId, column);
...

这里的逻辑是,判断一下同一个IP和User是不是已经访问过这篇文章了。如果没有,就把访问历史先保存起来,再调用Dao层把access_times加1。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值