個人水平比較菜,沒有這么高的實力簡單實現springmvc框架,我是看了一個老哥的博客,這老哥才是大神!
原文鏈接:https://www.cnblogs.com/xdp-gacl/p/4101727.html
ok,開始瞎扯一下,我們用springmvc的時候是不是要導入依賴或者是jar包啊,那是由於別人將很多功能都給設計好了,我們直接拿過來用,這有好處也有壞處;好處是用起來很方便,直接用就好了,但是壞處就是封裝的太好了我們很難真正的弄清楚其中的運行原理,雖然說可以走源碼,但是總是感覺差了一點什么東西。
反正我走源碼就是總感覺有點欠缺,但是又說不上來哪里不對勁,直到現在醒悟了一點,其實就是對原理了解的不夠徹底,還有就是源碼都是一層套一層,層層封裝,走着走着我也不知道上一步做了什么事了,簡直就是山路十八彎,哈哈哈。
瞎扯結束,接下來我根據那個老哥的博客自己稍微整理一下,簡單實現一個springmvc的框架,只為更透徹的了解springmvc的原理!
在看本篇博客之前,可以先忘記以前springmvc的所有東西,我們以 servlet+自定義注解 實現
新建一個普通的java web項目,我的目錄結構如下:
1.工具類
自己簡單實現一些什么框架,必不可少的就是要提前准備很多的工具類
首先就是要把下面這七個工具類准備好,我把代碼復制過來;
1.1 BeanUtils:對反射的一些操作,可以根據傳入xxx.Class參數,進行實例化或者找到其中某個方法

package com.wyq.utils; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; /** * 對java反射的一些封裝 */ public class BeanUtils { /** * 實例化一個class */ public static <T> T instanceClass(Class<T> clazz){ if(!clazz.isInterface()){ try { return clazz.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } return null; } /** * 通過構造函數實例化 * */ public static <T> T instanceClass(Constructor<T> ctor, Object... args) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException{ makeAccessible(ctor); return ctor.newInstance(args);//調用構造方法實例化 } /** * 查找某個class的方法 * */ public static Method findMethod(Class<?> clazz, String methodName, Class<?>... paramTypes){ try { return clazz.getMethod(methodName, paramTypes); } catch (NoSuchMethodException e) { return findDeclaredMethod(clazz, methodName, paramTypes);//返回共有的方法 } } public static Method findDeclaredMethod(Class<?> clazz, String methodName, Class<?>[] paramTypes){ try { return clazz.getDeclaredMethod(methodName, paramTypes); } catch (NoSuchMethodException ex) { if (clazz.getSuperclass() != null) { return findDeclaredMethod(clazz.getSuperclass(), methodName, paramTypes); } return null; } } public static Method [] findDeclaredMethods(Class<?> clazz){ return clazz.getDeclaredMethods(); } public static void makeAccessible(Constructor<?> ctor) { if ((!Modifier.isPublic(ctor.getModifiers()) || !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) && !ctor.isAccessible()) { ctor.setAccessible(true);//如果是私有的 設置為true 使其可以訪問 } } public static Field[] findDeclaredFields(Class<?> clazz){ return clazz.getDeclaredFields(); } }
1.2 DispatchActionConstant:里面定義兩個常量,“forward”和“redirect”,就是為了取值方便,沒什么特殊的

package com.wyq.utils; public class DispatchActionConstant { public static String FORWARD = "forward";//轉發 public static String REDIRECT = "redirect";//重定向 }
1.3 RequestMapingMap:這里面封裝了一個HashMap,鍵是@RequestMapping注解的value的值,就是那個路徑;值為當前被@Controller注解修飾的類的xxx.Class;這個鍵值對很關鍵,等一下好好體會一下;例如這樣的形式{“/aaa”:“MyController.class”,“xxx”:“xxxx.Class”}

package com.wyq.utils; import java.util.HashMap; import java.util.Map; /** * 存儲方法的訪問路徑 */ public class RequestMapingMap { /** * requesetMap:用於存儲方法的訪問路徑和類 */ private static Map<String, Class<?>> requesetMap = new HashMap<String, Class<?>>(); public static Class<?> getClassName(String path) { return requesetMap.get(path); } public static void put(String path, Class<?> className) { requesetMap.put(path, className); } public static Map<String, Class<?>> getRequesetMap() { return requesetMap; } }
1.4ScanClassUtil:可以根據傳入的包名或者jar包名稱,獲取到包下所有類的xxx.Class,然后放進集合中返回給你

package com.wyq.utils; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.JarURLConnection; import java.net.URL; import java.net.URLDecoder; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * 掃描指定包或者jar包下面的class */ public class ScanClassUtil { /** * 從包package中獲取所有的Class */ public static Set<Class<?>> getClasses(String pack) { // 第一個class類的集合 Set<Class<?>> classes = new LinkedHashSet<Class<?>>(); // 是否循環迭代 boolean recursive = true; // 獲取包的名字 並進行替換 String packageName = pack; String packageDirName = packageName.replace('.', '/'); // 定義一個枚舉的集合 並進行循環來處理這個目錄下的things Enumeration<URL> dirs; try { dirs = Thread.currentThread().getContextClassLoader().getResources( packageDirName); // 循環迭代下去 while (dirs.hasMoreElements()) { // 獲取下一個元素 URL url = dirs.nextElement(); // 得到協議的名稱 String protocol = url.getProtocol(); // 如果是以文件的形式保存在服務器上 if ("file".equals(protocol)) { System.err.println("file類型的掃描"); // 獲取包的物理路徑 String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); // 以文件的方式掃描整個包下的文件 並添加到集合中 findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes); } else if ("jar".equals(protocol)) { // 如果是jar包文件 // 定義一個JarFile System.err.println("jar類型的掃描"); JarFile jar; try { // 獲取jar jar = ((JarURLConnection) url.openConnection()) .getJarFile(); // 從這個jar包得到一個枚舉類 Enumeration<JarEntry> entries = jar.entries(); // 同樣的進行循環迭代 while (entries.hasMoreElements()) { // 獲取jar里的一個實體 可以是目錄 和一些jar包里的其他文件 如META-INF等文件 JarEntry entry = entries.nextElement(); String name = entry.getName(); // 如果是以/開頭的 if (name.charAt(0) == '/') { // 獲取后面的字符串 name = name.substring(1); } // 如果前半部分和定義的包名相同 if (name.startsWith(packageDirName)) { int idx = name.lastIndexOf('/'); // 如果以"/"結尾 是一個包 if (idx != -1) { // 獲取包名 把"/"替換成"." packageName = name.substring(0, idx) .replace('/', '.'); } // 如果可以迭代下去 並且是一個包 if ((idx != -1) || recursive) { // 如果是一個.class文件 而且不是目錄 if (name.endsWith(".class") && !entry.isDirectory()) { // 去掉后面的".class" 獲取真正的類名 String className = name.substring( packageName.length() + 1, name .length() - 6); try { // 添加到classes classes.add(Class .forName(packageName + '.' + className)); } catch (ClassNotFoundException e) { // log // .error("添加用戶自定義視圖類錯誤 找不到此類的.class文件"); e.printStackTrace(); } } } } } } catch (IOException e) { // log.error("在掃描用戶定義視圖時從jar包獲取文件出錯"); e.printStackTrace(); } } } } catch (IOException e) { e.printStackTrace(); } return classes; } /** * 以文件的形式來獲取包下的所有Class */ public static void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) { // 獲取此包的目錄 建立一個File File dir = new File(packagePath); // 如果不存在或者 也不是目錄就直接返回 if (!dir.exists() || !dir.isDirectory()) { // log.warn("用戶定義包名 " + packageName + " 下沒有任何文件"); return; } // 如果存在 就獲取包下的所有文件 包括目錄 File[] dirfiles = dir.listFiles(new FileFilter() { // 自定義過濾規則 如果可以循環(包含子目錄) 或則是以.class結尾的文件(編譯好的java類文件) public boolean accept(File file) { return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); } }); // 循環所有文件 for (File file : dirfiles) { // 如果是目錄 則繼續掃描 if (file.isDirectory()) { findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes); } else { // 如果是java類文件 去掉后面的.class 只留下類名 String className = file.getName().substring(0, file.getName().length() - 6); try { // 添加到集合中去 //classes.add(Class.forName(packageName + '.' + className)); //經過回復同學的提醒,這里用forName有一些不好,會觸發static方法,沒有使用classLoader的load干凈 classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className)); } catch (ClassNotFoundException e) { // log.error("添加用戶自定義視圖類錯誤 找不到此類的.class文件"); e.printStackTrace(); } } } } }
1.5 View:就是一個普通類,里面存放了要跳轉的url,用在Controller中方法的返回值那里;這里還可以往VIewDate中放數據(最后就是放進request中了)

package com.wyq.utils; /** * 視圖模型 **/ public class View { private String url;//跳轉路徑 private String dispathAction = DispatchActionConstant.FORWARD;//跳轉方式 public View(String url) { this.url = url; } public View(String url,String name,Object value) { this.url = url; ViewData view = new ViewData(); view.put(name, value); } public View(String url,String name,String dispathAction ,Object value) { this.dispathAction = dispathAction; this.url = url; ViewData view = new ViewData();//請看后面的代碼 view.put(name, value); } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getDispathAction() { return dispathAction; } public void setDispathAction(String dispathAction) { this.dispathAction = dispathAction; } }
1.6 ViewData:封裝了HttpServletRequest對象,可以往request中放數據,在jsp中可以取到數據

package com.wyq.utils; import javax.servlet.http.HttpServletRequest; /** * 需要發送到客戶端顯示的數據模型 */ public class ViewData { private HttpServletRequest request; public ViewData() { initRequest(); } private void initRequest(){ //從requestHodler中獲取request對象 this.request = WebContext.requestHodler.get(); } public void put(String name,Object value){ this.request.setAttribute(name, value); } }
1.7 WebContext:這里是為了多線程設計的,這里保存了當前線程的HttpServletReques和HttpServletReponse對象,可以在其他類中隨時獲取這兩個對象

package com.wyq.utils; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * WebContext主要是用來存儲當前線程中的HttpServletRequest和HttpServletResponse * 當別的地方需要使用HttpServletRequest和HttpServletResponse,就可以通過requestHodler和responseHodler獲取 **/ public class WebContext { public static ThreadLocal<HttpServletRequest> requestHodler = new ThreadLocal<HttpServletRequest>(); public static ThreadLocal<HttpServletResponse> responseHodler = new ThreadLocal<HttpServletResponse>(); public HttpServletRequest getRequest(){ return requestHodler.get(); } public HttpSession getSession(){ return requestHodler.get().getSession(); } public ServletContext getServletContext(){ return requestHodler.get().getSession().getServletContext(); } public HttpServletResponse getResponse(){ return responseHodler.get(); } }
2.自定義注解@MyController和@RequestMapping
這兩個注解名字隨意取,我覺得最好區分我們熟悉的@Controller和@RequestMapping

package com.wyq.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 我們自定義的Controller注解 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyController { public String value() default ""; }

package com.wyq.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 我們自定義的ResponseMapping注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyRequestMapping { public String value() default ""; }
3.我們自己的Controller
注意其中的注解哦~~

package com.wyq.test; import com.wyq.annotation.MyController; import com.wyq.annotation.MyRequestMapping; import com.wyq.utils.View; @MyController public class LoginUI { //使用MyRequestMapping注解指明forward1方法的訪問路徑 @MyRequestMapping("aaa") public View forward1(){ //執行完forward1方法之后返回的視圖 return new View("/WEB-INF/login.jsp"); } //使用MyRequestMapping注解指明forward2方法的訪問路徑 @MyRequestMapping("bbb") public View forward2(){ //執行完forward2方法之后返回的視圖 return new View("/bbb.jsp"); } }
4.注解處理器(最重要!!本質上就是一個普通的servlet,沒什么稀奇)
說一下大概邏輯:
這里由於我們自定義的兩個注解只有我們自己認識,但是程序不認識啊,於是我們要寫一個servlet,當我們用瀏覽器發送請求的時候,服務器首先會自動執行這個servlet,然后去解析拿到有@MyController注解的類,遍歷其中所有的方法,將帶有@MyRequestMapping注解的value的值作為鍵,@MyController注解的類作為值,保存在一個HashMap中,就是放在前面說的工具 RequestMapingMap中;
然后我們的請求到服務器的時候,假如請求路徑為http://localhost:8090/myspringmvc/aaa.do,這個servlet中就會對這個路徑進行分割,變成取到“aaa”,然后根據這個“aaa”為鍵,從 RequestMapingMap中取出我們要的那個類(被注解@MyController修飾的),再拿到這個類下的所有方法,遍歷,看看哪個方法被@MyRequestMapping(“aaa”)注解修飾,這個方法就是我們的目標!最后用反射調用就這個方法,比如返回一個 View("/aaa.jsp") 對象,根據View中的這個url就可以用request或者response進行轉發或者重定向操作,跳轉到相應的jsp中返回給瀏覽器。

package com.wyq.handler; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Set; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.wyq.annotation.MyController; import com.wyq.annotation.MyRequestMapping; import com.wyq.utils.BeanUtils; import com.wyq.utils.DispatchActionConstant; import com.wyq.utils.RequestMapingMap; import com.wyq.utils.ScanClassUtil; import com.wyq.utils.View; import com.wyq.utils.WebContext; /** * Description: AnnotationHandleServlet作為自定義注解的核心處理器以及負責調用目標業務方法處理用戶請求<p> */ public class AnnotationHandleServlet extends HttpServlet { private String pareRequestURI(HttpServletRequest request){ String path = request.getContextPath()+"/"; String requestUri = request.getRequestURI(); String midUrl = requestUri.replaceFirst(path, ""); String lasturl = midUrl.substring(0, midUrl.lastIndexOf(".")); return lasturl; } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.excute(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.excute(request, response); } private void excute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ //將當前線程中HttpServletRequest對象存儲到ThreadLocal中,以便在Controller類中使用 WebContext.requestHodler.set(request); //將當前線程中HttpServletResponse對象存儲到ThreadLocal中,以便在Controller類中使用 WebContext.responseHodler.set(response); //解析url String lasturl = pareRequestURI(request); //獲取要使用的類 Class<?> clazz = RequestMapingMap.getRequesetMap().get(lasturl); //創建類的實例 Object classInstance = BeanUtils.instanceClass(clazz); //獲取類中定義的方法 Method [] methods = BeanUtils.findDeclaredMethods(clazz); Method method = null; for(Method m:methods){//循環方法,找匹配的方法進行執行 if(m.isAnnotationPresent(MyRequestMapping.class)){ String anoPath = m.getAnnotation(MyRequestMapping.class).value(); if(anoPath!=null && !"".equals(anoPath.trim()) && lasturl.equals(anoPath.trim())){ //找到要執行的目標方法 method = m; break; } } } try { if(method!=null){ //執行目標方法處理用戶請求 Object retObject = method.invoke(classInstance); //如果方法有返回值,那么就表示用戶需要返回視圖 if (retObject!=null) { View view = (View)retObject; //判斷要使用的跳轉方式 if(view.getDispathAction().equals(DispatchActionConstant.FORWARD)){ //使用轉發方式 request.getRequestDispatcher(view.getUrl()).forward(request, response); }else if(view.getDispathAction().equals(DispatchActionConstant.REDIRECT)){ //使用重定向方式 response.sendRedirect(request.getContextPath()+view.getUrl()); }else{ request.getRequestDispatcher(view.getUrl()).forward(request, response); } } } } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } @Override public void init(ServletConfig config) throws ServletException { /** * 重寫了Servlet的init方法后一定要記得調用父類的init方法, * 否則在service/doGet/doPost方法中使用getServletContext()方法獲取ServletContext對象時 * 就會出現java.lang.NullPointerException異常 */ super.init(config); System.out.println("---初始化開始---"); //獲取web.xml中配置的要掃描的包 String basePackage = config.getInitParameter("basePackage"); //如果配置了多個包,例如:<param-value>me.gacl.web.controller,me.gacl.web.UI</param-value> if (basePackage.indexOf(",")>0) { //按逗號進行分隔 String[] packageNameArr = basePackage.split(","); for (String packageName : packageNameArr) { initRequestMapingMap(packageName); } }else { initRequestMapingMap(basePackage); } System.out.println("----初始化結束---"); } /** * @Method: initRequestMapingMap * @Description:添加使用了Controller注解的Class到RequestMapingMap中 * @Anthor:孤傲蒼狼 * @param packageName */ private void initRequestMapingMap(String packageName){ Set<Class<?>> setClasses = ScanClassUtil.getClasses(packageName); for (Class<?> clazz :setClasses) { if (clazz.isAnnotationPresent(MyController.class)) { Method [] methods = BeanUtils.findDeclaredMethods(clazz); for(Method m:methods){//循環方法,找匹配的方法進行執行 if(m.isAnnotationPresent(MyRequestMapping.class)){ String anoPath = m.getAnnotation(MyRequestMapping.class).value(); if(anoPath!=null && !"".equals(anoPath.trim())){ if (RequestMapingMap.getRequesetMap().containsKey(anoPath)) { throw new RuntimeException("RequestMapping映射的地址不允許重復!"); } RequestMapingMap.put(anoPath, clazz); } } } } } } }
5.將注解處理器(servlet)配置到web.xml中

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>myspringmvc</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>AnnotationHandleServlet</servlet-name> <servlet-class>com.wyq.handler.AnnotationHandleServlet</servlet-class> <init-param> <description>掃描包含@MyController注解的包; 如果有多個包,以逗號分隔</description> <param-name>basePackage</param-name> <param-value>com.wyq.test</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>AnnotationHandleServlet</servlet-name> <!-- 攔截所有以.do后綴結尾的請求 --> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>
6.兩個簡單的jsp文件
aaa.jsp
bbb.jsp
多加一個jsp,新建webapps/WEB-INF/login.jsp,隨便寫點什么東西
7.簡單測試
現在就可以啟動tomcat輸入localhost:8090/myspringmvc/aaa.do看到效果了(由於我以前改過tomcat端口8090,你們的端口應該是8080),可以訪問WEB-INF中的jsp
8.稍微提一下
注意:我們在web.xml中攔截的是xxx.do請求,所以我們的url路徑要類似這樣的 http://localhost:8090/myspringmvc/aaa.do
也可以根據自己需要設置攔截類型,比如"*action",或者直接"/",這個時候你就需要在注解處理器做點修改就ok了;
舉個例子,比如我要直接用http://localhost:8090/myspringmvc/aaa去訪問,不加do了,注解處理器修改如下:
web.xml修改如下:
然后就可以看到效果了
9.總結
有沒有發現我們這里最重要的就是那個注解處理器啊,沒有這個注解處理器,那些所謂的@Controller,@ResponseBody,@RequestMapping等等,都是一文不值,只不過注解處理器被那些框架封裝得太好了,我們一般也接觸不到,我們看到的只是這樣的--------->“哇,原來這些注解這么牛啊!”“好厲害的注解啊,我要記下來這個用法”....
而注解處理器其實就是一個servlet,在tomcat服務器啟動的時候會運行init()方法,將Controller路徑和類存到一個map中;然后我們發請求的時候(比如Get請求),就會到service()方法,接着就分發到doGet()方法,再接着就是到真正處理請求的excute()方法,這個方法可以說是核心的核心,將請求路徑進行解析,就可以找到對應的Controller,遍歷這個COntroller下的所有方法並且拿到所有方法的@RequestMapping的值,找到真正的處理方法,運行,返回一個view,在這個view的內部封裝了對ViewData的操作(ViewData其實就是將數據丟到request域中,以便在jsp中可以獲取到數據),所以返回的View也可以是這樣的 return new View("/WEB-INF/login.jsp",“name”,“這里是數據”);,最后就是在jsp中展示數據了;
搞懂了這些我們再回頭看看所謂的springmvc,簡直就是一目了然啊,只是在這個基礎上又封裝了很多而已!比如前端控制器就很類似於我們這里的注解處理器,處理器映射器其實就是在一個全局map中根據請求路徑找到對應的Controller,處理器適配器不就是從這個Controller中找到目標方法執行么,后面的視圖解析器其實跟我們這也很類似啊,根據方法返回的路徑去對應的目錄下找相應的jsp文件,然后根絕request域中的數據填充進去就ok了,是不是突然感覺整個springmvc就不再那么雲里霧里了啊!哈哈哈
原文鏈接:https://www.cnblogs.com/xdp-gacl/p/4101727.html