其实我开始学习Servlet和JSP是受了一篇《阿里社招面试如何准备,以及对于Java程序猿学习当中各个阶段的建议》的启发。作者左潇龙的个人主页在此,里面的文章都挺有意思的。这个博客系统是左潇龙自己写的,代码开源在GitHub上。
项目刚好是用Servlet + FreeMarker,没有上任何框架,但是MVC分层都有。
我Fork了一份代码,在已有基础上提交了一些Bug Fix和注释,项目地址在此。这篇文章就是来分析学习这个博客系统。
总体框架介绍
首先,这是一个Maven项目,核心的子项目是native-blog-webapp. 下面直接来看它的web.xml文件:
...
dynamic
com.zuoxiaolong.filter.DynamicFilter
dynamic
*.ftl
...
dispatcherServlet
com.zuoxiaolong.mvc.DispatcherServlet
1
dispatcherServlet
*.do
com.zuoxiaolong.listener.ConfigurationListener
html/index.html
500
/html/error.html
...
其中一些不重要的配置已在这里省略。可以看到,应用主要是通过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 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 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 scan() {
Map 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/dota_index.ftl的确是可以看到这个页面。主要有三个功能:
- 点击右边栏的链接可以录入对战阵容和结果。
- 左边可以输入一个五人的英雄阵容,然后系统去后台数据库里查,找到曾经战胜过这个阵容的英雄阵容,再按照胜率依次显示出来。
- 右边栏的下方有英雄热度排行榜、胜率排行榜等。
总之有点像11对战平台的一些功能。有些没玩过Dota的好孩纸可能不懂。还好我玩过,所以理解起来没有难度(*_*)… 之所以要先介绍这个是因为它比较简单,下面就来分析一下。
在webapp/dota/路径下放的是与Dota相关的所有FreeMarker模板。以第一个功能,录入比赛结果为例,它对应的是match_input.ftl,在底部有如下Javascript代码:
这显然是一段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
在第一节中讲到:
DynamicFilter
类负责把所有与被请求的.ftl页面相关的数据都生成出来,然后调用FreeMarker的帮助类来产生输出。
具体分为三步:
- 先扫描所有的动态数据类,存放在
dataMap
表中。 - 对于所有请求,都调用
FreemarkerHelper.buildCommonDataMap()
,创建通用数据。 - 以请求的页面作为键,从
dataMap
表中获得对应的动态数据类,然后让这个类创建动态数据。最后用所有的数据产生输出页面。
以对主页的请求http://localhost:8080/blog/index.ftl为例。先看第二步:方法:
public static Map buildCommonDataMap(String namespace, ViewMode viewMode) {
Map 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
前面的"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/index.ftl,namespace就是blog。还有一些其他的namespace,比如dota、admin、question…
这些namespace都能在dynamic包下的类中看到。有些类的注解声明并没有提供值,这是因为@Namespace
注解的默认值就是blog。
好了,既然我们请求的是http://localhost:8080/blog/index.ftl,那么对应的动态数据提供类就应该是namespace是blog的Index类。找一下自然有这个类。它的动作很简单:
@Namespace
public class Index implements DataMap {
@Override
public void putCustomData(Map data,HttpServletRequest request, HttpServletResponse response) {
IndexHelper.putDataMap(data, VIEW_MODE);
}
}
而IndexHelper.putDataMap()
方法只是从数据库中找出所有已发布的文章,把它们存放在变量"articles"
中。负责渲染主页正文的index_main.ftl使用了这个变量。来欣赏下它的代码:
最新文章
<#if articles??>
<#list articles as article>
<#if article_index gt 5>
<#break />
#if>
${article.subject}
${article.summary}...
${article.username}
${article.create_date?substring(0,10)}
浏览(${article.access_times})
评论(${article.comment_times})
#list>
#if>