(点击
上方公众号
,可快速关注)
来源:黄勇,
my.oschina.net/huangyong/blog/158738
如有好文章投稿,请点击 → 这里了解详情
整个 Web 应用中,只有一个 Servlet,它就是 DispatcherServlet。它拦截了所有的请求,内部的处理逻辑大致是这样的:
1. 获取请求相关信息(请求方法与请求 URL),封装为 RequestBean。
2. 根据 RequestBean 从 Action Map 中获取对应的 ActionBean(包括 Action 类与 Action 方法)。
3. 解析请求 URL 中的占位符,并根据真实的 URL 生成对应的 Action 方法参数列表(Action 方法参数的顺序与 URL 占位符的顺序相同)。
4. 根据反射创建 Action 对象,并调用 Action 方法,最终获取返回值(Result)。
5. 将返回值转换为 JSON 格式(或者 XML 格式,可根据 Action 方法上的 @Response 注解来判断)。
@WebServlet("/*")
public class DispatcherServlet extends HttpServlet {
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取当前请求相关数据
String currentRequestMethod = request.getMethod();
String currentRequestURL = request.getPathInfo();
// 屏蔽特殊请求
if (currentRequestURL.equals("/favicon.ico")) {
return;
}
// 获取并遍历 Action 映射
Map
actionMap = ActionHelper.getActionMap();
for (Map.Entry
actionEntry : actionMap.entrySet()) {
// 从 RequestBean 中获取 Request 相关属性
RequestBean reqestBean = actionEntry.getKey();
String requestURL = reqestBean.getRequestURL(); // 正则表达式
String requestMethod = reqestBean.getRequestMethod();
// 获取正则表达式匹配器(用于匹配请求 URL 并从中获取相应的请求参数)
Matcher matcher = Pattern.compile(requestURL).matcher(currentRequestURL);
// 判断请求方法与请求 URL 是否同时匹配
if (requestMethod.equals(currentRequestMethod) && matcher.matches()) {
// 初始化 Action 对象
ActionBean actionBean = actionEntry.getValue();
// 初始化 Action 方法参数列表
List
paramList = new ArrayList
();
for (int i = 1; i <= matcher.groupCount(); i++) {
String param = matcher.group(i);
// 若为数字,则需要强制转换,并放入参数列表中
if (StringUtil.isDigits(param)) {
paramList.add(Long.parseLong(param));
} else {
paramList.add(param);
}
}
// 从 ActionBean 中获取 Action 相关属性
Class> actionClass = actionBean.getActionClass();
Method actionMethod = actionBean.getActionMethod();
try {
// 创建 Action 实例
Object actionInstance = actionClass.newInstance();
// 调用 Action 方法(传入请求参数)
Object actionMethodResult = actionMethod.invoke(actionInstance, paramList.toArray());
if (actionMethodResult instanceof Result) {
// 获取 Action 方法返回值
Result result = (Result) actionMethodResult;
// 将返回值转为 JSON 格式并写入 Response 中
WebUtil.writeJSON(response, result);
}
} catch (Exception e) {
e.printStackTrace();
}
// 若成功匹配,则终止循环
break;
}
}
}
}
通过 ActionHelper 加载 classpath 中所有的 Action。凡是继承了 BaseAction 的类,都视为 Action。
public class ActionHelper {
private static final Map
actionMap = new HashMap
();
static {
// 获取并遍历所有 Action 类
List
> actionClassList = ClassHelper.getClassList(BaseAction.class);
for (Class> actionClass : actionClassList) {
// 获取并遍历该 Action 类中的所有方法(不包括父类中的方法)
Method[] actionMethods = actionClass.getDeclaredMethods();
if (ArrayUtil.isNotEmpty(actionMethods)) {
for (Method actionMethod : actionMethods) {
// 判断当前 Action 方法是否带有 @Request 注解
if (actionMethod.isAnnotationPresent(Request.class)) {
// 获取 @Requet 注解中的 URL 字符串
String[] urlArray = actionMethod.getAnnotation(Request.class).value().split(":");
if (ArrayUtil.isNotEmpty(urlArray)) {
// 获取请求方法与请求 URL
String requestMethod = urlArray[0];
String requestURL = urlArray[1]; // 带有占位符
// 将请求路径中的占位符 {\w+} 转换为正则表达式 (\\w+)
requestURL = StringUtil.replaceAll(requestURL, "\\{\\w+\\}", "(\\\\w+)");
// 将 RequestBean 与 ActionBean 放入 Action Map 中
actionMap.put(new RequestBean(requestMethod, requestURL), new ActionBean(actionClass, actionMethod));
}
}
}
}
}
}
public static Map
getActionMap() {
return actionMap;
}
}
封装请求相关数据,包括请求方法与请求 URL。
public class RequestBean {
private String requestMethod;
private String requestURL;
public RequestBean(String requestMethod, String requestURL) {
this.requestMethod = requestMethod;
this.requestURL = requestURL;
}
public String getRequestMethod() {
return requestMethod;
}
public void setRequestMethod(String requestMethod) {
this.requestMethod = requestMethod;
}
public String getRequestURL() {
return requestURL;
}
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
}
封装 Action 相关数据,包括 Action 类与 Action 方法。
public class ActionBean {
private Class> actionClass;
private Method actionMethod;
public ActionBean(Class> actionClass, Method actionMethod) {
this.actionClass = actionClass;
this.actionMethod = actionMethod;
}
public Class> getActionClass() {
return actionClass;
}
public void setActionClass(Class> actionClass) {
this.actionClass = actionClass;
}
public Method getActionMethod() {
return actionMethod;
}
public void setActionMethod(Method actionMethod) {
this.actionMethod = actionMethod;
}
}
封装 Action 方法的返回值,可序列化为 JSON 或 XML。
public class Result extends BaseBean {
private int error;
private Object data;
public Result(int error) {
this.error = error;
}
public Result(int error, Object data) {
this.error = error;
this.data = data;
}
public int getError() {
return error;
}
public void setError(int error) {
this.error = error;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
下面以 ProductAction为例,展示 Action 的写法:
public class ProductAction extends BaseAction {
private ProductService productService = new ProductServiceImpl(); // 目前尚未使用依赖注入
@Request("GET:/product/{id}")
public Result getProductById(long productId) {
if (productId == 0) {
return new Result(ERROR_PARAM);
}
Product product = productService.getProduct(productId);
if (product != null) {
return new Result(OK, product);
} else {
return new Result(ERROR_DATA);
}
}
}
大家可对以上实现进行点评!
补充(2013-09-04)
通过反射创建 Action 对象,性能确实有些低,我稍微做了一些优化,在调用 invoke 方法前,设置 Accessiable 属性为 true。注意:方法的 Accessiable 属性并非它的字面意思“可访问的”(为 true 才能访问,为 false 就不能访问了),它真正的作用是为了取消 Java 反射提供的类型安全性检测。在大量反射调用的过程中,这样做可以提高 20 倍以上的性能(据相关人事透露)。
...
// 从 ActionBean 中获取 Action 相关属性
Class> actionClass = actionBean.getActionClass();
Method actionMethod = actionBean.getActionMethod();
try {
// 创建 Action 实例
// Object actionInstance = actionClass.newInstance();
Object actionInstance = BeanHelper.getBean(actionClass);
// 调用 Action 方法(传入请求参数)
actionMethod.setAccessible(true); // 取消类型安全检测(可提高反射性能)
Object actionMethodResult = actionMethod.invoke(actionInstance, paramList.toArray());
if (actionMethodResult instanceof Result) {
// 获取 Action 方法返回值
Result result = (Result) actionMethodResult;
// 将返回值转为 JSON 格式并写入 Response 中
WebUtil.writeJSON(response, result);
}
} catch (Exception e) {
e.printStackTrace();
}
...
现在可通过 BeanHelper 来获取 Action 实例了(由于框架已实现轻量级依赖注入功能),所以无需在调用耗性能的 newInstance() 方法。
需要性能优化的地方还很多,也请网友们多多提供建议。
补充(2013-09-04)
有些网友提出怎么没有看见 ClassHelper 呢?不好意思,是我的疏忽,现在补上,相信还不晚吧?
public class ClassHelper {
private static final String packageName = ConfigHelper.getProperty("package.name");
public static List
> getClassListBySuper(Class> superClass) {
return ClassUtil.getClassListBySuper(packageName, superClass);
}
public static List
> getClassListByAnnotation(Class extends Annotation> annotationClass) {
return ClassUtil.getClassListByAnnotation(packageName, annotationClass);
}
public static List
> getClassListByInterface(Class> interfaceClass) {
return ClassUtil.getClassListByInterface(packageName, interfaceClass);
}
}
会不会太简单?ClassHelper 实际上是通过 ClassUtil 来操作的,关于 ClassUtil 的代码细节,请阅读这篇博文《
ClassUtil.java 代码细节
》。
http://my.oschina.net/huangyong/blog/159155
补充(2013-09-05)
肯定有网友会问:如果直接发送的是 HTML 请求,按照常规思路,返回的应该就是一个 HTML 文件啊?而 DispatcherServlet 拦截了所有的请求(”/*”),那么 .html、.css、.js 等这样的请求也会被拦截了,更不用说是图片文件了。此外,代码里还故意忽略掉了“/favicon.ico”,这个到是可以理解的。有没有办法过滤掉所有的静态资源呢?
没错,当时我忽略了这个问题,经过一番思考,有一个简单的方法可以处理以上问题。见如下代码:
@WebServlet(urlPatterns = "/*", loadOnStartup = 0)
public class DispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
// 用 Default Servlet 来映射静态资源
ServletContext context = config.getServletContext();
ServletRegistration registration = context.getServletRegistration("default");
registration.addMapping("/favicon.ico", "/www/*");
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取当前请求相关数据
String currentRequestMethod = request.getMethod();
String currentRequestURL = request.getPathInfo();
...
}
}
变更如下:
1. 在 @WebServlet 注解中添加了 loadOnStartup 属性,并将其值设置为0。这以为着,这个 Servlet 会在容器(Tomcat)启动时自动加载,此时容器会自动调用 init() 方法。
2. 在 init() 方法中,首先获取 ServletContext,拿到这个东西以后,直接调用 getServletRegistration() 方法,传入一个名为 default 的参数,这意味着,从容器中获取 Default Servlet(这个 Servlet 是由容器实现的,它负责做普通的静态资源响应)。
3. 以上拿到了 ServletRegistration 对象,那么直接调用该对象的 addMapping() 方法,该方法支持动态参数,直接添加需要 Default Servlet 处理的请求。注意:我已经 HTML、CSS、JS、图片等静态资源,放入 www 目录下,以后还可以在 Apache HTTP Server 中配置虚拟机,实现对静态资源的缓存、压缩等。
经过以上修改,DispatcherServlet 可忽略所有静态请求,只对动态请求进行处理。
补充(2013-09-05)
对于 POST 这类请求,又改如何处理呢?我尝试了一下,看看这样的方式能否让大家满意:
在 DispatcherServlet 中添加几行代码:
// 获取请求参数映射(包括:Query String 与 Form Data)
Map
requestParamMap = WebUtil.getRequestParamMap(request);
WebUtil.getRequestParamMap 方法,实际上就是对 request.getParameterNames() 方法的封装,WebUtil 代码片段如下:
public class WebUtil {
...
// 从Request中获取所有参数(当参数名重复时,用后者覆盖前者)
public static Map
getRequestParamMap(HttpServletRequest request) {
Map
paramMap = new HashMap
();
Enumeration
paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
String paramValue = request.getParameter(paramName);
paramMap.put(paramName, paramValue);
}
return paramMap;
}
}
拿到了这个 requestParamMap 之后,剩下来的事情就是想办法将其放入 paramList 中了,见如下代码片段:
...
// 向参数列表中添加请求参数映射
if (MapUtil.isNotEmpty(requestParamMap)) {
paramList.add(requestParamMap);
}
...
那么实际是如何运用的呢?请参考这篇博文《
再来一个示例吧
》。
http://my.oschina.net/huangyong/blog/159412
补充(2013-10-30)
感谢网友 zoujianfang 的建议:能否将 DispatcherServlet 中初始化的工作交给 Listener(ServletContextListener)去完成呢?这样可在 DispatcherServlet 之前就完成初始化。
此外,可将初始化工作从 DispatcherServlet 剥离出来,这样更加符合“单一职责原则”和“开放封闭原则”。
非常感谢,这个建议非常好!现已被采纳。
现已在框架中增加了一个 ContainerListener,实现 ServletContextListener 接口,专用于系统初始化与销毁工作。代码如下:
@WebListener
public class ContainerListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 初始化 Helper 类
InitHelper.init();
// 添加 Servlet 映射
addServletMapping(sce.getServletContext());
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
private void addServletMapping(ServletContext context) {
// 用 DefaultServlet 映射所有静态资源
ServletRegistration defaultServletRegistration = context.getServletRegistration("default");
defaultServletRegistration.addMapping("/favicon.ico", "/static/*", "/index.html");
// 用 JspServlet 映射所有 JSP 请求
ServletRegistration jspServletRegistration = context.getServletRegistration("jsp");
jspServletRegistration.addMapping("/dynamic/jsp/*");
// 用 UploadServlet 映射 /upload.do 请求
ServletRegistration uploadServletRegistration = context.getServletRegistration("upload");
uploadServletRegistration.addMapping("/upload.do");
}
}
同时去掉了 DispatcherServlet 中 init 方法及其相关代码。
@WebServlet("/*")
public class DispatcherServlet extends HttpServlet {