JAVA反射+运行时注解实现URL路由
一直想了解一下Spring mvc这套框架,但是每次看完就头大,大量的注解、XML配置让人捉摸不透。
为了减少一些痛苦,我决定简单的学习一下JAVA的反射与注解,实现一个简单的URL路由来验证这些高级功能。
目标
最终我可以写一些Controller类,在里面通过注解配置URL路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package cc.yuerblog.Controller; import cc.yuerblog.Router.RouteMapping; public class IndexController { @RouteMapping(uri = "/login") public void actionLogin() { System.out.println("/login called"); } @RouteMapping(uri = "/logout") public void actionLogout() { System.out.println("/logout called"); } private void someFunc() { } } |
访问/login时,路由应该调用IndexController.actionLogin方法;访问/logout时,路由应该调用IndexController.actionLogout方法。
对于一个强类型语言来说,如何实现这样的动态效果呢?答案就是:注解,反射。
预备知识
我是通过学习这篇博客对注解有了基本的掌握,建议大家也先学习一下:《自己动手实现JAVA注解》。
注解
上面使用的@RouteMapping是一个注解,它声明了action方法对应的URI地址是什么。
注解相当于对方法附加了一些描述信息,后续可以通过反射机制获得注解里的信息,从而获知action对应的uri是/login还是/logout。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package cc.yuerblog.Router; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 运行时可以反射类方法得到注解信息 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RouteMapping { String uri(); // 注解需要声明路由的uri } |
定义注解需要使用@interface语法,Target说明注解可以用于一个类方法还是一个类成员变量。
Retention说明注解在什么阶段可以被访问,这里我的需求是在程序运行时通过反射解析注解里的uri变量,所以配置了RUNTIME。
注册路由
Spring是通过XML配置指定controller类存储的目录,这样Spring框架会在初始化时扫描下面的所有class,解析其中的注解并生成路由表。
为了简化,我这里需要显式的向路由添加controller的完整class名称,避免扫描目录的步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package cc.yuerblog; import cc.yuerblog.Router.Router; public class App { public static void main( String[] args ) { // 注册路由 Router router = new Router("target/classes/"); router.addRouter("cc.yuerblog.Controller.IndexController"); // 测试路由 router.testRoute("/login"); router.testRoute("/logout"); } } |
Router是路由器类,它查找controller的根路径是target/classes,这个目录存放了所有controller编译后的.class文件。
接下来通过addRouter注册一个controller到路由器,Router会根据类名在target/classes目录下找到对应的.class文件进行类加载。
此后,通过testRoute方法进行测试,传入一个URI会触发某个action被调用。
路由器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
public class Router { private class Action { public Action(Object object, Method method) { this.object = object; this.method = method; } public void call() { try { method.invoke(object); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } private Object object; private Method method; } private ControllerLoader controllerLoader; private Map<String, Object> controllerBeans = new HashMap<String, Object>(); private Map<String, Action> uri2Action = new HashMap<String, Action>(); public Router(String basePath) { controllerLoader = new ControllerLoader(basePath); } public void addRouter(String controllerClass) { try { // 加载class Class<?> cls = controllerLoader.loadClass(controllerClass); // 反射class中所有方法 Method[] methods = cls.getDeclaredMethods(); for (Method method : methods) { // 反射方法所有注解 Annotation[] annotations = method.getAnnotations(); for (Annotation annotation : annotations) { // 如果注解类型是RouteMapping, 解析其URI if (annotation.annotationType() == RouteMapping.class) { RouteMapping anno = (RouteMapping)annotation; // 路由uri String uri = anno.uri(); // 保存Bean单例 if (!controllerBeans.containsKey(cls.getName())) { controllerBeans.put(cls.getName(), cls.newInstance()); } // 保存uri -> (obj,method) uri2Action.put(uri, new Action(controllerBeans.get(cls.getName()), method)); } } } } catch (Exception e) { e.printStackTrace(); } } public void testRoute(String uri) { Action action = uri2Action.get(uri); if (action != null) { action.call(); } else { System.out.println(uri + " is not found"); } } } |
addRouter首先调用controllerLoader对象从磁盘上的.class文件加载对应的controller类。
这里用到了JAVA的另外一个重要概念就是class loader,我是通过这篇博客学习的:《深入探讨JAVA类加载器》。
无论如何,在这里IndexController类在运行时从.class文件被加载到运行时环境中,也就是得到了对应的class对象。
通过class对象可以进行反射,得到这个类的方法,再解析方法上的注解。
如果注解对象的class类型等于RouteMapping.class,就获取注解对象的uri属性,也就是之前@RouteMapping(uri=…)配置的属性。
通过controllerBeans维护controller单例对象,通过uri2Action维护uri -> action的关联关系。
Action
所谓action就是一个接口,它使用单例controller对象作为调用主体,通过method反射可以完成方法的调起:
1 |
method.invoke(object) |
相当于object.method(),应该不难理解
最后,testAction方法根据uri在uri2Action中找到对应的Action对象,通过action.call()唤起对应的处理方法。
class loader
实现自己的class loader,只需要覆写findClass方法,在该方法中将对应的类文件读取进来,通过defineClass方法完成加载,最后返回即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package cc.yuerblog.Router; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; // 为了加载.class文件中的类 public class ControllerLoader extends ClassLoader { private String basePath; public ControllerLoader(String basePath) { this.basePath = basePath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 找到.class文件 String path = basePath + name.replaceAll("\\.", "/"); // 读取.class文件 byte[] classBytes; InputStream ins = null; try { ins = new FileInputStream(path); ByteArrayOutputStream bout = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { bout.write(buffer, 0, bytesNumRead); } classBytes = bout.toByteArray(); } catch (Exception e) { throw new ClassNotFoundException(); } finally { try { if (ins != null) { ins.close(); } } catch (Exception e) { throw new ClassNotFoundException(); } } // 生成class类 return defineClass(name, classBytes, 0, classBytes.length); } } |
最后
项目地址:https://github.com/owenliang/annotation。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

3 thoughts on “JAVA反射+运行时注解实现URL路由”