这天一热,胖子的的世界就是一片凄惨啊,随便动动身子,就跟洗个澡似得,心情固然烦躁,一烦躁就很难静下心来写东西了......所以这一段没咋用心写,就稍微水点吧,同时,我又打算要减肥了!>_<!。
上一次我们介绍了session的实现,使web服务器(现在总觉得准确来说应该叫可独立部署的web框架,称不上服务器)具备了基本功能,但是仔细一想就会发现一个严重的问题:每当实现一个新的controller,那么就需要在invokController方法里边增加判断,以便url能够找到对应controller。对于一个web服务器(或是框架)而言,内部应该是封闭的,即使是为了支持拓展也应该是提供外部接口实现,通过改代码来修改,绝对是极其糟糕的设计。下边我们来看几种实现方式:
一、如何实现
首先我们来思考一个url请求时如何映射到对应的处理方法的,常规思路如下:
可以看到这里边有两个关键点:1、路由表的结构是怎么实现的?2、查到对应类的对象是怎么加载的(实例化)?只要弄清楚了这两个问题,其实一切就迎刃而解了。
路由表的主要作用是通过url能够找到对应的对应处理类的对应方法,我们很容易就能想到的结构就是key-value结构的map了,实际上多数情况也的确如此。但是要注意到是,在实际存储的时候,具体的内容要看情况而定,在简单情况下直接key存uri,value存处理类信息就行,但是当你想支持多种映射规则时(如前缀匹配、正则匹配等等),就要归类存储了。总体而言这一部分还是很容易实现的。
下边我们来看一下第二个问题,路由表里边一般存储的应该是类信息(至于到具体方法的映射大同小异),而且多数情况下只是类名,但是实际处理却需要类对象来执行。那么我们就需要考虑这些类对象时如何加载的。一种方法就是在web服务器启动时就实例化所有注册的类(应该也是通过反射),而另一种则是在使用的时候根据类名通过反射动态生成对象,其实两者区别只是对象实例化的时机。(这里突然想到一个问题,一般而言这里实例化的对象应该是单例的,也就是相同请求用的对象都是一个,并发处理时在器内部实现的,本项目目前暂未考虑这点,之后会改进的)
二、功能设计
还是先来设计一下,这一次比较简单,对于路由表我们目前只需要支持uri和controller一一对应的情况,所以直接使用一个map存储就好。而配置为了方便直接通过注解实现(之前实现了配置文件配置,后来去掉了,需要的话可以自己加上),web服务器启动时先获取制定目录下所有controller类型注解,然后根据注解信息将映射存入map。而url请求时,从map中查找到对应类名,通过反射实例化并调用对应方法。
三、实现代码
首先来看一下注解的定义( java自定义注解 ),这里定义了两个注解,一个是url的映射,另一个则是方法映射(只有声明该注解的方法会被映射 ):
package org.eh.core.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Controller注解 * @author guojing * @date 2014-3-5 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface Controller { /** * controller名,暂时无用 */ public String name(); /** * 对应url请求路径,如htp://127.0.0.1/test/list.do 则对应 controller为:/test/,对应方法为:list */ public String url(); } /** * 方法映射注解 * @author guojing * @date 2014-3-13 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface RequestMapping { }
定义完了注解,再来实现一个 AnnocationHandler处理注解信息 ,该类主要实现三个功能:1. 获取指定包下的所有类名(包含包名),2. 将所有注解Controller加入Constants.UrlClassMap,3. 获取类的指定方法 ,其实现如下:
/** * 注解处理类 * @author guojing * @date 2014-3-5 */ public class AnnocationHandler { /** * 将所有注解Controller加入Constants.UrlClassMap * @param parkage 类名(包含包路径) */ @SuppressWarnings({ "unchecked", "rawtypes" }) public void paserControllerAnnocation(String parkage) throws ClassNotFoundException { List<String> classlist = getPkgClass(parkage); for (String str : classlist) { Class c = Class.forName(str); if (c.isAnnotationPresent(Controller.class)) { Controller desc = (Controller) c.getAnnotation(Controller.class); Constants.UrlClassMap.put(desc.url(), str); } } } /** * 获取指定包下的所有类名(包含包名) * @param parkage 指定包名 * @return */ public List<String> getPkgClass(String parkage) { String path = Constants.CLASS_PATH + parkage.replace(".", "/") + "/"; List<String> list = new ArrayList<String>(); File file = new File(path); for (String str : file.list()) { if (str.endsWith(".class")) { list.add(parkage + "." + str.replace(".class", "")); } else if (str.indexOf(".") == -1) { list.addAll(getPkgClass(parkage + "." + str)); } } return list; } /** * 获取类的指定方法 * @param c * @param methodName */ @SuppressWarnings({ "unchecked", "rawtypes" }) public Method getMethod(Class c, String methodName) throws NoSuchMethodException, SecurityException { Method method = c.getMethod(methodName, Map.class); return method.isAnnotationPresent(RequestMapping.class) ? method : null; } }
然后要在 EHHttpHandler的 invokController方法中通过反射( java反射 )调用方法,如下:
/** * 调用对应Controller处理业务 */ private ResultInfo invokController(HttpExchange httpExchange) { String path = httpExchange.getRequestURI().getPath(); String classPath = Constants.UrlClassMap.get(path.substring(0, path.lastIndexOf("/") + 1)); if (classPath == null || classPath.length() == 0) { return null; } Class controllerClass = Class.forName(classPath); Controller controller = (Controller) controllerClass.newInstance(); String methodName = path.substring(path.lastIndexOf("/") + 1, path.lastIndexOf(".")); // 通过反射获取对应方法 AnnocationHandler annocationHandler = new AnnocationHandler(); Method method = annocationHandler .getMethod(controllerClass, methodName); Map<String, Object> map = null; // 参数 map = analysisParms(httpExchange); // 设置session HttpSession httpSession = ApplicationContext.getApplicationContext() .getSession(httpExchange); map.put("session", httpSession); return (ResultInfo) method.invoke(controller, new Object[] { map }); }
最后还要在启动时加载配置信息,在 EHServer的 startServer中添加
// 加载注解配置的controller AnnocationHandler annocationHandler = new AnnocationHandler(); try { annocationHandler .paserControllerAnnocation("org.eh.core.web.controller"); } catch (Exception e) { log.error("加载controller配置出错!", e); return; }
至此,此次功能完成。
四、测试
下边我们来测试下该功能是否好用,还是用 IndexController吧,我们加入注解信息:
/** * 主页对应的contoller * @author guojing */ @org.eh.core.annotation.Controller(name = "session", url = "/session/") public class IndexController implements Controller{ @RequestMapping public ResultInfo process(Map<String, Object> map){ ResultInfo result =new ResultInfo(); // 这里我们判断请求中是否有name参数,如果有则放入session,没有则从session中取出name放入map HttpSession session = (HttpSession) map.get("session"); if (map.get("name") != null) { Object name = map.get("name"); session.addAttribute("name", name); } else { Object name = session.getAttribute("name"); if (name != null) { map.put("name", name); } } result.setView("index"); result.setResultMap(map); return result; } }
然后浏览器打开 http://localhost:8899/session/process.do?name=guojing,结果又看到这个熟悉的页面了,说明一切ok,你也可以多写几个controller,直接加入注解就能访问,不用改其他任何代码 。
五、总结
照例最后来点废话,至此其实本项目才真正能拿去用了,不过如果你真打算用,就会发现很多不爽的地方,比如本项目代码和业务代码混在一起、模板支持太差、配置信息写在代码里等等,那么下次我们将来解决这些问题。
最后献上源码,learn-3源码(对应的 master 为完整项目): 源码