博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Action 分发机制实现原理
阅读量:6520 次
发布时间:2019-06-24

本文共 17255 字,大约阅读时间需要 57 分钟。

hot3.png

本文是《》的系列博文。

整个 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
annotationClass) { return ClassUtil.getClassListByAnnotation(packageName, annotationClass); } public static List
> getClassListByInterface(Class
interfaceClass) { return ClassUtil.getClassListByInterface(packageName, interfaceClass); }}

会不会太简单?ClassHelper 实际上是通过 ClassUtil 来操作的,关于 ClassUtil 的代码细节,请阅读这篇博文《 》。

补充(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);}...

那么实际是如何运用的呢?请参考这篇博文《 》。

补充(2013-10-30)

感谢网友  的建议:能否将 DispatcherServlet 中初始化的工作交给 Listener(ServletContextListener)去完成呢?这样可在 DispatcherServlet 之前就完成初始化。

此外,可将初始化工作从 DispatcherServlet 剥离出来,这样更加符合“单一职责原则”和“开放封闭原则”。

非常感谢,这个建议非常好!现已被采纳。

现已在框架中增加了一个 ContainerListener,实现 ServletContextListener 接口,专用于系统初始化与销毁工作。代码如下:

@WebListenerpublic 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 {    ...    @Override    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        ...    }}

补充(2013-11-14)

以前在 Action 方法的参数类型转换的时候存在一些问题,比如:String 类型的 123,只能转换成 long 类型,而不能转换为 String 类型。这是不恰当的,应该根据 Action 方法的参数类型来决定具体转换为哪一种类型,此外还需要考虑基本类型与包装类型的兼容问题。下面是具体的改进细节:

@WebServlet("/*")public class DispatcherServlet extends HttpServlet {    ...    @Override    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        ...        // 获取 Action 方法参数类型        Class
[] requestParamTypes = actionBean.getActionMethod().getParameterTypes(); // 创建 Action 方法参数列表 List paramList = createParamList(matcher, requestParamMap, requestParamTypes); ... } ... private List createParamList(Matcher matcher, Map
requestParamMap, Class
[] requestParamTypes) { List
paramList = new ArrayList(); // 遍历正则表达式中所匹配的组 for (int i = 1; i <= matcher.groupCount(); i++) { // 获取请求参数 String param = matcher.group(i); // 获取参数类型(支持四种类型:int/Integer、long/Long、double/Double、String) Class
paramType = requestParamTypes[i - 1]; if (paramType.equals(int.class) || paramType.equals(Integer.class)) { paramList.add(CastUtil.castInt(param)); } else if (paramType.equals(long.class) || paramType.equals(Long.class)) { paramList.add(CastUtil.castLong(param)); } else if (paramType.equals(double.class) || paramType.equals(Double.class)) { paramList.add(CastUtil.castDouble(param)); } else if (paramType.equals(String.class)) { paramList.add(param); } } // 向参数列表中添加请求参数映射 if (MapUtil.isNotEmpty(requestParamMap)) { paramList.add(requestParamMap); } return paramList; } ...}

首先从 actionBean 中获取 actionMethod 的参数类型(requestParamTypes),然后调用 createParamList 方法获取参数列表。在遍历正则表达式的时候,获取当前的参数类型(paramType),通过一个多条件语句去判断具体是那种类型,此时需要考虑原始类型与包装类型,统一使用 Java 的“自动装箱”技术来实现类型转换。

需要说明的是,目前只支持四种类型:int/Integer、long/Long、double/Double、String,个人觉得已经够用了,Date 类型可用 String 类型或 long 类型取代。

非常感谢网友  的建议!让 Action 方法的参数类型转换更加灵活。感谢开源中国,提供了这么好的交流平台!

补充(2013-11-19)

如果在 form 中有两个 checkbox(name 相同),现在将它们同时勾选,然后提交表单。此时无法从 request 中获取到所有的 checkbox 中的 value,而只能获取第一个 value,这是一个 bug!

以前的代码是这样写的:

...                Enumeration
paramNames = request.getParameterNames(); while (paramNames.hasMoreElements()) { String paramName = paramNames.nextElement(); if (checkParamName(paramName)) { String paramValue = request.getParameter(paramName); // 注意:这里有 bug,因为不同的 checkbox 会拥有相同的 paramName。 paramMap.put(paramName, paramValue); } }...

经过 的贡献,现已修改此 bug,代码片段如下:

...                Enumeration
paramNames = request.getParameterNames();                while (paramNames.hasMoreElements()) {                    String paramName = paramNames.nextElement();                    if (checkParamName(paramName)) {                        String[] paramValues = request.getParameterValues(paramName); // 先获取一个数组,然后分两种情况进行判断                        if (ArrayUtil.isNotEmpty(paramValues)) {                            if (paramValues.length == 1) { // 若只有一个参数,则直接获取                                paramMap.put(paramName, paramValues[0]);                            } else {                                // 若有多个参数,则通过一个循环去追加,并通过特殊字符进行分割                                StringBuilder paramValue = new StringBuilder("");                                for (int i = 0; i < paramValues.length; i++) {                                    paramValue.append(paramValues[i]);                                    if (i != paramValues.length - 1) {                                        paramValue.append(StringUtil.SEPARATOR);                                    }                                }                                paramMap.put(paramName, paramValue.toString());                            }                        }                    }                }...

其中 StringUtil.SEPARATOR 在 StringUtil 中定义了:

public class StringUtil {    // 字符串分隔符    public static final String SEPARATOR = String.valueOf((char) 29);...}

有了 ,妈妈再也不用担心我的 bug 了!

转载于:https://my.oschina.net/huangyong/blog/158738

你可能感兴趣的文章
如何防止应用程序泄密?
查看>>
一文带你看懂物联网开源操作系统
查看>>
什么是实践中真正在用的数据科学系统?
查看>>
新型智慧城市:构建“互联网+”新生活
查看>>
韩企全球首造72层3D NAND芯片 下半年或量产
查看>>
《R语言编程艺术》——1.4 R语言中一些重要的数据结构
查看>>
如何让你的手机比别人最先升级到 Android L
查看>>
阿里云开源编程马拉松入围项目
查看>>
Mozilla 开源支持计划:首批捐助 7 开源项目 50 万美元
查看>>
《Photoshop混合模式深度剖析》目录—导读
查看>>
《为iPad而设计:打造畅销App》——抓住iPad的核心用法
查看>>
华尔街宫斗戏升温:银行巨头和纽交所争夺交易数据所有权
查看>>
《精通自动化测试框架设计》—第2章 2.6节使用数据库
查看>>
《网站性能监测与优化》一2.4 软件服务应用网站
查看>>
《HTML5 开发实例大全》——1.26 使用鼠标光标拖动网页中的文字
查看>>
【JSP开发】有关session的一些重要的知识点
查看>>
生产库中遇到mysql的子查询
查看>>
redis debug命令详解
查看>>
阿里P6Java工程师的学习经历自述,希望新人少走弯路
查看>>
【工具推荐】ADB IDEA
查看>>