Servlet與JSP項目實戰 — 博客系統(上)

其實我開始學習Servlet和JSP是受了一篇《阿里社招面試如何準備,以及對於Java程序猿學習當中各個階段的建議》的啓發。做者左瀟龍的我的主頁在此,裏面的文章都挺有意思的。這個博客系統是左瀟龍本身寫的,代碼開源在GitHub上。html

項目恰好是用Servlet + FreeMarker,沒有上任何框架,可是MVC分層都有。java

我Fork了一份代碼,在已有基礎上提交了一些Bug Fix和註釋,項目地址在此。這篇文章就是來分析學習這個博客系統。git

整體框架介紹

首先,這是一個Maven項目,核心的子項目是native-blog-webapp. 下面直接來看它的web.xml文件:github

<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頁面、錯誤頁面,這些將在後面介紹。web

那麼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的幫助類來產生輸出。ajax

再來看DispatcherServlet類,它的思路和Struts框架有點像,就是本身做爲一個分發器,把各類Action.do請求轉發到對應的Servlet那裏。具體的實現原理將在下一小節講解。數據庫

綜上,博客系統實際上只須要處理三類請求:服務器

  • 靜態的HTML頁面,好比歡迎頁面和錯誤頁面。直接交給Web服務器處理便可。mvc

  • 對某個具體頁面的請求。因爲全部的頁面都是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>
相關文章
相關標籤/搜索