手擼了一個 Spring MVC 框架
廢話不多說,我們進入今天的正題,在Web應用程序設計中,MVC模式已經被廣泛使用。SpringMVC以DispatcherServlet為核心,負責協調和組織不同組件以完成請求處理并返回響應的工作,實現了MVC模式。想要實現自己的SpringMVC框架,需要從以下幾點入手:
了解SpringMVC運行流程及九大組件
梳理自己的SpringMVC的設計思路
實現自己的SpringMVC框架
一、了解SpringMVC運行流程及九大組件
1. SpringMVC的運行流程

⑴ 用戶發(fā)送請求至前端控制器DispatcherServlet
⑵ DispatcherServlet收到請求調用HandlerMapping處理器映射器。
⑶ 處理器映射器根據請求url找到具體的處理器,生成處理器對象及處理器攔截器(如果有則生成)一并返回給DispatcherServlet。
⑷ DispatcherServlet通過HandlerAdapter處理器適配器調用處理器
⑸ 執(zhí)行處理器(Controller,也叫后端控制器)。
⑹ Controller執(zhí)行完成返回ModelAndView
⑺ HandlerAdapter將controller執(zhí)行結果ModelAndView返回給DispatcherServlet
⑻ DispatcherServlet將ModelAndView傳給ViewReslover視圖解析器
⑼ ViewReslover解析后返回具體View
⑽ DispatcherServlet對View進行渲染視圖(即將模型數據填充至視圖中)
從上面可以看出,DispatcherServlet有接收請求,響應結果,轉發(fā)等作用。有了DispatcherServlet之后,可以減少組件之間的耦合度。
2. SpringMVC的九大組件
protected?void?initStrategies(ApplicationContext context) {
??//用于處理上傳請求。處理方法是將普通的request包裝成MultipartHttpServletRequest,后者可以直接調用getFile方法獲取File.
??initMultipartResolver(context);
??//SpringMVC主要有兩個地方用到了Locale:一是ViewResolver視圖解析的時候;二是用到國際化資源或者主題的時候。
??initLocaleResolver(context);
??//用于解析主題。SpringMVC中一個主題對應一個properties文件,里面存放著跟當前主題相關的所有資源、
??//如圖片、css樣式等。SpringMVC的主題也支持國際化,
??initThemeResolver(context);
??//用來查找Handler的。
??initHandlerMappings(context);
??//從名字上看,它就是一個適配器。Servlet需要的處理方法的結構卻是固定的,都是以request和response為參數的方法。
??//如何讓固定的Servlet處理方法調用靈活的Handler來進行處理呢?這就是HandlerAdapter要做的事情
??initHandlerAdapters(context);
??//其它組件都是用來干活的。在干活的過程中難免會出現問題,出問題后怎么辦呢?
??//這就需要有一個專門的角色對異常情況進行處理,在SpringMVC中就是HandlerExceptionResolver。
??initHandlerExceptionResolvers(context);
??//有的Handler處理完后并沒有設置View也沒有設置ViewName,這時就需要從request獲取ViewName了,
??//如何從request中獲取ViewName就是RequestToViewNameTranslator要做的事情了。
??initRequestToViewNameTranslator(context);
??//ViewResolver用來將String類型的視圖名和Locale解析為View類型的視圖。
??//View是用來渲染頁面的,也就是將程序返回的參數填入模板里,生成html(也可能是其它類型)文件。
??initViewResolvers(context);
??//用來管理FlashMap的,FlashMap主要用在redirect重定向中傳遞參數。
??initFlashMapManager(context);
}二、梳理SpringMVC的設計思路
本文只實現自己的@Controller、@RequestMapping、@RequestParam注解起作用,其余SpringMVC功能讀者可以嘗試自己實現。
?1、讀取配置? ? ?

從圖中可以看出,SpringMVC本質上是一個Servlet,這個 Servlet 繼承自 HttpServlet。FrameworkServlet負責初始化SpringMVC的容器,并將Spring容器設置為父容器。
因為本文只是實現SpringMVC,對于Spring容器不做過多講解(有興趣同學可以看看我另一篇文章:向spring大佬低頭--大量源碼流出解析)。
為了讀取web.xml中的配置,我們用到ServletConfig這個類,它代表當前Servlet在web.xml中的配置信息。通過web.xml中加載我們自己寫的MyDispatcherServlet和讀取配置文件。
2、初始化階段
在前面我們提到DispatcherServlet的initStrategies方法會初始化9大組件,但是這里將實現一些SpringMVC的最基本的組件而不是全部,按順序包括:
加載配置文件
掃描用戶配置包下面所有的類
拿到掃描到的類,通過反射機制,實例化。并且放到ioc容器中(Map的鍵值對? beanName-bean) beanName默認是首字母小寫
初始化HandlerMapping,這里其實就是把url和method對應起來放在一個k-v的Map中,在運行階段取出
3、運行階段
異常的攔截 獲取請求傳入的參數并處理參數 通過初始化好的handlerMapping中拿出url對應的方法名,反射調用
三、實現自己的SpringMVC框架

<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.0modelVersion>
??<groupId>com.liughgroupId>
??<artifactId>liughMVCartifactId>
??<version>0.0.1-SNAPSHOTversion>
??<packaging>warpackaging>
??
??<properties>
????<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
????<maven.compiler.source>1.8maven.compiler.source>
????<maven.compiler.target>1.8maven.compiler.target>
????<java.version>1.8java.version>
??properties>
??
??<dependencies>
???????<dependency>
?????????<groupId>javax.servletgroupId>?
???????<artifactId>javax.servlet-apiartifactId>?
???????<version>3.0.1version>?
???????<scope>providedscope>
????dependency>
????dependencies>
project>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"?xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
??xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
??version="3.0">
??<servlet>
????<servlet-name>MySpringMVCservlet-name>
????<servlet-class>com.liugh.servlet.MyDispatcherServletservlet-class>
????<init-param>
??????<param-name>contextConfigLocationparam-name>
??????<param-value>application.propertiesparam-value>
????init-param>
????<load-on-startup>1load-on-startup>
??servlet>
??<servlet-mapping>
????<servlet-name>MySpringMVCservlet-name>
????<url-pattern>/*url-pattern>
??servlet-mapping>web-app>scanPackage=com.liugh.core
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public?@interface?MyController {
??/**
?????* 表示給controller注冊別名
?????* @return
?????*/
????String?value() default?"";
}RequestMapping注解,可以在類和方法上:
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public?@interface?MyRequestMapping {
??/**
?????* 表示訪問該方法的url
?????* @return
?????*/
????String?value() default?"";
}RequestParam注解,只能注解在參數上
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface?MyRequestParam {
??/**
?????* 表示參數的別名,必填
?????* @return
?????*/
????String?value();
}然后創(chuàng)建MyDispatcherServlet這個類,去繼承HttpServlet,重寫init方法、doGet、doPost方法,以及加上我們第二步分析時要實現的功能:
public?class?MyDispatcherServlet extends?HttpServlet{
??
??private?Properties properties = new?Properties();
??private?List<String> classNames = new?ArrayList<>();
??private?Map<String, Object> ioc = new?HashMap<>();
??private?Map<String, Method> handlerMapping = new??HashMap<>();
??private?Map<String, Object> controllerMap =new?HashMap<>();
??@Override
??public?void?init(ServletConfig config) throws ServletException {
????
????//1.加載配置文件
????doLoadConfig(config.getInitParameter("contextConfigLocation"));
????//2.初始化所有相關聯的類,掃描用戶設定的包下面所有的類
????doScanner(properties.getProperty("scanPackage"));
????//3.拿到掃描到的類,通過反射機制,實例化,并且放到ioc容器中(k-v beanName-bean) beanName默認是首字母小寫
????doInstance();
????//4.初始化HandlerMapping(將url和method對應上)
????initHandlerMapping();
??}
??@Override
??protected?void?doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
????this.doPost(req,resp);
??}
??@Override
??protected?void?doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
????try?{
??????//處理請求
??????doDispatch(req,resp);
????} catch?(Exception e) {
??????resp.getWriter().write("500!! Server Exception");
????}
??}
??private?void?doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
????if(handlerMapping.isEmpty()){
??????return;
????}
????
????String?url =req.getRequestURI();
????String?contextPath = req.getContextPath();
????
????url=url.replace(contextPath, "").replaceAll("/+", "/");
????
????if(!this.handlerMapping.containsKey(url)){
??????resp.getWriter().write("404 NOT FOUND!");
??????return;
????}
????
????Method method =this.handlerMapping.get(url);
????
????//獲取方法的參數列表
????Class>[] parameterTypes = method.getParameterTypes();
??
????//獲取請求的參數
????Map<String, String[]> parameterMap = req.getParameterMap();
????
????//保存參數值
????Object?[] paramValues= new?Object[parameterTypes.length];
????
????//方法的參數列表
????????for?(int i = 0; i????????????//根據參數名稱,做某些處理
????????????String?requestParam = parameterTypes[i].getSimpleName();
????????????
????????????
????????????if?(requestParam.equals("HttpServletRequest")){
????????????????//參數類型已明確,這邊強轉類型
??????????????paramValues[i]=req;
????????????????continue;
????????????}
????????????if?(requestParam.equals("HttpServletResponse")){
??????????????paramValues[i]=resp;
????????????????continue;
????????????}
????????????if(requestParam.equals("String")){
??????????????for?(Entry<String, String[]> param : parameterMap.entrySet()) {
???????????????String?value =Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",\\s", ",");
???????????????paramValues[i]=value;
?????????????}
????????????}
????????}
????//利用反射機制來調用
????try?{
??????method.invoke(this.controllerMap.get(url), paramValues);//第一個參數是method所對應的實例 在ioc容器中
????} catch?(Exception e) {
??????e.printStackTrace();
????}
??}
??private?void??doLoadConfig(String?location){
????//把web.xml中的contextConfigLocation對應value值的文件加載到流里面
????InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(location);
????try?{
??????//用Properties文件加載文件里的內容
??????properties.load(resourceAsStream);
????} catch?(IOException e) {
??????e.printStackTrace();
????}finally?{
??????//關流
??????if(null!=resourceAsStream){
????????try?{
??????????resourceAsStream.close();
????????} catch?(IOException e) {
??????????e.printStackTrace();
????????}
??????}
????}
????
??}
??
??private?void?doScanner(String?packageName) {
????//把所有的.替換成/
????URL url =this.getClass().getClassLoader().getResource("/"+packageName.replaceAll("\\.", "/"));
????File dir = new?File(url.getFile());
????for?(File file : dir.listFiles()) {
??????if(file.isDirectory()){
????????//遞歸讀取包
????????doScanner(packageName+"."+file.getName());
??????}else{
????????String?className =packageName +"."?+file.getName().replace(".class", "");
????????classNames.add(className);
??????}
????}
??}
??
??
??
??private?void?doInstance() {
????if?(classNames.isEmpty()) {
??????return;
????}
????for?(String?className : classNames) {
??????try?{
????????//把類搞出來,反射來實例化(只有加@MyController需要實例化)
????????Class> clazz =Class.forName(className);
?????????if(clazz.isAnnotationPresent(MyController.class)){
??????????ioc.put(toLowerFirstWord(clazz.getSimpleName()),clazz.newInstance());
????????}else{
??????????continue;
????????}
????????
????????
??????} catch?(Exception e) {
????????e.printStackTrace();
????????continue;
??????}
????}
??}
??private?void?initHandlerMapping(){
????if(ioc.isEmpty()){
??????return;
????}
????try?{
??????for?(Entry<String, Object> entry: ioc.entrySet()) {
????????Class extends?Object> clazz = entry.getValue().getClass();
????????if(!clazz.isAnnotationPresent(MyController.class)){
??????????continue;
????????}
????????
????????//拼url時,是controller頭的url拼上方法上的url
????????String?baseUrl ="";
????????if(clazz.isAnnotationPresent(MyRequestMapping.class)){
??????????MyRequestMapping annotation = clazz.getAnnotation(MyRequestMapping.class);
??????????baseUrl=annotation.value();
????????}
????????Method[] methods = clazz.getMethods();
????????for?(Method method : methods) {
??????????if(!method.isAnnotationPresent(MyRequestMapping.class)){
????????????continue;
??????????}
??????????MyRequestMapping annotation = method.getAnnotation(MyRequestMapping.class);
??????????String?url = annotation.value();
??????????
??????????url =(baseUrl+"/"+url).replaceAll("/+", "/");
??????????handlerMapping.put(url,method);
??????????controllerMap.put(url,clazz.newInstance());
??????????System.out.println(url+","+method);
????????}
????????
??????}
??????
????} catch?(Exception e) {
??????e.printStackTrace();
????}
????
??}
??/**
???* 把字符串的首字母小寫
???* @param name
???* @return
???*/
??private?String?toLowerFirstWord(String?name){
????char[] charArray = name.toCharArray();
????charArray[0] += 32;
????return?String.valueOf(charArray);
??}
??
????
} @MyController
@MyRequestMapping("/test")
public class TestController {
??
???@MyRequestMapping("/doTest")
????public void test1(HttpServletRequest request, HttpServletResponse response,
????????@MyRequestParam("param") String param){
?????System.out.println(param);
??????try?{
????????????response.getWriter().write( "doTest method success! param:"+param);
????????} catch?(IOException e) {
????????????e.printStackTrace();
????????}
????}
???
???@MyRequestMapping("/doTest2")
????public?void?test2(HttpServletRequest request, HttpServletResponse response){
????????try?{
????????????response.getWriter().println("doTest2 method success!");
????????} catch?(IOException e) {
????????????e.printStackTrace();
????????}
????}
}訪問 http://localhost:8080/liughMVC/test/doTest?param=liugh 如下:


(完)
?關注公眾號:Java后端編程,回復下面關鍵字?
要Java學習完整路線,回復??路線?
缺Java入門視頻,回復:?視頻?
要Java面試經驗,回復??面試?
缺Java項目,回復:?項目?
進Java粉絲群:?加群?
PS:如果覺得我的分享不錯,歡迎大家隨手點贊、在看。
(完) 加我"微信"?獲取一份 最新Java面試題資料 請備注:666,不然不通過~
最近好文
最近面試BAT,整理一份面試資料《Java面試BAT通關手冊》,覆蓋了Java核心技術、JVM、Java并發(fā)、SSM、微服務、數據庫、數據結構等等。 獲取方式:關注公眾號并回復?java?領取,更多內容陸續(xù)奉上。 明天見(??ω??)??
