java的springMvc框架简单实现

前言
在本文中,博主一步步地从servlet到controller层实现一个简单的框架。通过此框架,我们可以像spring那样使用以下基础注解:

  • @XxgController
  • @XxgRequestMapping
  • @XxgParam
  • @XxgRequestBody
观看本文之前,你或许应该先了解以下内容:
  • BeanUtils
  • ObjectMapper
  • Servlet相关知识
思路:拦截器实现路由分发。利用注解?
思考:
  1. 拦截器可以在servlet之前拦截所有请求路径
  2. 可以找到注解中路径与请求路径相匹配的那个方法
  3. 然后将req,resp转发给该方法来执行
问题:
拦截器如何找到使用了该注解的方法?包扫描?如何实现?
分析:
包扫描,就涉及IO流, 而File类可以递归查询其下面所有的文件,我们
可以过滤一下:
  1. 只要后缀名为.class的文件,并获取其className(包括包路径)
  2. 通过反射获取这个类,判断其是否有指定的注解进而再次过滤
这样在拦截器拦截到请求路径,我们可以进行匹配并调用该方法。
偷个懒:
因为MVC设计模式,我们一般把api接口都放在同一个包下,所以我们可以直接指定要扫描包,其它包就不管
一.扫描类1.0版的实现
public class FileScanner { private final String packetUrl = "com.dbc.review.controller"; private final ClassLoader classLoader = FileScanner.class.getClassLoader(); private List allClazz = new ArrayList<>(10); //存该包下所有用了注解的类 ? public List getAllClazz(){ return this.allClazz; } ? public String getPacketUrl(){ return this.packetUrl; } ? // 查询所有使用了给定注解的类 // 递归扫描包,如果扫描到class,则调用class处理方法来收集想要的class public void loadAllClass(String packetUrl) throws Exception{ String url = packetUrl.replace(".","/"); URL resource = classLoader.getResource(url); if (resource == null) { return; } String path = resource.getPath(); File file = new File(URLDecoder.decode(path, "UTF-8")); if (!file.exists()) { return; } if (file.isDirectory()){ File[] files = file.listFiles(); if (files == null) { return; } for (File f : files) { String classname = f.getName().substring(0, f.getName().lastIndexOf(".")); if (f.isDirectory()) { loadAllClass(packetUrl + "." + classname); } if (f.isFile() && f.getName().endsWith(".class")) { Class clazz = Class.forName(packetUrl + "." + classname); dealClass( clazz); } } } } ? private void dealClass(Class clazz) { if ((clazz.isAnnotationPresent(Controller.class))) { allClazz.add(clazz); } } ? // 真正使用的时候,根据请求路径及请求方法来获取处理的方法 public boolean invoke(String url,String requestMethod) { for (Class clazz : allClazz){ Controller controller = (Controller) clazz.getAnnotation(Controller.class); Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { RequestMapping requestMapping = method.getDeclaredAnnotation(RequestMapping.class); if (requestMapping == null) { continue; } for (String m : requestMapping.methods()) { m = m.toUpperCase(); if (!m.toUpperCase().equals(requestMethod.toUpperCase())) { continue; } StringBuilder sb = new StringBuilder(); String urlItem = sb.append(controller.url()).append(requestMapping.url()).toString(); if (urlItem.equals(url)) { // 获取到用于处理此api接口的方法 try { //method.getGenericParameterTypes() // 可以根据此方法来判断该方法需要传哪些参数 method.invoke(clazz.newInstance()); return true; } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); return false; } } } } } return false; } ? @Test public void test() throws Exception { // 1. 在Filter的静态代码块中实例化 FileScanner fileScanner = new FileScanner(); // 2. 启动扫描 fileScanner.loadAllClass(fileScanner.getPacketUrl()); // 3. 拦截到请求后,调用此方法来执行 // 若该包下没有定义post请求的/test/post 的处理方法,则返回false // 执行成功返回true fileScanner.invoke("/test/post","post"); // 4. 执行失败,返回false,则抛出405 方法未定义。 ? // 最后 :对于controller的传参,本类未实现 //暂时想到:根据method获取其参数列表,再传对应参数,就是不太好实现 } }

TestController
@Controller(url = "/test") public class TestController { ? @RequestMapping(url = "/get",methods = "GET") public void get(){ System.out.println(111); } @RequestMapping(url = "/post",methods = {"POST","get"}) public void post(){ System.out.println(22); } ? public void test(HttpServletRequest req, HttpServletResponse res){ System.out.println(req.getPathInfo()); } }

扫描类2.0版
通过1.0版,我们初步实现递归扫描包下的所有controller,并能通过路径映射实现访问。但很明显有至少以下问题:
  1. 执行方法时,方法不能有参数。不符合业务需求
  2. 每次访问,都要反复处理Class反射来找到路径映射的方法,效率低。
针对以上2个问题,我们在2.0版进行一下修改:
  1. 将controller、requestmapping对应方法,方法对应参数的可能用到的相关信息存放在一个容器中。在服务器初次启动时进行扫描,并装配到容器中。这样在每次访问时,遍历这个容器,比1.0版的容器更方便。
  2. 定义参数类型,通过注解@XxgRequestBody以及@XxgParam区分参数从请求体拿或者从url的?后面拿。从而获取前端传来的数据
  3. 通过ObjectMapper进行不同类型参数的装配,最后调用方法的invoke实现带参/不带参的方法处理。
BeanDefinition
/** * 用来存放controller类的相关参数、方法等 */ @Data @NoArgsConstructor @AllArgsConstructor public class BeanDefinition { private Class typeClazz; // 类对象 private String typeName; // 类名 private Object annotation; // 注解 private String controllerUrlPath; // controller的path路径 private List methodDefinitions; // 带有RequestMapping的注解 }

MethodDefinition
/** * 描述方法的类 */ @Data @NoArgsConstructor @AllArgsConstructor public class MethodDefinition { private Class parentClazz; // 所属父类的class private Method method; // 方法 private String methodName; // 方法名 private Object annotation; // 注解类 private String requestMappingUrlPath; // url private String[] allowedRequestMethods; // allowedRequestMethods private List parameterDefinitions; // 参数列表 private Object result; // 返回数据 }

ParameterDefinition
/** * 描述参数的类 */ @Data @NoArgsConstructor @AllArgsConstructor public class ParameterDefinition { private Class paramClazz; // 参数类对象 private String paramName; // 参数名称 private Object paramType; // 参数类型 private boolean isRequestBody; // 是否是获取body中数据 }

单例模式的容器 赋予扫描包及根据uri获取对应方法的方法
/** * 用于存放请求路径 与 controller对应关系的类 * 设计成单例模型 */ public class RequestPathContainer { private static List requestList = new ArrayList<>(); private static final ClassLoader classLoader = RequestPathContainer.class.getClassLoader(); private static volatile RequestPathContainer instance = null; ? public static RequestPathContainer getInstance() { if (instance == null) { synchronized(RequestPathContainer.class){ if (instance == null) { instance = new RequestPathContainer(); } } } return instance; } ? private RequestPathContainer() { ? } ? public List getRequestList() { return requestList; } ? // 扫描包 public void scanner(String packetUrl) throws UnsupportedEncodingException, ClassNotFoundException { String url = packetUrl.replace(".", "/"); URL resource = classLoader.getResource(url); if (resource == null) { return; } String path = resource.getPath(); File file = new File(URLDecoder.decode(path, "UTF-8")); if (!file.exists()) { return; } if (file.isDirectory()){ File[] files = file.listFiles(); if (files == null) { return; } for (File f : files) { if (f.isDirectory()) { scanner(packetUrl + "." + f.getName()); } if (f.isFile() && f.getName().endsWith(".class")) { String classname = f.getName().replace(".class", ""); // 去掉.class后缀名 Class clazz = Class.forName(packetUrl + "." + classname); dealClass(clazz); } } } } ? // 筛选包中的类,并添加到List中 private void dealClass(Class clazz) { if (!clazz.isAnnotationPresent(XxgController.class)) { // 没有controller注解 return; } List methodDefinitions = new ArrayList<>(); Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { // 方法转 方法描述类 MethodDefinition methodDefinition = convertMethodToMethodDefinition(method, clazz); if (methodDefinition != null) { methodDefinitions.add(methodDefinition); } } if (methodDefinitions.size() == 0) { return; } // 设置类描述类 BeanDefinition beanDefinition = convertBeanToBeanDefinition(clazz, methodDefinitions); requestList.add(beanDefinition); } ? // 根据uri 和 请求方法 获取执行方法 public MethodDefinition getMethodDefinition(String uri, String method) { for (BeanDefinition beanDefinition: requestList) { if (!uri.contains(beanDefinition.getControllerUrlPath())) { continue; } List methodDefinitions = beanDefinition.getMethodDefinitions(); for (MethodDefinition methodDefinition: methodDefinitions) { StringBuilder sb = new StringBuilder().append(beanDefinition.getControllerUrlPath()); sb.append(methodDefinition.getRequestMappingUrlPath()); if (!sb.toString().equals(uri)) { continue; } String[] allowedRequestMethods = methodDefinition.getAllowedRequestMethods(); for (String str : allowedRequestMethods) { if (str.toUpperCase().equals(method.toUpperCase())) { // 请求路径 与 请求方法 均满足,返回该方法描述类 return methodDefinition; } } } } return null; } ? /** * 将controller类 转换为 类的描述类 */ private BeanDefinition convertBeanToBeanDefinition(Class clazz, List methodDefinitions) { BeanDefinition beanDefinition = new BeanDefinition(); beanDefinition.setTypeName(clazz.getName()); beanDefinition.setTypeClazz(clazz); XxgController controller = (XxgController) clazz.getAnnotation(XxgController.class); beanDefinition.setAnnotation(controller); beanDefinition.setControllerUrlPath(controller.value()); beanDefinition.setMethodDefinitions(methodDefinitions); // 增加方法体 return beanDefinition; } ? /** * 将方法 转换为 方法描述类 */ private MethodDefinition convertMethodToMethodDefinition(Method method, Class clazz) { if (!method.isAnnotationPresent(XxgRequestMapping.class)) { // 没有RequestMapping注解 return null; } method.setAccessible(true); Parameter[] parameters = method.getParameters(); // 设置参数描述类 List parameterDefinitions = new ArrayList<>(); for ( Parameter parameter : parameters) { ParameterDefinition parameterDefinition = convertParamToParameterDefinition(parameter); parameterDefinitions.add(parameterDefinition); } // 设置方法描述类 MethodDefinition methodDefinition = new MethodDefinition(); methodDefinition.setParameterDefinitions(parameterDefinitions); // 增加参数列表 methodDefinition.setMethod(method); methodDefinition.setMethodName(method.getName()); methodDefinition.setResult(method.getReturnType()); XxgRequestMapping requestMapping = method.getAnnotation(XxgRequestMapping.class); methodDefinition.setRequestMappingUrlPath(requestMapping.value()); methodDefinition.setAnnotation(requestMapping); methodDefinition.setAllowedRequestMethods(requestMapping.methods()); methodDefinition.setParentClazz(clazz); return methodDefinition; } ? /** * 将参数 转换为 参数描述类 */ private ParameterDefinition convertParamToParameterDefinition(Parameter parameter) { ParameterDefinition parameterDefinition = new ParameterDefinition(); if ( parameter.isAnnotationPresent(XxgParam.class)) { parameterDefinition.setParamName(parameter.getAnnotation(XxgParam.class).value()); } else { parameterDefinition.setParamName(parameter.getName()); } parameterDefinition.setParamClazz(parameter.getType()); parameterDefinition.setParamType(parameter.getType()); parameterDefinition.setRequestBody(parameter.isAnnotationPresent(XxgRequestBody.class)); return parameterDefinition; } ? }

全局servlet 不使用拦截器,仍然使用servlet来进行路由分发。此servlet监听/
public class DispatcherServlet extends HttpServlet { private ObjectMapper objectMapper = new ObjectMapper(); ? @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ? // 编码设置 resp.setContentType("text/json; charset=utf-8"); RequestPathContainer requestPathContainer = RequestPathContainer.getInstance(); MethodDefinition methodDefinition = requestPathContainer.getMethodDefinition(req.getRequestURI(), req.getMethod()); ? if (methodDefinition == null) { resp.setStatus(404); sendResponse(R.failed("请求路径不存在"), req, resp); return; } ? List parameterDefinitions = methodDefinition.getParameterDefinitions(); List params = new ArrayList<>(parameterDefinitions.size()); for (ParameterDefinition parameterDefinition : parameterDefinitions) { try { Object value = https://www.it610.com/article/dealParam(parameterDefinition, req, resp); params.add(value); } catch (ParamException e) { resp.setStatus(404); sendResponse(R.failed(e.getMessage()), req, resp); return ; } } ? try { Object result = methodDefinition.getMethod().invoke(methodDefinition.getParentClazz().newInstance(), params.toArray()); sendResponse(result, req, resp); } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) { e.printStackTrace(); sendResponse(e.getMessage(), req, resp); } ? } ? /** * 处理参数 * @param parameterDefinition * @param req * @param resp */ private Object dealParam(ParameterDefinition parameterDefinition, HttpServletRequest req, HttpServletResponse resp) throws ParamException, IOException { Object value; String data =""; if (parameterDefinition.isRequestBody()) { // 从请求体(request的输入流)中获取数据 data = https://www.it610.com/article/getJsonString(req); } else if (parameterDefinition.getParamType() == HttpServletRequest.class) { return req; } else if (parameterDefinition.getParamType() == HttpServletResponse.class) { return resp; } else if (isJavaType(parameterDefinition)) { // 从url中取出参数 data = req.getParameter(parameterDefinition.getParamName()); if(data == null) { throw new ParamException("服务器无法拿到请求数据,请检查请求头等"); } } else { // 将请求url中的参数封装成对象 try { Object obj = parameterDefinition.getParamClazz().newInstance(); ConvertUtils.register(new DateConverter(), Date.class); BeanUtils.populate(obj, req.getParameterMap()); return obj; } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new ParamException("未找到参数'" + parameterDefinition.getParamName() + "'对应的值"); } } try { value = https://www.it610.com/article/objectMapper.readValue(data, parameterDefinition.getParamClazz()); } catch (JsonProcessingException e) { String errMsg ="参数'" + parameterDefinition.getParamName() + "'需要'" + parameterDefinition.getParamType() + "类型"; throw new ParamException(errMsg); } return value; } ? private void sendResponse(Object result, HttpServletRequest req, HttpServletResponse resp) throws IOException { if (result == null) { return; } resp.setContentType("text/json; charset=utf-8"); objectMapper.writeValue(resp.getWriter(), result); } ? /** * 判断参数是否是普通类型 * @return */ private boolean isJavaType(ParameterDefinition parameterDefinition) { Object[] javaTypes = MyJavaType.getJavaTypes(); for (Object item : javaTypes) { if (item.equals(parameterDefinition.getParamClazz())) { return true; } } return false; } ? /** * 获取请求头的json字符串 */ private String getJsonString(HttpServletRequest req) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(req.getInputStream(), "utf-8")); char[] chars = new char[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = br.read(chars)) != -1) { sb.append(chars, 0, len); } return sb.toString(); } }
servletcontext监听器初始化容器
@Override public void contextInitialized(ServletContextEvent servletContextEvent) { RequestPathContainer requestPathContainer = RequestPathContainer.getInstance(); String configClassName = servletContextEvent.getServletContext().getInitParameter("config"); Class appListenerClass = null; try { appListenerClass = Class.forName(configClassName); XxgScanner xxgScanner = (XxgScanner)appListenerClass.getAnnotation(XxgScanner.class); if (xxgScanner != null) { try { requestPathContainer.scanner(xxgScanner.value()); // 扫描controller类,初始化List } catch (UnsupportedEncodingException | ClassNotFoundException e) { e.printStackTrace(); } } } catch (ClassNotFoundException e) { e.printStackTrace(); } }

遗留的问题 静态资源也被拦截了
处理静态资源
default servlet 打开tomcat的conf/web.xml文件,可以发现tomcat默认有个default servlet,有如下配置:
default org.apache.catalina.servlets.DefaultServlet debug 0 listings false 1

但是他并没有匹配servlet-mapping,即处理的路径,那么可以在我们项目的web.xml中做以下配置来处理静态资源:
DispatcherServlet / default *.html default *.js default *.css default *.jpg

最后 一.本文其实主要做了以下两个操作
  1. 服务器启动时,扫描controller包,将符合我们预期的类、方法、参数装配到容器中。
  2. 前端访问服务器,获取容器中指定路径对应的方法
    2.1 将访问参数按不同类型装配到参数列表中
    2.2 执行对应方法
    2.3 处理方法返回数据
二.参考说明
  • 项目实现过程
1.0版是博主自己思考并完成的。
2.0版是博主的小高老师给博主讲了思路,写出来后又看了小高老师的实现,然后综合着完善的。
  • 在写了文章后,博主对项目中不同类进行了解耦等操作,代码重构了一番,主要为了应付开闭原则、单一职责原则等。
  • 代码:https://github.com/dengbenche...
传送门 【java的springMvc框架简单实现】下一节:AutoWired属性上使用的简单实现

    推荐阅读