专栏名称: ImportNew
伯乐在线旗下账号,专注Java技术分享,包括Java基础技术、进阶技能、架构设计和Java技术领域动态等。
目录
相关文章推荐
芋道源码  ·  SpringBoot 实现任意文件在线预览功能 ·  18 小时前  
芋道源码  ·  AI 正在培养一代不会编程的“文盲程序员” ·  2 天前  
Java编程精选  ·  不引入ES,如何利用 MySQL 实现模糊匹配 ·  3 天前  
51好读  ›  专栏  ›  ImportNew

Action 分发机制实现原理

ImportNew  · 公众号  · Java  · 2017-09-08 12:00

正文

(点击 上方公众号 ,可快速关注)


来源:黄勇,

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 {







请到「今天看啥」查看全文