其實我開始學習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。
這個類在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"。
到此,項目的主體框架就講完了。下面就每一個具體的功能來分析。
這個功能不知道從哪來的,主頁上也沒有連接能夠訪問。可是訪問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到:
全部class是heroInput
的輸入框都有自動提示,提示的內容要去地址"${contextPath}/heroFinder.do"
查。
提交按鈕點擊時把數據提交到地址"${contextPath}/saveMatch.do"
,而後鳴謝。
竟然還有自動完成提示,看上去蠻高級的!效果以下。
下面首先來看${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
類負責把全部與被請求的.ftl頁面相關的數據都生成出來,而後調用FreeMarker的幫助類來產生輸出。
具體分爲三步:
先掃描全部的動態數據類,存放在dataMap
表中。
對於全部請求,都調用FreemarkerHelper.buildCommonDataMap()
,建立通用數據。
以請求的頁面做爲鍵,從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,好比dota、admin、question…
這些namespace都能在dynamic包下的類中看到。有些類的註解聲明並無提供值,這是由於@Namespace
註解的默認值就是blog。
好了,既然咱們請求的是http://localhost:8080/blog/in...,那麼對應的動態數據提供類就應該是namespace是blog的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>