美文网首页
Servlet与JSP项目实战 — 博客系统(中)

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

作者: Toconscience | 来源:发表于2017-04-11 23:51 被阅读129次

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

    管理员界面

    在这个系统中,普通用户只能发表评论,只有管理员才能够创建文章。项目的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("![](" + 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。

    相关文章

      网友评论

          本文标题:Servlet与JSP项目实战 — 博客系统(中)

          本文链接:https://www.haomeiwen.com/subject/lmjpattx.html