其实我开始学习Servlet和JSP是受了一篇《阿里社招面试如何准备,以及对于Java程序猿学习当中各个阶段的建议》的启发。作者左潇龙的个人主页在此,里面的文章都挺有意思的。这个博客系统是左潇龙自己写的,代码开源在GitHub上。

项目刚好是用Servlet + FreeMarker,没有上任何框架,但是MVC分层都有。

我Fork了一份代码,在已有基础上提交了一些Bug Fix和注释,项目地址在此。这篇文章就是来分析学习这个博客系统。

总体框架介绍

首先,这是一个Maven项目,核心的子项目是native-blog-webapp. 下面直接来看它的web.xml文件:

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    ...    
    <filter>
        <filter-name>dynamic</filter-name>
        <filter-class>com.zuoxiaolong.filter.DynamicFilter</filter-class>
    </filter>
    
    <filter-mapping>
        <filter-name>dynamic</filter-name>
        <url-pattern>*.ftl</url-pattern>
    </filter-mapping>

    ...
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>com.zuoxiaolong.mvc.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

    <listener>
        <listener-class>com.zuoxiaolong.listener.ConfigurationListener</listener-class>
    </listener>
    
    <welcome-file-list>
        <welcome-file>html/index.html</welcome-file>
    </welcome-file-list>

    <error-page>
        <error-code>500</error-code>
        <location>/html/error.html</location>
    </error-page>
    ...
</web-app>

其中一些不重要的配置已在这里省略。可以看到,应用主要是通过DynamicFilter来拦截所有对.ftl文件的访问,而用DispatcherServlet来处理所有对*.do的访问。其次,还配置了一个监听应用配置变化的listener,以及welcome页面、错误页面,这些将在后面介绍。

那么DynamicFilter是如何操作的呢?直接来看它的代码:

package com.zuoxiaolong.filter;

/**
 * @author 左潇龙
 * @since 2015年5月24日 上午1:24:45
 * Filter for all the .ftl files. It will generate all the data for the requested .ftl file
 * and output the merged result
 */
public class DynamicFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String requestUri = StringUtil.replaceSlants(((HttpServletRequest)request).getRequestURI());
        try {
            Map<String, Object> data = FreemarkerHelper.buildCommonDataMap(FreemarkerHelper.getNamespace(requestUri), ViewMode.DYNAMIC);
            boolean forbidden = loginFilter(data, requestUri, request);
            if (forbidden) {
                ((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);
                return;
            }
            String template = putCustomData(data, requestUri, request, response);
            response.setCharacterEncoding("UTF-8");
            FreemarkerHelper.generateByTemplatePath(template + ".ftl", response.getWriter(), data);
        } catch (Exception e) {
            throw new RuntimeException(requestUri, e);
        }
    }
    ...
}

它就是简单地把所有与这个页面相关的数据都生成出来,然后调用FreeMarker的帮助类来产生输出。

再来看DispatcherServlet类,它的思路和Struts框架有点像,就是自己作为一个分发器,把各种Action.do请求转发到对应的Servlet那里。具体的实现原理将在下一小节讲解。

综上,博客系统实际上只需要处理三类请求:

  • 静态的HTML页面,比如欢迎页面和错误页面。直接交给Web服务器处理即可。

  • 对某个具体页面的请求。由于所有的页面都是FreeMarker模板文件,请求将被DynamicFilter拦截处理。

  • 页面中一些动作的请求。这些*.do请求会被DispatcherServlet转发给合适的Servlet。

DispatcherServlet类的工作原理

这个类在com.zuoxiaolong.mvc包中,看名字就知道它是想实现MVC框架中的某些东东。注意这个包下面定义了两个注解:@Namespace@RequestMapping,后者会先介绍到。

com.zuoxiaolong.servlet包下面存放的是所有的处理具体动作请求的Servlet。这里它们并没有继承HTTPServlet,而是继承自抽象类AbstractServlet,重写其service()方法来完成具体功能。AbstractServlet类提供了一些通用的辅助函数给子类使用。

观察这些Servlet,你会发现有的在类前面声明了@RequestMapping,而有的并没有。比如AdminLogin类:

@RequestMapping("/admin/login.do")
public class AdminLogin extends AbstractServlet {
    ...

看这个注解的字面意思,就是要把"/admin/login.do"这个Url和AdminLogin这个Servlet对应起来,即这个Url的请求由AdminLogin类处理。

知道Spring-MVC的一看就懂。可是项目并没有用到Spring-MVC啊?关子就卖到这里,我们来看DispatcherServlet的代码:

public class DispatcherServlet extends HttpServlet {

    ...    
    private Map<String, Servlet> mapping;

    @Override
    public void init() throws ServletException {
        super.init();
        mapping = Scanner.scan();
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestUri = request.getRequestURI();
        String realRequestUri = requestUri.substring(request.getContextPath().length(), requestUri.length());
        Servlet servlet = mapping.get(realRequestUri);
        while (servlet == null) {
            if (realRequestUri.startsWith("/")) {
                servlet = mapping.get(StringUtil.replaceStartSlant(realRequestUri));
            } else {
                throw new RuntimeException("unknown request mapping.");
            }
        }
        servlet.execute(request, response);
    }
    ...
}

doPost()方法只是根据URI到mapping中查找具体的Servlet,那么关键就在于mapping的来源——Scanner.scan()方法了。

来看代码:

public abstract class Scanner {

    /**
     * Scan all the Servlet classes and put them into map. If Servlet has RequestMapping annotation,
     * then use the annotation as key; else, use servlet name + ".do" as key.
     * @return
     */
    public static Map<String, Servlet> scan() {
        Map<String, Servlet> mapping = new HashMap<>();
        File[] files = Configuration.getClasspathFile("com/zuoxiaolong/servlet").listFiles();
        for (int i = 0; i < files.length; i++) {
            String fileName = files[i].getName();
            if (fileName.endsWith(".class")) {
                fileName = fileName.substring(0, fileName.lastIndexOf(".class"));
            }
            try {
                Class<?> clazz = Configuration.getClassLoader().loadClass("com.zuoxiaolong.servlet." + fileName);
                if (Servlet.class.isAssignableFrom(clazz) && clazz != Servlet.class && clazz != AbstractServlet.class) {
                    RequestMapping requestMappingAnnotation = clazz.getDeclaredAnnotation(RequestMapping.class);
                    if (requestMappingAnnotation != null) {
                        mapping.put(requestMappingAnnotation.value(), (Servlet) clazz.newInstance());
                    } else {
                        String lowerCaseFileName = fileName.toLowerCase();
                        char[] originChars = fileName.toCharArray();
                        char[] lowerChars = lowerCaseFileName.toCharArray();
                        StringBuffer key = new StringBuffer();
                        for (int j = 0; j < originChars.length; j++) {
                            if (j == 0) {
                                key.append(lowerChars[j]);
                            } else if (j == originChars.length - 1) {
                                key.append(originChars[j]).append(".do");
                            } else {
                                key.append(originChars[j]);
                            }
                        }
                        mapping.put(key.toString(), (Servlet) clazz.newInstance());
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return mapping;
    }
}

是了,这里就是扫描com.zuoxiaolong.servlet包下的所有具体Servlet类,遇到有@RequestMapping注解的,就保存注解声明的映射;如果没有注解,默认的Url就是类名+".do"

到此,项目的主体框架就讲完了。下面就每个具体的功能来分析。

隐藏功能,Dota排行榜

这个功能不知道从哪来的,主页上也没有链接可以访问。但是访问http://localhost:8080/dota/do...的确是可以看到这个页面。主要有三个功能:

  • 点击右边栏的链接可以录入对战阵容和结果。

  • 左边可以输入一个五人的英雄阵容,然后系统去后台数据库里查,找到曾经战胜过这个阵容的英雄阵容,再按照胜率依次显示出来。

  • 右边栏的下方有英雄热度排行榜、胜率排行榜等。

总之有点像11对战平台的一些功能。有些没玩过Dota的好孩纸可能不懂。还好我玩过,所以理解起来没有难度(*^_^*)… 之所以要先介绍这个是因为它比较简单,下面就来分析一下。

webapp/dota/路径下放的是与Dota相关的所有FreeMarker模板。以第一个功能,录入比赛结果为例,它对应的是match_input.ftl,在底部有如下Javascript代码:

<script>
    $(document).ready(function() {
        $(".heroInput").autocomplete({
            source: "${contextPath}/heroFinder.do"
        });
        $("#submitButton").click(function(){
            $.ajax({
                url:"${contextPath}/saveMatch.do",
                type:"POST",
                data:{"a":$("#a1").val() + "," + $("#a2").val() + "," + $("#a3").val() + "," + $("#a4").val() + "," + $("#a5").val(),
                    "d":$("#d1").val() + "," + $("#d2").val() + "," + $("#d3").val() + "," + $("#d4").val() + "," + $("#d5").val(),
                    "result":$(":radio[name=result]:checked").val(),
                    "count":$("#count").val()
                },
                success:function(data){
                    if(data && data == 'success') {
                        alert("感谢你对公会的贡献,你输入的数据将会为公会贡献一份力量。");
                        window.location.href="${contextPath}/dota/dota_index.ftl";
                    } else {
                        alert(data);
                    }
                }
            });
        });
    });
</script>

这显然是一段JQuery代码。大致意思可以get到:

  1. 所有classheroInput的输入框都有自动提示,提示的内容要去地址"${contextPath}/heroFinder.do"查。

  2. 提交按钮点击时把数据提交到地址"${contextPath}/saveMatch.do",然后鸣谢。

居然还有自动完成提示,看上去蛮高级的!效果如下。

dota

下面首先来看${contextPath},它显然是个FTL变量。那么值是在哪设置的呢?还记得大明湖畔的夏雨荷吗?——哦不对,还记得前面讲到的DynamicFilter吗?它负责设置模板数据。在方法FreemarkerHelper.buildCommonDataMap()中设置了这个变量。它实际上是保存在setting.properties文件中的。在我们的环境中就是http://localhost:8080

接下来再看看"heroFinder.do"。在com.zuoxiaolong.servlet包中找到HeroFinder类,没错就是它。因为它没有用注解,所以对应的就是"heroFinder.do"。它的实现很简单,调用Dao层的数据库代码,查找英雄,再把结果用Json返回。

最后是"saveMatch.do"。类似地,有SaveMatch类,它的service()方法对数据做检查后存入数据库。

剩下的功能以此类推,可以在比赛输入页多输入几场比赛,就能在排行榜中看到英雄了。

再谈DynamicFilter

在第一节中讲到:

DynamicFilter类负责把所有与被请求的.ftl页面相关的数据都生成出来,然后调用FreeMarker的帮助类来产生输出。

具体分为三步:

  1. 先扫描所有的动态数据类,存放在dataMap表中。

  2. 对于所有请求,都调用FreemarkerHelper.buildCommonDataMap(),创建通用数据。

  3. 以请求的页面作为键,从dataMap表中获得对应的动态数据类,然后让这个类创建动态数据。最后用所有的数据产生输出页面。

以对主页的请求http://localhost:8080/blog/in...为例。先看第二步:方法:

public static Map<String, Object> buildCommonDataMap(String namespace, ViewMode viewMode) {
        Map<String, Object> data = new HashMap<>();
        String contextPath = Configuration.getSiteUrl();
        data.put("contextPath", contextPath);
        data.put("questionUrl", contextPath + "/question/question_index.ftl");
        if (ViewMode.DYNAMIC == viewMode) {
            data.put("indexUrl", IndexHelper.generateDynamicPath());
            data.put("questionIndexUrl", QuestionListHelper.generateDynamicPath(1));
            data.put("recordIndexUrl", RecordListHelper.generateDynamicPath(1));
            data.put("novelIndexUrl", ArticleListHelper.generateDynamicTypePath(1, 1));
        } else {
            ...
        }
        if (namespace.equals("dota")) {
            ...
        } else {
            List<Map<String, String>> articleList = DaoFactory.getDao(ArticleDao.class).getArticles("create_date", Status.published, Type.article, viewMode);
            data.put("accessCharts",DaoFactory.getDao(ArticleDao.class).getArticles("access_times", Status.published, Type.article, viewMode));
            data.put("newCharts",DaoFactory.getDao(ArticleDao.class).getArticles("create_date", Status.published, viewMode));
            data.put("recommendCharts",DaoFactory.getDao(ArticleDao.class).getArticles("good_times", Status.published, Type.article, viewMode));
            data.put("imageArticles",Random.random(articleList, DEFAULT_RIGHT_ARTICLE_NUMBER));
            data.put("hotTags", Random.random(DaoFactory.getDao(TagDao.class).getHotTags(), DEFAULT_RIGHT_TAG_NUMBER));
            data.put("newComments", DaoFactory.getDao(CommentDao.class).getLastComments(DEFAULT_RIGHT_COMMENT_NUMBER, viewMode));
            if (ViewMode.DYNAMIC == viewMode) {
                data.put("accessArticlesUrl", ArticleListHelper.generateDynamicPath("access_times", 1));
                data.put("newArticlesUrl", ArticleListHelper.generateDynamicPath("create_date", 1));
                data.put("recommendArticlesUrl", ArticleListHelper.generateDynamicPath("good_times", 1));
            } else {
                ...
            }
        }
        return data;
    }

前面的"indexUrl""questionIndexUrl"等变量很显然就是顶部的导航菜单的Url
而后面创建的这些变量,大部分都在右边栏里面用到。比如排行榜,它是三块互相切换的div

排行榜

相信你已经明白了。这里的FreeMarker模板在webapp/common/chart.ftl文件中。

再接着来看动态数据是怎么产生的。观察DataMapLoader.load()的代码,你会发现它跟前面讲的Scanner.scan()方法很像。类似地,它会到com.zuoxiaolong.dynamic包下面找DataMap接口的实现类,并把类名中的大写用下划线转化后,作为key存到Map中。DataMap接口表示这是一个动态数据的提供类,其putCustomData()方法用来输出数据。

注意这里使用到了@Namespace注解。

...
Namespace namespaceAnnotation = clazz.getDeclaredAnnotation(Namespace.class);
if (namespaceAnnotation == null) {
    throw new RuntimeException(clazz.getName() + " must has annotation with @Namespace");
}
dataMap.put(namespaceAnnotation.value() + "/" + key.toString(), (DataMap) clazz.newInstance());
...

扫描时还把key加上了@Namespace的值。那么这个注解到底是干嘛的呢?

@Namespace注解就是请求的第一层路径。比如对请求http://localhost:8080/blog/in...namespace就是blog。还有一些其他的namespace,比如dotaadminquestion

这些namespace都能在dynamic包下的类中看到。有些类的注解声明并没有提供值,这是因为@Namespace注解的默认值就是blog

好了,既然我们请求的是http://localhost:8080/blog/in...,那么对应的动态数据提供类就应该是namespaceblog的Index类。找一下自然有这个类。它的动作很简单:

@Namespace
public class Index implements DataMap {
    
    @Override
    public void putCustomData(Map<String, Object> data,HttpServletRequest request, HttpServletResponse response) {
        IndexHelper.putDataMap(data, VIEW_MODE);
    }
}

IndexHelper.putDataMap()方法只是从数据库中找出所有已发布的文章,把它们存放在变量"articles"中。负责渲染主页正文的index_main.ftl使用了这个变量。来欣赏下它的代码:

<div class="main-div">
    <h1>
        最新文章
    </h1>
<#if articles??>
    <#list articles as article>
        <#if article_index gt 5>
            <#break />
        </#if>
        <div class="blogs">
            <figure><img src="${article.icon}" title="niubi-job——一个分布式的任务调度框架"></figure>
            <ul>
                <h3><a href="${contextPath}${article.url}">${article.subject}</a></h3>
                <p>
                ${article.summary}...
                </p>
                <p class="autor">
                    <span class="username_bg_image float_left"><a href="#">${article.username}</a></span>
                    <span class="time_bg_image float_left">${article.create_date?substring(0,10)}</span>
                    <span class="access_times_bg_image float_right">浏览(${article.access_times})</span>
                    <span class="comment_times_bg_image float_right">评论(${article.comment_times})</span>
                </p>
            </ul>
        </div>
    </#list>
</#if>
</div>

Toconscience
153 声望39 粉丝

引用和评论

0 条评论