當初看了《從零開始寫一個Java Web框架》,也跟着寫了一遍,但當時學藝不精,真正進腦子里的並不是很多,作者將依賴注入框架和MVC框架寫在一起也給我造成了不小的困擾。最近剛好看了一遍springMVC的官方文檔,對過去一段時間的使用做了一下總結,總結了一些MVC的使用需求,打算自己開坑寫一個MVC框架,雖然是重復造輪子的過程,但也是學習提高的過程。
1.我們可能需要一個什么樣的MVC框架
(1)用戶一:我討厭配置文件,最好能用注解的全用注解注解,能掃描直接掃描
(2)用戶二:最好我導入一個jar包,有默認的servlet配置,也可以按自己的需要配置,然后就直接寫這樣的代碼就可以處理請求了
@Controller public class Test { @MapURL(value = "/as.do") public String main(HttpServletRequest req,HttpServletResponse resp,ModelMap model,String name) throws IOException, ServletException { System.out.println("as1"); model.put("time",new Date(System.currentTimeMillis())); return "test"; } }
(3)用戶三:返回的話,直接返回一個字符串,在配置中寫明頁面根路徑,直接返回頁面名字就好了
(4)用戶四:springMVC里想用什么參數都可以直接寫到函數里,好方便,最好也實現一下
每個新出的框架都會說自己簡單,性能好不會像已經成熟的框架那樣復雜,冗余,而且用不到的功能很多,但會降低系統性能,但隨着需求越來越多,每個框架都會越來越復雜,功能越來越多,只有最適合自己的才是最好的框架。
2.從annotation說起
annotation是從jdk1.5起引入的新機制,annotation旨在將類,方法,變量與特定的信息或是元數據相關聯,注解可以理解為一個框架可以識別的注釋或是標簽,當我在類上標了@Controller時,框架就知道了這是個控制器,掃描類的時候遇到有這個注解的就放進來。
一個最簡單的演示就是我定義一個注解類,使用@interface,設置對應的元數據屬性,然后創建一個類,在類上面標上@Controller,
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Controller {}
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MapURL { String value(); String method() default "GET"; }
然后我獲得這個類,可以new完了獲取class,也可以直接Class.forName,調用class.isAnnotationPresent(Controller.class),通過得到的布爾值來判斷這個類的注解是不是Controller。對於方法級別的注釋,采用@MapURL標簽,設置2個屬性value和method,分別代表短url和http的請求方式,默認是get,不是的話需要單獨設置。在實際的項目中,Controller的類一定會有很多,我們需要在初始化的時候把這些類從全部類中拿出來,放到集合中,然后在這個class的集合中拿出所有的被注解的方法,將url作為key,方法作為value存到map中(http請求方式暫未考慮,以后再重構),這個map是整個框架的核心。
在拿出全部類的過程中,用戶可以在config.ini中設置要掃描的包,程序會通過文件操作不停的遞歸找到所有的class文件,然后從這個包的所有類中挑選出Controller的類。
public static void scanClassSetByPackage(String packageName) { methodMap=new HashMap<String, MethodPro>(); classMap=new HashMap<String, Class<?>>(); classSet=new HashSet<Class<?>>(); String filePath=Config.getProPath()+ StringUtils.modifyPackagePath(packageName); FileUtils.getClassSet(filePath,classSet,packageName); for(Class<?> clazz:classSet) { if(clazz.isAnnotationPresent(Controller.class)) { Method[] methods=clazz.getDeclaredMethods(); for(Method method:methods) { if(method.isAnnotationPresent(MapURL.class)) { MapURL mapURL=method.getAnnotation(MapURL.class); MethodPro mp=new MethodPro(method,mapURL.value(),mapURL.method()); methodMap.put(mapURL.value(),mp); classMap.put(mapURL.value(),clazz); } } } } }
3.建立轉發servlet
框架歸根到底還是servlet這一套東西,請求進來,處理請求,返回結果。現在框架只是屏蔽了servlet的一些東西,讓開發者能夠使用更友好的,更簡單的方式來實現業務邏輯。框架需要servlet,一個就夠了,這個servlet要把傳進來的請求交給別人處理,交給那些別標記了@Controller中的被標記了@MapURL的方法來處理,這里就用到了剛才用到的map,map什么時候生成,可是放在靜態塊里加載的時候生成,也可以放在servlet的init方法中初始化生成。在servlet的service中,會通過request的getPathInfo(),或是getServletPath(),來獲得當前請求的短路徑,通過map拿到這個路徑url對應的method,反射,將request和response傳入,然后就可以調用任何@Controller的任何方法,一個最簡單的MVC框架到這也就算完成了。這個框架配置簡單,只需要一個servlet就可以處理各種不同的請求,開發者不必再寫一大堆servlet,只需要在@Controller中加一大堆方法就好了。現在的問題就是即使我不必寫servlet,但我在方法中寫的代碼太servlet化了,簡直沒什么區別,我寫個跳轉jsp的頁面還得request.getDispatcher("test.jsp").forward(request.response)要找一個傳進來的值還得去request中拿,想傳出去還得再放到request中。
4.實現一個springMVC式的參數填充
在最開始使用springMVC的時候,十分驚訝於這種設計,一直在想這是怎么做到的,我為什么可以使用任意多個參數,在form提交的表單中為什么同名的會直接被賦值,為什么把模型類寫到參數里會自動填充類中的屬性,還沒來得及看springMVC這塊的實現代碼,就先把自己理解的和用到的整理了一下邏輯,實現了一遍。
public Object invoke(Object obj, Object... args),在method的反射調用中采用的是可變長參數,這個機制很有意思,我可以在反射中使用很多參數比如invoke(obj,req,resp,model,name);或是把后面幾項放到數組中invoke(obj,obj[]);因為在@Controller的函數的長度是任意的,所以需要先得到對應method的信息,得到參數的個數,並生成相應長度的數組供調用。然后就得到了2個問題:
(1)怎么獲得method 的信息,為了保證名字相同的參數的替換(或是注入)以及不同類型的轉換,我們需要先知道method的參數的類型以及method參數的名字,類型還是很簡單的,只需要method.getParameterTypes()就能得到method中各個參數的類型,獲得參數的名字就比較復雜了,為了解決這個問題需要另外一個黑科技:asm。我並沒有過於深入的研究框架本身,找到了一個Demo,能夠直接獲得函數中參數的名字,將asm整合進入框架中,得到該method的2個classNames,paraNames分別為類型名和參數名的集合。
List<String> paraNames= MethodResolver.getMethodNames(clazz.getName(),methodPro.getName()); List<String> classNames= CollectionUtils.classArrToStringList(method.getParameterTypes()); Object[] args=MethodResolver.makeArgs(paraNames,classNames,req,resp,model);
public static List<String> getMethodNames(String className,String methodName) throws IOException { List<String> list=new ArrayList<String>(); String cn=Config.getProPath()+className.replace(".", "/")+".class"; InputStream is=new FileInputStream(new File(cn)); ClassReader cr = new ClassReader(is); ReadMethodArgNameClassVisitor classVisitor = new ReadMethodArgNameClassVisitor(); cr.accept(classVisitor, 0); for(Entry<String, List<String>> entry : classVisitor.nameArgMap.entrySet()) { if(entry.getKey().equals(methodName)) { for (String s : entry.getValue()) { list.add(s); } } } return list; }
(2)怎么將剛才得到的method信息,通過已有的request和response生成一個Object[] args來供反射調用。這里要分成幾個策略來進行。遍歷classNames,paraNames,對於每一組 參數類型[參數名],如果這個類型是javax.servlet.http.HttpServletRequest,或javax.servlet.http.HttpServletResponse,或ModelMap,直接加入到args中,否則繼續判斷;如果參數類型為String且參數名在request的ParameterNames中有相同的名字,加入args;不滿足的話:判斷參數名在request的ParameterNames中有相同的名字,有的話此時肯定不是String,轉換類型,不能轉換類型的話自動報錯,不必處理;不滿足的話判斷這個類型能否被加載且屬性中存在與當前參數名相同的屬性:如果滿足條件,使用反射生成這個類,並把類中的屬性與相同的request的ParameterNames那部分賦值,實現模型類的自動裝配並加入args。如果不滿足上述所有條件,args加入null。遍歷完所有的classNames,paraNames后就得到method的參數的被調用數組。
5.頁面路徑設置
在@Controller的代碼中會返回頁面的名稱字符串,在配置文件中配置頁面路徑和頁面后綴就能簡單的實現頁面的轉發。
Object result=method.invoke(clazz.newInstance(),args); Map<String,Object> map=model.getMap(); for(String key:map.keySet()) { req.setAttribute(key,map.get(key)); } if(result instanceof String) { req.getRequestDispatcher(Config.getConfig("pagePath")+File.separator+result.toString()+Config.getConfig("suffix")).forward(req, resp); }
6.部署
在框架完成后可以使用IDE自帶的功能打成jar包,然后使用 mvn install命令放到本地倉庫,新建一個web項目,導入jar包,servlet在程序中寫死了攔截*.do請求,因此不必設置web.xml,只需要最基本的內容,后面會改,實現可配置。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>11</title> </head> <body> 當前時間:${time} </body> </html>
后台的代碼就是最上面那一段代碼,這個為jsp的代碼,啟動tomcat,最后效果為
后:
這個框架前前后后寫了3,4天,加深了annotation,asm,可變長參數,反射,泛型函數以及對MVC本身框架的理解,還是很有收獲的。
gitHub: https://github.com/Asens/new-AsMVC