使用Java元注解和反射实现简单MVC框架
Springmvc的核心是DispatcherServlet来进行各种请求的拦截,进而进行后续的各种转发处理。流程图如下:
说明:客户端发出一个http请求给web服务器,web服务器对http请求进行解析,如果匹配DispatcherServlet的请求映射路径(在web.xml中指定),web容器将请求转交给DispatcherServlet.DipatcherServlet接收到这个请求之后将根据请求的信息(包括URL、Http方法、请求报文头和请求参数Cookie等)以及HandlerMapping的配置找到处理请求的处理器(Handler)。DispatcherServlet根据HandlerMapping找到对应的Handler,将处理权交给Handler(Handler将具体的处理进行封装),再由具体的HandlerAdapter对Handler进行具体的调用。Handler对数据处理完成以后将返回一个ModelAndView()对象给DispatcherServlet。Handler返回的ModelAndView()只是一个逻辑视图并不是一个正式的视图,DispatcherSevlet通过 ViewResolver将逻辑视图转化为真正的视图View。Dispatcher通过model解析出ModelAndView()中的参数进行解析最终展现出完整的view并返回给客户端。
以下基于java元注解和反射等知识来实现简单MVC框架。
1、Servlet
Servlet 3.0 之前使用web.xml文件进行配置,例如:
<servlet> <serlvet-name>myServlet</servlet-name> <servlet-calss>MyServlet的类路径</servlet-class> </servlet> <servlet-mapping> <serlvet-name>myServlet</servlet-name> <url-pattern>/servlet/myServlet</url-pattern> </servlet-mapping>
Servlet 3.0 后可以基于注解来处理Servlet
@WebServlet(name = "dispatcherServlet", urlPatterns = "/*", loadOnStartup = 1,
initParams = {@WebInitParam(name = "base-package", value = "com.kinson.myspring")})
name:servlet名
urlPatterns:url匹配模式
loadOnStartup:标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法),它的值必须是一个整数,表示servlet应该被载入的顺序.
- 当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet
- 当值小于0或者没有指定时,则表示容器在该servlet被选择时才会去加载
- 正数的值越小,该servlet的优先级越高,应用启动时就越先加载
- 当值相同时,容器就会自己选择顺序来加载
initParams:初始化参数,此处表示定义了一个名为base-package,值为com.kinson.myspring的WebInitParam对象,可以通过ServletConfig的getInitParameter("base-package");方法获取对应的值。
2、实现
2.1 工程目录结构
相关代码说明:
-
在 annotation 包下,我将提供自定义的注解,为了方便理解,会与 Spring MVC 保持一致。JDK 元注解介绍
-
为了模拟 Spring MVC 的方法调用链,我这里提供 Controller/Service/Dao 层进行测试。
-
提供自定义的 DispatcherServlet 来完成核心逻辑处理。
具体代码实现:
2.2 pom引入servlet依赖
<!--项目依赖--> <dependencies> <!-- servlet 依赖--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.version}</version> </dependency> </dependencies>
2.3 Annotation
以Controller为例,其他的注解类似:
//用于类、接口、枚举enum @Target(ElementType.TYPE) //生命周期为运行时 @Retention(RetentionPolicy.RUNTIME) //javadoc @Documented public @interface Controller { /** * 作用于该注解的一个value属性 * @return */ String value(); }
2.4 请求拦截类DispatcherServlet :
定义相关全局存储变量:
// @WebServlet 以前我们定义一个 Servlet ,需要在 web.xml 中去配置,不过在 Servlet 3.0 后出现了基于注解的 Servlet 。 @WebServlet(name = "dispatcherServlet", urlPatterns = "/*", loadOnStartup = 1, initParams = {@WebInitParam(name = "base-package", value = "com.kinson.myspring")}) public class DispatcherServlet extends HttpServlet { /** * 扫描的包 */ private String basePackage = ""; /** * 基包下面所有的带包路径权限定类名 */ private List<String> packageNames = new ArrayList<String>(); /** * 注解实例化 格式为注解上的名称:注解实例化对象 */ private Map<String, Object> instanceMap = new HashMap<String, Object>(); /** * 包路径权限定类名称:注解上的名称 */ private Map<String, String> nameMap = new HashMap<String, String>(); /** * Url地址和方法的映射关系:注解上的名称 */ private Map<String, Method> urlMethodMap = new HashMap<String, Method>(); /** * Method和权限定类名的映射关系,用于通过Method找到该方法的对象利用反射执行 */ private Map<Method, String> methodPackageMap = new HashMap<Method, String>();
}
初始化方法init:
/** * 初始化 * 1、扫描基包下的类,得到信息 A。 * 2、对于 @Controller/@Service/@Repository 注解而言,我们需要拿到对应的名称,并初始化它们修饰的类,形成映射关系 B。 * 3、扫描类中的字段,如果发现有 @Qualifier 的话,我们需要完成注入。 * 4、扫描 @RequestMapping,完成 URL 到某一个 Controller 的某一个方法上的映射关系 C。 * * @param config */ @Override public void init(ServletConfig config) { System.out.println("开始初始化。。。。。。"); //通过初始化参数直接将需要扫描的基包路径传入 basePackage = config.getInitParameter("base-package"); try { //扫描基包得到全部的带包路径权限定类名 scanBasePackage(basePackage); //把代用注解的类实例化方如Map中,key为注解上的名称 instance(packageNames); //IOC注入 springIOC(); //完成Url地址与方法的映射关系 handleUrlMethodMap(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } System.out.println("初始化结束。。。。。。"); }
方法scanBasePackage通过初始化参数直接将需要扫描的基包路径传入:
/** * 通过初始化参数直接将需要扫描的基包路径传入 * * @param basePackage 基包路径 */ private void scanBasePackage(String basePackage) { //加载类资源路径 URL url = this.getClass().getClassLoader() .getResource(basePackage.replaceAll("\\.", "/")); File basePackageFile = new File(url.getPath()); System.out.println("scan:" + basePackageFile); File[] childFiles = basePackageFile.listFiles(); for (File file : childFiles) { //目录递归扫描 if (file.isDirectory()) { scanBasePackage(basePackage + "." + file.getName()); } else if (file.isFile()) { //Controller.class====Controller,即去掉.class System.out.println(">>>>>>>>>>> " + file.getName() + "====" + file.getName().split("\\.")[0]); packageNames.add(basePackage + "." + file.getName().split("\\.")[0]); } } }
方法instance把代用注解的类实例化方如Map中,key为注解上的名称:
/** * 把代用注解的类实例化方如Map中,key为注解上的名称 * * @param packageNames 包路径名集合 */ private void instance(List<String> packageNames) throws ClassNotFoundException, InstantiationException, IllegalAccessException { if (packageNames.size() < 1) { return; } for (String packageName : packageNames) { //根据包路径获取Class对象 Class<?> clazz = Class.forName(packageName); //Controller注解处理 if (clazz.isAnnotationPresent(Controller.class)) { Controller controller = (Controller) clazz.getAnnotation(Controller.class); String controllerName = controller.value(); instanceMap.put(controllerName, clazz.newInstance()); nameMap.put(packageName, controllerName); System.out.println("Controller :" + packageName + ", value :" + controllerName); } else if (clazz.isAnnotationPresent(Service.class)) { //Service注解处理 Service service = (Service) clazz.getAnnotation(Service.class); String serviceName = service.value(); instanceMap.put(serviceName, clazz.newInstance()); nameMap.put(packageName, serviceName); System.out.println("Service :" + packageName + ", value :" + serviceName); } else if (clazz.isAnnotationPresent(Repository.class)) { //Repository注解处理 Repository repository = clazz.getAnnotation(Repository.class); String repositoryName = repository.value(); instanceMap.put(repositoryName, clazz.newInstance()); nameMap.put(packageName, repositoryName); System.out.println("Repository :" + packageName + ", value :" + repositoryName); } } }
方法springIOC注入:
/** * IOC注入 */ private void springIOC() throws IllegalAccessException { for (Map.Entry<String, Object> instanceEntry : instanceMap.entrySet()) { //获取当前对象的所有字段 Field[] declaredFields = instanceEntry.getValue().getClass().getDeclaredFields(); for (Field field : declaredFields) { //字段上是否有Qualifier注解 if (field.isAnnotationPresent(Qualifier.class)) { Qualifier qualifier = field.getAnnotation(Qualifier.class); String qualifierName = qualifier.value(); //设置当前的字段为可访问 field.setAccessible(Boolean.TRUE); //设置当前字段 field.set(instanceEntry.getValue(), instanceMap.get(qualifierName)); System.out.println("==========" + field); } } } }
方法handleUrlMethodMap处理Url地址与方法的映射关系:
/** * Url地址与方法的映射关系 * * @throws ClassNotFoundException */ private void handleUrlMethodMap() throws ClassNotFoundException { if (packageNames.size() < 1) { return; } for (String packageName : packageNames) { //根据包路径获取Class对象 Class clazz = Class.forName(packageName); //当前类是否有Controller注解 if (clazz.isAnnotationPresent(Controller.class)) { //获取当前Controller类的所有方法 Method[] methods = clazz.getMethods(); //拼接访问URI StringBuffer baseUrl = new StringBuffer(); //当前Controller是否有RequestMapping注解 if (clazz.isAnnotationPresent(RequestMapping.class)) { RequestMapping requestMapping = (RequestMapping) clazz.getAnnotation(RequestMapping.class); //XxxController类上的requestMapping值 baseUrl.append(requestMapping.value()); } for (Method method : methods) { if (method.isAnnotationPresent(RequestMapping.class)) { RequestMapping requestMapping = method.getAnnotation(RequestMapping.class); //XxxController类上的响应方法上的requestMapping值 baseUrl.append(requestMapping.value()); //URL 提取出来,映射到 Controller 的 Method 上。 System.out.println("baseUrl : " + baseUrl.toString()); urlMethodMap.put(baseUrl.toString(), method); methodPackageMap.put(method, packageName); } } } } }
doGet/doPost方法处理拦截请求的业务逻辑:
@Override public void doGet(HttpServletRequest req, HttpServletResponse resp) { doPost(req, resp); } @Override public void doPost(HttpServletRequest req, HttpServletResponse resp) { //获取请求URI,eg:/user/hello final String uri = req.getRequestURI(); final String contextPath = req.getContextPath(); final String path = uri.replaceAll(contextPath, ""); //提取出 URL,通过 URL 映射到Method 上,然后通过反射的方式进行调用即可。 Method method = urlMethodMap.get(path); if (null != method) { //通过方法获取方法所在的包路径 String packageName = methodPackageMap.get(method); //通过包路径获取注解上的名称 String controllerName = nameMap.get(packageName); //通过注解名称获取对应的实例对象 UserController userController = (UserController) instanceMap.get(controllerName); try { //设置方法可访问 method.setAccessible(Boolean.TRUE); //利用反射进行方法调用 method.invoke(userController); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
测试UserController类:
@Controller("userController") @RequestMapping("/user") public class UserController { @Qualifier("userServiceImpl") private UserService userService; @RequestMapping(value = "/hello") public String hello() { System.out.println("UserController.hello"); return "UserController.hello"; } }
测试UserService接口:
public interface UserService { void hello(); }
测试UserServiceImpl接口:
@Service("userServiceImpl") public class UserServiceImpl implements UserService { @Override public void hello() { System.out.println("hello, myspring"); } }
3、测试
配置tomcat运行项目,此处我用的是idea,具体配置:
选择本地安装的tomcat:
设置相关内容:
点击右下角的Fix按钮选择部署包:
配置好之后运行,在浏览器输入测试url:
idea控制台打印了内容:
到此,一个简单的MVC框架就ok了。
Github源码参照