前言
前面幾篇文章中,我們講解了Spring MVC執⾏的⼤致原理及關鍵元件的原始碼解析,今天,我們來模仿它⼿寫⾃⼰的mvc框架。
先梳理一下需要實現的功能點:
- tomcat載入配置檔案web.xml;
- 呼叫web.xml中指定的前端控制器DispatcherServlet載入指定的配置檔案(一般為springmvc.xml,本文中的為springmvc.properties);
- 掃描相關的類,掃描註解(@Controller,@Service,@RequestMapping,@Autowired);
- IOC容器進行相應Bean初始化以及依賴注入維護;
- Spring MVC相關元件的初始化,建立url與method之間的對映關係——HandlerMapping(處理器對映器);
- 等待請求進來,處理請求。
實現過程
閒話少說,直接來看程式碼。
1、註解開發
HardyController:
package com.hardy.edu.mvcframework.annotations; import java.lang.annotation.*; @Documented // 表明這個註解應該被 javadoc 工具記錄 @Target(ElementType.TYPE) // 用於設定註解使用範圍,ElementType.TYPE標明該註解可用於類或者介面上,用於描述類、介面(包括註解型別) 或enum宣告 @Retention(RetentionPolicy.RUNTIME) // 指定生存週期,執行時有效 public @interface HardyController { String value() default ""; }
HardyService:
package com.hardy.edu.mvcframework.annotations; import java.lang.annotation.*; @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface HardyService { String value() default ""; }
HardyRequestMapping:
package com.hardy.edu.mvcframework.annotations; import java.lang.annotation.*; @Documented @Target({ElementType.TYPE,ElementType.METHOD}) // 可用於類、介面或方法上 @Retention(RetentionPolicy.RUNTIME) public @interface HardyRequestMapping { String value() default ""; }
HardyAutowired:
package com.hardy.edu.mvcframework.annotations; import java.lang.annotation.*; @Documented @Target(ElementType.FIELD) // 可用於域上,用於描述域 @Retention(RetentionPolicy.RUNTIME) public @interface HardyAutowired { String value() default ""; }
2、Pojo類Handler
package com.hardy.edu.mvcframework.pojo; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; /** * @author HardyYao * @date 2021/5/13 * @description 封裝handler方法相關的資訊 */ public class Handler { private Object controller; // method.invoke(obj,) private Method method; private Pattern pattern; // spring中url是支援正則的 private Map<String,Integer> paramIndexMapping; // 引數順序,是為了進行引數繫結,key是引數名,value代表是第幾個引數 此次測試中我們傳遞的引數那麼的map為:<name,2> public Handler(Object controller, Method method, Pattern pattern) { this.controller = controller; this.method = method; this.pattern = pattern; this.paramIndexMapping = new HashMap<>(); } public Object getController() { return controller; } public void setController(Object controller) { this.controller = controller; } public Method getMethod() { return method; } public void setMethod(Method method) { this.method = method; } public Pattern getPattern() { return pattern; } public void setPattern(Pattern pattern) { this.pattern = pattern; } public Map<String, Integer> getParamIndexMapping() { return paramIndexMapping; } public void setParamIndexMapping(Map<String, Integer> paramIndexMapping) { this.paramIndexMapping = paramIndexMapping; } }
3、前端控制器實現
package com.hardy.edu.mvcframework.servlet; import com.hardy.edu.mvcframework.annotations.HardyAutowired; import com.hardy.edu.mvcframework.annotations.HardyController; import com.hardy.edu.mvcframework.annotations.HardyRequestMapping; import com.hardy.edu.mvcframework.annotations.HardyService; import com.hardy.edu.mvcframework.pojo.Handler; import org.apache.commons.lang3.StringUtils; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author HardyYao * @date 2021/5/13 * @description 前端控制器 */ public class HardyDispatcherServlet extends HttpServlet { private Properties properties = new Properties(); // 快取掃描到的類的全限定類名 private List<String> classNames = new ArrayList<>(); // ioc容器 private Map<String,Object> ioc = new HashMap<>(); // handlerMapping private List<Handler> handlerMapping = new ArrayList<>(); @Override public void init(ServletConfig config) throws ServletException { // 1 載入配置檔案 springmvc.properties String contextConfigLocation = config.getInitParameter("contextConfigLocation"); doLoadConfig(contextConfigLocation); // 2 掃描相關的類,掃描註解 doScan(properties.getProperty("scanPackage")); // 3 初始化bean物件(實現ioc容器,基於註解) doInstance(); // 4 實現依賴注入 doAutoWired(); // 5 構造一個HandlerMapping處理器對映器,將配置好的url和Method建立對映關係 initHandlerMapping(); System.out.println("hardy mvc 初始化完成...."); // 6 等待請求進入,處理請求 } /** * 構造一個HandlerMapping處理器對映器 * 這是最關鍵的環節 * 目的:將url和method建立關聯 */ private void initHandlerMapping() { if (ioc.isEmpty()) { return ; } for(Map.Entry<String,Object> entry: ioc.entrySet()) { // 獲取ioc中當前遍歷的物件的class型別 Class<?> aClass = entry.getValue().getClass(); if(!aClass.isAnnotationPresent(HardyController.class)) {continue;} String baseUrl = ""; if(aClass.isAnnotationPresent(HardyRequestMapping.class)) { HardyRequestMapping annotation = aClass.getAnnotation(HardyRequestMapping.class); baseUrl = annotation.value(); // 等同於/demo } // 獲取方法 Method[] methods = aClass.getMethods(); for (int i = 0; i < methods.length; i++) { Method method = methods[i]; // 方法沒有標識HardyRequestMapping,就不處理 if(!method.isAnnotationPresent(HardyRequestMapping.class)) {continue;} // 如果標識,就處理 HardyRequestMapping annotation = method.getAnnotation(HardyRequestMapping.class); String methodUrl = annotation.value(); // /query String url = baseUrl + methodUrl; // 計算出來的url /demo/query // 把method所有資訊及url封裝為一個Handler Handler handler = new Handler(entry.getValue(),method, Pattern.compile(url)); // 計算方法的引數位置資訊 // query(HttpServletRequest request, HttpServletResponse response,String name) Parameter[] parameters = method.getParameters(); for (int j = 0; j < parameters.length; j++) { Parameter parameter = parameters[j]; if(parameter.getType() == HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) { // 如果是request和response物件,那麼引數名稱寫HttpServletRequest和HttpServletResponse handler.getParamIndexMapping().put(parameter.getType().getSimpleName(),j); }else{ handler.getParamIndexMapping().put(parameter.getName(),j); // <name,2> } } // 建立url和method之間的對映關係(map快取起來) handlerMapping.add(handler); } } } /** * 實現依賴注入 */ private void doAutoWired() { // 如果物件為空,則直接返回 if (ioc.isEmpty()) { return ; } // 有物件,則進行依賴注入處理 // 遍歷ioc中所有物件,檢視物件中的欄位,是否有@HardyAutowired註解,如果有需要維護依賴注入關係 for (Map.Entry<String,Object> entry: ioc.entrySet()) { // 獲取bean物件中的欄位資訊 Field[] declaredFields = entry.getValue().getClass().getDeclaredFields(); // 遍歷判斷處理 for (int i = 0; i < declaredFields.length; i++) { Field declaredField = declaredFields[i]; // @HardyAutowired private IDemoService demoService; if (!declaredField.isAnnotationPresent(HardyAutowired.class)) { continue; } // 有該註解 HardyAutowired annotation = declaredField.getAnnotation(HardyAutowired.class); String beanName = annotation.value(); // 需要注入的bean的id if ("".equals(beanName.trim())) { // 沒有配置具體的bean id,那就需要根據當前欄位型別注入(介面注入) IDemoService beanName = declaredField.getType().getName(); } // 開啟賦值 declaredField.setAccessible(true); try { declaredField.set(entry.getValue(), ioc.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } /** * ioc容器:基於classNames快取的類的全限定類名,以及反射技術,完成物件建立和管理 * */ private void doInstance() { if (classNames.size() == 0) { return ; } try{ for (int i = 0; i < classNames.size(); i++) { String className = classNames.get(i); // com.hardy.demo.controller.DemoController // 反射 Class<?> aClass = Class.forName(className); // 區分controller,區分service' if(aClass.isAnnotationPresent(HardyController.class)) { // controller的id此處不做過多處理,不取value了,就拿類的首字母小寫作為id,儲存到ioc中 String simpleName = aClass.getSimpleName();// DemoController String lowerFirstSimpleName = lowerFirst(simpleName); // demoController Object o = aClass.newInstance(); ioc.put(lowerFirstSimpleName,o); }else if(aClass.isAnnotationPresent(HardyService.class)) { HardyService annotation = aClass.getAnnotation(HardyService.class); //獲取註解value值 String beanName = annotation.value(); // 如果指定了id,就以指定的為準 if(!"".equals(beanName.trim())) { ioc.put(beanName,aClass.newInstance()); }else{ // 如果沒有指定,就以類名首字母小寫 beanName = lowerFirst(aClass.getSimpleName()); ioc.put(beanName,aClass.newInstance()); } // service層往往是有介面的,面向介面開發,此時再以介面名為id,放入一份物件到ioc中,便於後期根據介面型別注入 Class<?>[] interfaces = aClass.getInterfaces(); for (int j = 0; j < interfaces.length; j++) { Class<?> anInterface = interfaces[j]; // 以介面的全限定類名作為id放入 ioc.put(anInterface.getName(),aClass.newInstance()); } } else{ continue; } } } catch (Exception e) { e.printStackTrace(); } } /** * 首字母小寫方法 * @param str * @return */ public String lowerFirst(String str) { char[] chars = str.toCharArray(); if ('A' <= chars[0] && chars[0] <= 'Z') { chars[0] += 32; } return String.valueOf(chars); } /** * 掃描類:scanPackage: com.hardy.demo package----> 磁碟上的資料夾(File) com/hardy/demo * @param scanPackage */ private void doScan(String scanPackage) { String scanPackagePath = Thread.currentThread().getContextClassLoader().getResource("").getPath() + scanPackage.replaceAll("\\.", "/"); File pack = new File(scanPackagePath); File[] files = pack.listFiles(); if (files != null && files.length > 0) { for (File file: files) { if(file.isDirectory()) { // 子package // 遞迴 doScan(scanPackage + "." + file.getName()); // com.hardy.demo.controller }else if(file.getName().endsWith(".class")) { String className = scanPackage + "." + file.getName().replaceAll(".class", ""); classNames.add(className); } } } } /** * 載入配置檔案 * @param contextConfigLocation */ private void doLoadConfig(String contextConfigLocation) { InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation); try { properties.load(resourceAsStream); } catch (IOException e) { e.printStackTrace(); } } /** * 接收處理請求 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 處理請求:根據url,找到對應的Method方法,進行呼叫 // 獲取uri // String requestURI = req.getRequestURI(); // Method method = handlerMapping.get(requestURI);// 獲取到一個反射的方法 // 反射呼叫,需要傳入物件,需要傳入引數,此處無法完成呼叫,沒有把物件快取起來,也沒有引數!!!!改造initHandlerMapping(); // method.invoke() // // 根據uri獲取到能夠處理當前請求的hanlder(從handlermapping中(list)) Handler handler = getHandler(req); // handler為空則返回:404 not found if (handler == null) { resp.getWriter().write("404 not found"); return; } // 引數繫結 // 獲取所有引數型別陣列,這個陣列的長度就是我們最後要傳入的args陣列的長度 Class<?>[] parameterTypes = handler.getMethod().getParameterTypes(); // 根據上述陣列長度建立一個新的陣列(引數陣列,是要傳入反射呼叫的) Object[] paraValues = new Object[parameterTypes.length]; // 以下就是為了向引數陣列中塞值,而且還得保證引數的順序和方法中形參順序一致 Map<String, String[]> parameterMap = req.getParameterMap(); // 遍歷request中所有引數 (填充除了request,response之外的引數) for (Map.Entry<String,String[]> param: parameterMap.entrySet()) { // name=1&name=2 name [1,2] String value = StringUtils.join(param.getValue(), ","); // 如同 1,2 // 如果引數和方法中的引數匹配上了,填充資料 if (!handler.getParamIndexMapping().containsKey(param.getKey())) { continue ; } // 方法形參確實有該引數,找到它的索引位置,對應的把引數值放入paraValues Integer index = handler.getParamIndexMapping().get(param.getKey());//name在第 2 個位置 paraValues[index] = value; // 把前臺傳遞過來的引數值填充到對應的位置去 } int requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName()); // 0 paraValues[requestIndex] = req; int responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName()); // 1 paraValues[responseIndex] = resp; // 最終呼叫handler的method屬性 try { handler.getMethod().invoke(handler.getController(), paraValues); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } private Handler getHandler(HttpServletRequest req) { // 為空則直接返回null if (handlerMapping.isEmpty()) { return null; } String url = req.getRequestURI(); // 遍歷handlerMapping for (Handler handler : handlerMapping) { // pattern封裝了url,判斷其是否匹配url Matcher matcher = handler.getPattern().matcher(url); // 不匹配則跳過 if (!matcher.matches()) { continue; } // 匹配則返回handler return handler; } return null; } }
4、web.xml配置
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <!-- 配置servlet --> <servlet> <servlet-name>hardymvc</servlet-name> <servlet-class>com.hardy.edu.mvcframework.servlet.HardyDispatcherServlet</servlet-class> <!-- servlet需要載入的配置檔案--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>springmvc.properties</param-value> </init-param> </servlet> <!-- 配置對映,攔截所有 --> <servlet-mapping> <servlet-name>hardymvc</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
5、springmvc.properties
scanPackage=com.hardy.demo
呼叫過程
IDemoService:
package com.hardy.demo.service; /** * @author HardyYao * @date 2021/5/13 * @description */ public interface IDemoService { String get(String name);
}
DemoServiceImpl:
package com.hardy.demo.service.impl; import com.hardy.demo.service.IDemoService; import com.hardy.edu.mvcframework.annotations.HardyService; /** * @author HardyYao * @date 2021/5/13 * @description */ @HardyService("demoService") public class DemoServiceImpl implements IDemoService { @Override public String get(String name) { System.out.println("service 實現類中的name引數:" + name) ; return name; } }
DemoController:
package com.hardy.demo.controller; import com.hardy.demo.service.IDemoService; import com.hardy.edu.mvcframework.annotations.HardyAutowired; import com.hardy.edu.mvcframework.annotations.HardyController; import com.hardy.edu.mvcframework.annotations.HardyRequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author HardyYao * @date 2021/5/13 * @description */ @HardyController @HardyRequestMapping("/demo") public class DemoController { @HardyAutowired private IDemoService demoService; /** * URL: /demo/query?name=zhangsan * @param request * @param response * @param name * @return */ @HardyRequestMapping("/query") public String query(HttpServletRequest request, HttpServletResponse response, String name) { return demoService.get(name); } }
完整的專案結構如下所示:
執行結果
使用外掛tomcat執行專案,使用debug模式,執行後訪問:http://localhost:8080/,控制檯會列印:hardy mvc 初始化完成.... 的訊息,如下所示:
由於沒有指明要呼叫哪個介面,故返回:404 not found,如下所示:
訪問我們前面編寫好的介面:/demo/query?name=zhangsan,訪問結果如下所示:
由上述結果可知,因為後端介面沒有返回對應的檢視,故頁面返回結果為空,但控制檯確實列印出了我們傳遞的引數資訊,可以看到我們的自定義mvc框架實現了它的基本功能。
分析介面請求引數
最後我們看一下除錯打斷點時 各個引數的情況,如下圖所示,可以看到handler中封裝了controller、訪問的介面、匹配到的url及引數順序等資訊:
介面請求對應的引數型別及引數值如下所示:
總結
今天我們自定義了一個mvc框架,該框架實現了:載入配置檔案、掃描相關的類,掃描註解、Bean初始化以及依賴注入維護、Spring MVC相關元件的初始化、建立url與method之間的對映關係及接受並處理請求的功能。
雖然這裡僅僅實現了最基本的功能,但是需要學習的東西也還是挺多的,部分程式碼也是比較複雜的。
下一篇文章,會在Spring MVC框架的基礎上實現訪問攔截的功能。