Action 分发机制实现原理

摘要

整个 Web 应用中,只有一个 Servlet,它就是 DispatcherServlet,它拦截了所有的请求。

本文是《轻量级 Java Web 框架架构设计》的系列博文。

整个 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<RequestBean, ActionBean> actionMap = ActionHelper.getActionMap();
        for (Map.Entry<RequestBean, ActionBean> 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<Object> paramList = new ArrayList<Object>();
                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<RequestBean, ActionBean> actionMap = new HashMap<RequestBean, ActionBean>();

    static {
        // 获取并遍历所有 Action 类
        List<Class<?>> 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<RequestBean, ActionBean> 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<Class<?>> getClassListBySuper(Class<?> superClass) {
        return ClassUtil.getClassListBySuper(packageName, superClass);
    }

    public static List<Class<?>> getClassListByAnnotation(Class<? extends Annotation> annotationClass) {
        return ClassUtil.getClassListByAnnotation(packageName, annotationClass);
    }

    public static List<Class<?>> getClassListByInterface(Class<?> interfaceClass) {
        return ClassUtil.getClassListByInterface(packageName, interfaceClass);
    }
}

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

补充(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<String, Object> requestParamMap = WebUtil.getRequestParamMap(request);

WebUtil.getRequestParamMap 方法,实际上就是对 request.getParameterNames() 方法的封装,WebUtil 代码片段如下:

public class WebUtil {
    ...
    // 从Request中获取所有参数(当参数名重复时,用后者覆盖前者)
    public static Map<String, Object> getRequestParamMap(HttpServletRequest request) {
        Map<String, Object> paramMap = new HashMap<String, Object>();
        Enumeration<String> 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)

感谢网友 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 {
    ...
    @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<Object> paramList = createParamList(matcher, requestParamMap, requestParamTypes);
        ...
    }
    ...
    private List<Object> createParamList(Matcher matcher, Map<String, String> requestParamMap, Class<?>[] requestParamTypes) {
        List<Object> paramList = new ArrayList<Object>();
        // 遍历正则表达式中所匹配的组
        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 类型取代。

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

 

补充(2013-11-19)

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

以前的代码是这样写的:

...
                Enumeration<String> 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);
                    }
                }
...

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

...
                Enumeration<String> 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);
...
}

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

IT家园
IT家园

网友最新评论 (0)

发表我的评论
取消评论
表情