SpringMvc本質上就是對Servlet的封裝。html
由於建立一個Maven項目,而後在pom文件中增長一個依賴:java
<dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <!-- 部署在服務器時,不使用這個servlet-api 而使用tomcat的--> <scope>provided</scope> </dependency>
2,建立DispatcherServlet,並註冊到web.xml中web
package com.dxh.edu.mvcframework.servlet; 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.IOException; public class DxhDispatcherServlet extends HttpServlet { /** * 接收處理請求 */ @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 { } }
web.xml:spring
<!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-name>dxhmvc</servlet-name> <servlet-class>com.dxh.edu.mvcframework.servlet.DxhDispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>springmvc.properties</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dxhmvc</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
由於要使用到註解,因此首先要自定義幾個註解:apache
這裏就不贅述如何自定義註解了,詳情請看:https://www.cnblogs.com/peida/archive/2013/04/24/3036689.htmlapi
Controller註解:數組
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DxhController { String value() default ""; }
Service註解:瀏覽器
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DxhService { String value() default ""; }
RequestMapping註解:緩存
@Documented @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DxhRequestMapping { String value() default ""; }
Autowired註解:tomcat
@Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface DxhAutowired { String value() default ""; }
測試代碼咱們放在同項目中的com.dxh.demo包中:
package com.dxh.demo.controller; import com.dxh.demo.service.IDemoService; import com.dxh.edu.mvcframework.annotations.DxhAutowired; import com.dxh.edu.mvcframework.annotations.DxhController; import com.dxh.edu.mvcframework.annotations.DxhRequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @DxhController @DxhRequestMapping("/demo") public class DemoController { @DxhAutowired private IDemoService demoService; /** * URL:/demo/query */ @DxhRequestMapping("/query") public String query(HttpServletRequest request, HttpServletResponse response, String name){ return demoService.get(name); } }
package com.dxh.demo.service; public interface IDemoService { String get(String name); }
package com.dxh.demo.service.impl; import com.dxh.demo.service.IDemoService; import com.dxh.edu.mvcframework.annotations.DxhService; @DxhService("demoService") public class IDemoServiceImpl implements IDemoService { @Override public String get(String name) { System.out.println("Service實現類中的Name:"+ name); return name; } }
在建立好的DxhDispatcherServlet中重寫init()方法,並在init方法中作初始化配置:
@Override public void init(ServletConfig config) throws ServletException { //1. 加載配置文件 springmvc.properties String contextConfigLocation = config.getInitParameter("contextConfigLocation"); doLoadConfig(contextConfigLocation); //2. 掃描相關的類——掃描註解 doScan(""); //3. 初始化Bean對象(實現IOC容器,基於註解) doInstance(); //4. 實現依賴注入 doAutoWired(); //5. 構造一個handleMapping處理器映射器,將配置好的url和method創建映射關係 initHandleMapping(); System.out.println("MVC 初始化完成"); //6. 等待請求進入處理請求 }
以及5個空方法,這篇文章自定義MVC框架其實就是須要對這5個步驟的編寫。
//TODO 5,構造一個映射器 private void initHandleMapping() { } //TODO 4,實現依賴注入 private void doAutoWired() { } //TODO 3,IOC容器 private void doInstance() { } //TODO 2,掃描類 private void doScan(String scanPackage) { } //TODO 1,加載配置文件 private void doLoadConfig(String contextConfigLocation) { }
首先在resource目錄中建立一個配置文件——springmvc.properties
表示要掃描com.dxh.demo下的全部註解。
而後在web.xml中進行配置:
這樣,就能夠經過config.getInitParameter("contextConfigLocation")得到這個路徑。
private Properties properties = new Properties();;
//1,加載配置文件 private void doLoadConfig(String contextConfigLocation) { //根據指定路徑加載成流: InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation); try { properties.load(resourceAsStream); } catch (IOException e) { e.printStackTrace(); } }
//2. 掃描相關的類——掃描註解 doScan(properties.getProperty("scanPackage"));
//緩存掃描到的類的全類名 private List<String> classNames = new ArrayList<>();
//2,掃描類 //scanPackage :com.dxh.demo package--->磁盤的文件夾(File) private void doScan(String scanPackage) { //1.得到classPath路徑 String clasPath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); //2.拼接,獲得scanPackage在磁盤上的路徑 String scanPackagePath= clasPath + scanPackage.replaceAll("\\.","/"); File pack = new File(scanPackagePath); File[] files = pack.listFiles(); for (File file : files) { if (file.isDirectory()){ //子 package //遞歸 doScan(scanPackage+"."+file.getName()); //com.dxh.demo.controller }else if(file.getName().endsWith(".class")){ String className = scanPackage + "." + file.getName().replace(".class", ""); classNames.add(className); } } }
上一步驟咱們把掃描到的類的全類名放到了,list中,那麼本次步驟須要遍歷整個list:
代碼實現:
//IOC容器 private Map<String,Object> ioc = new HashMap<>();
//3,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.dxh.demo.controller.DemoController //反射 Class<?> aClass = Class.forName(className); //區分controller ,區分service if (aClass.isAnnotationPresent(DxhController.class)){ //controller的id此處不作過多處理,不取value了,用類的首字母小寫做爲id,保存到IOC容器中 String simpleName = aClass.getSimpleName();//DemoController String lowerFirstSimpleName = lowerFirst(simpleName); //demoController Object bean = aClass.newInstance(); ioc.put(lowerFirstSimpleName,bean); }else if (aClass.isAnnotationPresent(DxhService.class)){ DxhService annotation = aClass.getAnnotation(DxhService.class); //獲取註解的值 String beanName = annotation.value(); //指定了id就以指定的id爲準 if (!"".equals(beanName.trim())){ ioc.put(beanName,aClass.newInstance()); }else{ //沒有指定id ,首字母小寫 String lowerFirstSimpleName = lowerFirst(aClass.getSimpleName()); ioc.put(lowerFirstSimpleName,aClass.newInstance()); } //service層每每是有接口的,再以接口名爲id再存入一分bean到ioc,便於後期根據接口類型注入 Class<?>[] interfaces = aClass.getInterfaces(); for (Class<?> anInterface : interfaces) { //以接口的類名做爲id放入。 ioc.put(anInterface.getName(),aClass.newInstance()); } }else { continue; } } }catch (Exception e){ e.printStackTrace(); } }
上一步驟把全部須要加載的bean,存入了ioc Map中,此時,咱們就須要遍歷這個map而後依次獲得每一個bean對象,而後判斷對象中有沒有被@****DxhAutowired修飾的屬性。
代碼實現:
//4,實現依賴注入 private void doAutoWired() { if (ioc.isEmpty()){return;} //1,判斷容器中有沒有被@DxhAutowried註解的屬性,若是有須要維護依賴注入關係 for (Map.Entry<String,Object> entry: ioc.entrySet()){ //獲取bean對象中的字段信息 Field[] declaredFields = entry.getValue().getClass().getDeclaredFields(); for (Field declaredField : declaredFields) { if (!declaredField.isAnnotationPresent(DxhAutowired.class)){ continue; } //有該註解: DxhAutowired annotation = declaredField.getAnnotation(DxhAutowired.class); String beanName = annotation.value(); //須要注入的bean的Id if ("".equals(beanName.trim())){ //沒有配置具體的beanId,須要根據當前字段的類型注入(接口注入) IDemoService beanName = declaredField.getType().getName(); } //開啓賦值 declaredField.setAccessible(true); try { //字段調用,兩個參數:(哪一個對象的字段,傳入什麼) declaredField.set(entry.getValue(),ioc.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } }
構造一個handleMapping處理器映射器,將配置好的url和method創建映射關係****。
手寫MVC框架最關鍵的環節
假設有一個:
那麼如何經過/demo/query定位到 DemoController類中的query這個方法 ?
以前咱們全部被@DxhController(自定義Controller註解)的類,都存在了ioc 這個map中。
咱們能夠遍歷這個map,獲得每一個bean對象
而後判斷是否被@DxhController所修飾(排除@DxhService所修飾的bean)
而後判斷是否被@DxhRequestMapping所修飾,有的話,就取其value值,做爲baseUrl
而後遍歷該bean對象中的全部方法,獲得被@DxhRequestMapping修飾的方法。獲得其value值,做爲methodUrl。
baseUrl + methodUrl = url
咱們把url和當前method綁定起來,存在map中,也就是創建了url和method創建映射關係。
代碼實現:
//handleMapping ,存儲url和method直接的映射關係 private Map<String,Object> handleMapping = new HashMap<>();
//5,構造一個映射器,將url和method進行關聯 private void initHandleMapping() { if (ioc.isEmpty()){return;} for (Map.Entry<String,Object> entry: ioc.entrySet()){ //獲取ioc中當前遍歷對象的class類型 Class<?> aClass = entry.getValue().getClass(); //排除非controller層的類 if (!aClass.isAnnotationPresent(DxhController.class)){ continue; } String baseUrl = ""; if (aClass.isAnnotationPresent(DxhRequestMapping.class)){ //Controller層 類上 註解@DxhRequestMapping中的value值 baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value(); } //獲取方法 Method[] methods = aClass.getMethods(); for (Method method : methods) { //排除沒有@DxhRequestMapping註解的方法 if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;} //Controller層 類中方法上 註解@DxhRequestMapping中的value值 String methodUrl = method.getAnnotation(DxhRequestMapping.class).value(); String url = baseUrl+methodUrl; //創建url和method之間的映射關係,用map緩存起來 handleMapping.put(url,method); } } }
到目前位置,尚未徹底寫完,可是不妨礙咱們測試一下看看剛纔寫的那部份內容有沒有什麼問題:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.dxh.edu</groupId> <artifactId>mvc</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>mvc Maven Webapp</name> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <!-- 部署在服務器時,不使用這個servlet-api 而使用tomcat的--> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugins> <!-- 編譯插件定義編譯細節--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>11</source> <target>11</target> <encoding>utf-8</encoding> <!-- 告訴編譯器,編譯的時候記錄下形參的真實名稱--> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <path>/</path> </configuration> </plugin> </plugins> </build> </project>
pom文件中加入了一個tomcat插件,並設置端口爲8080,所以咱們經過tomcat啓動項目:
啓動完成後,打開瀏覽器url中輸入:
http://localhost:8080/demo/query
瀏覽器中什麼都沒返回(咱們的代碼還沒真正的完成,還沒有編寫處理請求步驟),同時控制檯中打印了MVC初始化完成,能夠認爲,目前的代碼沒有明顯的缺陷。 咱們繼續~~~~~
DxhDispatcherServlet這個類繼承了HttpServlet,並重寫了doGet和doPost方法,在doGet中調用了doPost方法,當咱們使用反射調用方法時(method.invoke(......))發現少了一部分參數:
所以咱們要改造initHandleMapping(),修改url和method的映射關係(不簡簡單單的存入map中)。
package com.dxh.edu.mvcframework.pojo; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; public class Handler { //method.invoke(obj,)須要 private Object controller; private Method method; //spring中url支持正則 private Pattern pattern; //參數的順序,爲了進行參數綁定 ,Key 參數名, Value 表明第幾個參數 private Map<String,Integer> paramIndexMapping; public Handler(Object controller, Method method, Pattern pattern) { this.controller = controller; this.method = method; this.pattern = pattern; this.paramIndexMapping = new HashMap<>(); } //getset方法這裏省略,實際代碼中須要... }
在Handler類中編寫了4個屬性:
首先,就不能直接經過Map<url,Method>的得方式進行關係映射了,使用一個list,泛型是剛纔建立的Handler。
//handleMapping ,存儲url和method直接的映射關係 // private Map<String,Method> handleMapping = new HashMap<>(); private List<Handler> handlerMapping = new ArrayList<>();
改動前,改動後代碼對比:
改動後的initHandleMapping():
//5,構造一個映射器,將url和method進行關聯 private void initHandleMapping() { if (ioc.isEmpty()){return;} for (Map.Entry<String,Object> entry: ioc.entrySet()){ //獲取ioc中當前遍歷對象的class類型 Class<?> aClass = entry.getValue().getClass(); //排除非controller層的類 if (!aClass.isAnnotationPresent(DxhController.class)){ continue; } String baseUrl = ""; if (aClass.isAnnotationPresent(DxhRequestMapping.class)){ //Controller層 類上 註解@DxhRequestMapping中的value值 baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value(); } //獲取方法 Method[] methods = aClass.getMethods(); for (Method method : methods) { //排除沒有@DxhRequestMapping註解的方法 if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;} //Controller層 類中方法上 註解@DxhRequestMapping中的value值 String methodUrl = method.getAnnotation(DxhRequestMapping.class).value(); String url = baseUrl+methodUrl; //把method全部信息以及url封裝爲Handler Handler handler = new Handler(entry.getValue(),method, Pattern.compile(url)); //處理計算方法的參數位置信息 Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { Parameter parameter = parameters[i]; //不作太多的參數類型判斷,只作:HttpServletRequest request, HttpServletResponse response和基本類型參數 if (parameter.getType()==HttpServletRequest.class||parameter.getType()==HttpServletResponse.class){ //若是時request和response對象,那麼參數名稱存 HttpServletRequest 和 HttpServletResponse handler.getParamIndexMapping().put(parameter.getType().getSimpleName(),i); }else{ handler.getParamIndexMapping().put(parameter.getName(),i); } } handlerMapping.add(handler); } } }
上一步驟,咱們配置了 uri和method的映射關係,並封裝到了Handler中存入list,那麼接下來,就要經過HttpServletRequest,取出uri,而後找到具體的Handler:
private Handler getHandler(HttpServletRequest req) { if (handlerMapping.isEmpty()){return null;} String url = req.getRequestURI(); //遍歷 handlerMapping for (Handler handler : handlerMapping) { Matcher matcher = handler.getPattern().matcher(url); if (!matcher.matches()){continue;} return handler; } return null; }
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { //根據uri獲取到可以處理當前請求的Handler(從handlerMapping中(list)) Handler handler = getHandler(req); if (handler==null){ resp.getWriter().write("404 not found"); return; } //參數綁定 //該方法全部參數得類型數組 Class<?>[] parameterTypes = handler.getMethod().getParameterTypes(); //根據上述數組長度建立一個新的數組(參數數組,傳入反射調用的) Object[] paramValues = new Object[parameterTypes.length]; //如下就是爲了向參數數組中設值,並且還得保證參數得順序和方法中形參順序一致。 Map<String,String[]> parameterMap = req.getParameterMap(); //遍歷request中全部的參數 ,(填充除了request、response以外的參數) for (Map.Entry<String,String[]> entry: parameterMap.entrySet()){ //name=1&name=2 name[1,2] String value = StringUtils.join(entry.getValue(), ",");// 如同 1,2 //若是參數和方法中的參數匹配上了,填充數據 if (!handler.getParamIndexMapping().containsKey(entry.getKey())){continue;} //方法形參確實有該參數,找到它得索引位置,對應得把參數值放入paramValues Integer index = handler.getParamIndexMapping().get(entry.getKey()); //把前臺傳遞過來的參數值,填充到對應得位置去 paramValues[index] = value; } Integer requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName()); paramValues[requestIndex] = req; Integer responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName()); paramValues[responseIndex] = resp; //最終調用handler得method屬性 try { Object invoke = handler.getMethod().invoke(handler.getController(), paramValues); //簡單操做,把方法返回的數據以字符串的形式寫出 resp.getWriter().write(invoke.toString()); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
打開瀏覽器,url中輸入:http://localhost:8080/demo/query?name=lisi
返回:
控制檯中打印出:
OK完成~