
隨着Spring的崛起以及其功能的完善,現在可能絕大部分項目的開發都是使用Spring(全家桶)來進行開發,Spring也確實和其名字一樣,是開發者的春天,Spring解放了程序員的雙手,而等到SpringBoot出來之後配置文件大大減少,更是進一步解放了程序員的雙手,但是也正是因為Spring家族產品的強大,使得我們習慣了面向Spring開發,那麼假如有一天沒有了Spring,是不是感覺心裡一空,可能一下子連最基本的接口都不會寫了,尤其是沒有接觸過Servlet編程的朋友。因為加入沒有了Spring等框架,那麼我們就需要利用最原生的Servlet來自己實現接口路徑的映射,對象也需要自己進行管理。
Spring 能幫我們做什麼
Spring是為解決企業級應用開發的複雜性而設計的一款框架,Spring的設計理念就是:簡化開發。
在Spring框架中,一切對象都是bean,所以其通過面向bean編程(BOP),結合其核心思想依賴注入(DI)和面向切面((AOP)編程,Spring實現了其偉大的簡化開發的設計理念。
控制反轉(IOC)
IOC全稱為:Inversion of Control。控制反轉的基本概念是:不用創建對象,但是需要描述創建對象的方式。
簡單的說我們本來在代碼中創建一個對象是通過new關鍵字,而使用了Spring之後,我們不在需要自己去new一個對象了,而是直接通過容器裡面去取出來,再將其自動注入到我們需要的對象之中,即:依賴注入。
也就說創建對象的控制權不在我們程序員手上了,全部交由Spring進行管理,程序要只需要注入就可以了,所以才稱之為控制反轉
依賴注入(DI)
依賴注入(Dependency Injection,DI)就是Spring為了實現控制反轉的一種實現方式,所有有時候我們也將控制反轉直接稱之為依賴注入。
面向切面編程(AOP)
AOP全稱為:Aspect Oriented Programming。AOP是一種編程思想,其核心構造是方面(切面),即將那些影響多個類的公共行為封裝到可重用的模塊中,而使原本的模塊內只需關注自身的個性化行為。
AOP編程的常用場景有:Authentication(權限認證)、Auto Caching(自動緩存處理)、Error Handling(統一錯誤處理)、Debugging(調試信息輸出)、Logging(日誌記錄)、Transactions(事務處理)等。
利用 Spring 來完成 Hello World
最原生的Spring需要較多的配置文件,而SpringBoot省略了許多配置,相比較於原始的Spring又簡化了不少,在這裡我們就以SpringBoot為例來完成一個簡單的接口開發。
1、新建一個maven項目,pom文件中引入依賴(省略了少部分屬性):
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.0</version><relativePath/></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies>
2、新建一個HelloController類:
packagecom.lonely.wolf.note.springboot.demo;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/hello")publicclassHelloController{@GetMapping("/demo")publicStringhelloWorld(Stringname){return"Hello:"+name;}}
3、最後新建一個SpringBoot啟動類:
packagecom.lonely.wolf.note.springboot;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication(scanBasePackages="com.lonely.wolf.note.springboot")classMySpringBootApplication{publicstaticvoidmain(String[]args){SpringApplication.run(MySpringBootApplication.class,args);}}
4、現在就可以輸入測試路徑:http://localhost:8080/hello/demo?name=雙子孤狼進行測試,正常輸出:Hello:雙子孤狼。
我們可以看到,利用SpringBoot來完成一個簡單的應用開發非常簡單,可以不需要任何配置完成一個簡單的應用,這是因為SpringBoot內部已經做好了約定(約定優於配置思想),包括容器Tomcat都被默認集成,所以我們不需要任何配置文件就可以完成一個簡單的demo應用。
假如沒有了 Spring
通過上面的例子我們可以發現,利用Spring來完成一個Hello World非常簡單,但是假如沒有了Spring,我們又該如何完成這樣的一個Hello World接口呢?
基於 Servlet 開發
在還沒有框架之前,編程式基於原始的Servlet進行開發,下面我們就基於原生的Servlet來完成一個簡單的接口調用。
1、pom文件引入依賴,需要注意的是,package屬性要設置成war包,為了節省篇幅,這裡沒有列出pom完整的信息:
<packaging>war</packaging><dependencies><dependency><groupId>javax.servlet</groupId><artifactId>servlet-api</artifactId><version>2.4</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.7</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.72</version></dependency></dependencies>
2、在src/main下面新建文件夾webapp/WEB-INF,然後在WEB-INF下面新建一個web.xml文件:
<?xmlversion="1.0"encoding="UTF-8"?><web-appxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://java.sun.com/xml/ns/j2ee"xmlns:javaee="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/j2eehttp://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"version="2.4"><display-name>LonelyWolfWebApplication</display-name><servlet><servlet-name>helloServlet</servlet-name><servlet-class>com.lonely.wolf.mini.spring.servlet.HelloServlet</servlet-class></servlet><servlet-mapping><servlet-name>helloServlet</servlet-name><url-pattern>/hello/*</url-pattern></servlet-mapping></web-app>
這裡面定義了selvlet和servlet-mapping兩個標籤,這兩個標籤必須一一對應,上面的標籤定義了servlet的位置,而下面的servlet-mapping文件定義了路徑的映射,這兩個標籤通過servlet-name標籤對應。
3、新建一個HelloServlet類繼承HttpServlet:
packagecom.lonely.wolf.mini.spring.servlet;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServlet;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;/***原始Servlet接口編寫,一般需要實現GET和POST方法,其他方法可以視具體情況選擇性繼承*/publicclassHelloServletextendsHttpServlet{@OverrideprotectedvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{this.doPost(request,response);}@OverrideprotectedvoiddoPost(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{response.setContentType("text/html;charset=utf-8");response.getWriter().write("Hello:"+request.getParameter("name"));}}
4、執行maven打包命令,確認成功打包成war包:

img
5、RUN-->Edit Configurations,然後點擊左上角的+號,新建一個Tomcat Server,如果是第一次配置,默認沒有Tomcat Server選項,需要點擊底部的xx more items...:

img
6、點擊右邊的Deployment,然後按照下圖依次點擊,最後在彈框內找到上面打包好的war包文件:

img
7、選中之後,需要注意的是,下面Application Context默認會帶上war包名,為了方便,我們需要把它刪掉,即不用上下文路徑,只保留一個根路徑/(當然上下文也可以保留,但是每次請求都要帶上這一部分), 再選擇Apply,點擊OK,即可完成部署:

img
8、最後我們在瀏覽器輸入請求路徑http://localhost:8080/hello?name=雙子孤狼,即可得到返回:Hello:雙子孤狼。
上面我們就完成了一個簡單的 基於Servlet的接口開發,可以看到,配置非常麻煩,每增加一個Servlet都需要增加對應的配置,所以才會有許多框架的出現來幫我們簡化開發,比如原來很流行的Struts2框架,當然現在除了一些比較老的項目,一般我們都很少使用,而更多的是選擇Spring框架來進行開發。
模仿 Spring
Spring的源碼體系非常龐大,大部分人對其源碼都敬而遠之。確實,Spring畢竟經過了這麼多年的迭代,功能豐富,項目龐大,不是一下子就能看懂的。雖然Spring難以理解,但是其最核心的思想仍然是我們上面介紹的幾點,接下來就基於Spring最核心的部分來模擬,自己動手實現一個超級迷你版本的Spring(此版本並不包含AOP功能)。
1、pom依賴和上面保持不變,然後web.xml作如下改變,這裡會攔截所有的接口/*,然後多配置了一個參數,這個參數其實也是為了更形象的模擬Spring:
<servlet><servlet-name>myDispatcherServlet</servlet-name><servlet-class>com.lonely.wolf.mini.spring.v1.MyDispatcherServlet</servlet-class><init-param><param-name>defaultConfig</param-name><param-value>application.properties</param-value></init-param></servlet><servlet-mapping><servlet-name>myDispatcherServlet</servlet-name><url-pattern>/*</url-pattern></servlet-mapping>
2、在respurces下面新建一個配置文件application.properties,用來定義掃描的基本路徑:
basePackages=com.lonely.wolf.mini.spring
3、創建一些相關的註解類:
packagecom.lonely.wolf.mini.spring.annotation;importjava.lang.annotation.*;@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceWolfAutowired{Stringvalue()default"";}packagecom.lonely.wolf.mini.spring.annotation;importjava.lang.annotation.*;@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceWolfController{Stringvalue()default"";}packagecom.lonely.wolf.mini.spring.annotation;importjava.lang.annotation.*;@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceWolfGetMapping{Stringvalue()default"";}packagecom.lonely.wolf.mini.spring.annotation;importjava.lang.annotation.*;@Target({ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceWolfRequestParam{Stringvalue()default"";}packagecom.lonely.wolf.mini.spring.annotation;importjava.lang.annotation.*;@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceWolfService{Stringvalue()default"";}
4、這個時候最核心的邏輯就是MyDispatcherServlet類了:
packagecom.lonely.wolf.mini.spring.v1;importcom.lonely.wolf.mini.spring.annotation.*;importcom.lonely.wolf.mini.spring.v1.config.MyConfig;importorg.apache.commons.lang3.StringUtils;importjavax.servlet.ServletConfig;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServlet;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.File;importjava.io.IOException;importjava.io.InputStream;importjava.lang.annotation.Annotation;importjava.lang.reflect.Field;importjava.lang.reflect.Method;importjava.net.URL;importjava.util.*;publicclassMyDispatcherServletextendsHttpServlet{privateMyConfigmyConfig=newMyConfig();privateList<String>classNameList=newArrayList<String>();privateMap<String,Object>iocContainerMap=newHashMap<>();privateMap<String,HandlerMapping>handlerMappingMap=newHashMap<>();@OverrideprotectedvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{this.doPost(request,response);}@OverrideprotectedvoiddoPost(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{try{this.doDispatch(request,response);}catch(Exceptione){e.printStackTrace();}}privatevoiddoDispatch(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{StringrequestUrl=this.formatUrl(request.getRequestURI());HandlerMappinghandlerMapping=handlerMappingMap.get(requestUrl);if(null==handlerMapping){response.getWriter().write("404NotFound");return;}//獲取方法中的參數類型Class<?>[]paramTypeArr=handlerMapping.getMethod().getParameterTypes();Object[]paramArr=newObject[paramTypeArr.length];for(inti=0;i<paramTypeArr.length;i++){Class<?>clazz=paramTypeArr[i];//參數只考慮三種類型,其他不考慮if(clazz==HttpServletRequest.class){paramArr[i]=request;}elseif(clazz==HttpServletResponse.class){paramArr[i]=response;}elseif(clazz==String.class){Map<Integer,String>methodParam=handlerMapping.getMethodParams();paramArr[i]=request.getParameter(methodParam.get(i));}else{System.out.println("暫不支持的參數類型");}}//反射調用controller方法handlerMapping.getMethod().invoke(handlerMapping.getTarget(),paramArr);}privateStringformatUrl(StringrequestUrl){requestUrl=requestUrl.replaceAll("/+","/");if(requestUrl.lastIndexOf("/")==requestUrl.length()-1){requestUrl=requestUrl.substring(0,requestUrl.length()-1);}returnrequestUrl;}@Overridepublicvoidinit(ServletConfigconfig)throwsServletException{//1.加載配置文件try{doLoadConfig(config.getInitParameter("defaultConfig"));}catch(Exceptione){System.out.println("加載配置文件失敗");return;}//2.根據獲取到的掃描路徑進行掃描doScanPacakge(myConfig.getBasePackages());//3.將掃描到的類進行初始化,並存放到IOC容器doInitializedClass();//4.依賴注入doDependencyInjection();System.out.println("DispatchServletInitEnd...");}privatevoiddoDependencyInjection(){if(iocContainerMap.size()==0){return;}//循環IOC容器中的類Iterator<Map.Entry<String,Object>>iterator=iocContainerMap.entrySet().iterator();while(iterator.hasNext()){Map.Entry<String,Object>entry=iterator.next();Class<?>clazz=entry.getValue().getClass();Field[]fields=clazz.getDeclaredFields();//屬性注入for(Fieldfield:fields){//如果屬性有WolfAutowired註解則注入值(暫時不考慮其他註解)if(field.isAnnotationPresent(WolfAutowired.class)){Stringvalue=toLowerFirstLetterCase(field.getType().getSimpleName());//默認bean的value為類名首字母小寫if(field.getType().isAnnotationPresent(WolfService.class)){WolfServicewolfService=field.getType().getAnnotation(WolfService.class);value=wolfService.value();}field.setAccessible(true);try{Objecttarget=iocContainerMap.get(beanName);if(null==target){System.out.println(clazz.getName()+"requiredbean:"+beanName+",butwenotfoundit");}field.set(entry.getValue(),iocContainerMap.get(beanName));//初始化對象,後面注入}catch(IllegalAccessExceptione){e.printStackTrace();}}}//初始化HanderMappingStringrequestUrl="";//獲取Controller類上的請求路徑if(clazz.isAnnotationPresent(WolfController.class)){requestUrl=clazz.getAnnotation(WolfController.class).value();}//循環類中的方法,獲取方法上的路徑Method[]methods=clazz.getMethods();for(Methodmethod:methods){//假設只有WolfGetMapping這一種註解if(!method.isAnnotationPresent(WolfGetMapping.class)){continue;}WolfGetMappingwolfGetMapping=method.getDeclaredAnnotation(WolfGetMapping.class);requestUrl=requestUrl+"/"+wolfGetMapping.value();//拼成完成的請求路徑//不考慮正則匹配路徑/xx/*的情況,只考慮完全匹配的情況if(handlerMappingMap.containsKey(requestUrl)){System.out.println("重複路徑");continue;}Annotation[][]annotationArr=method.getParameterAnnotations();//獲取方法中參數的註解Map<Integer,String>methodParam=newHashMap<>();//存儲參數的順序和參數名retryParam:for(inti=0;i<annotationArr.length;i++){for(Annotationannotation:annotationArr[i]){if(annotationinstanceofWolfRequestParam){WolfRequestParamwolfRequestParam=(WolfRequestParam)annotation;methodParam.put(i,wolfRequestParam.value());//存儲參數的位置和註解中定義的參數名continueretryParam;}}}requestUrl=this.formatUrl(requestUrl);//主要是防止路徑多了/導致路徑匹配不上HandlerMappinghandlerMapping=newHandlerMapping();handlerMapping.setRequestUrl(requestUrl);//請求路徑handlerMapping.setMethod(method);//請求方法handlerMapping.setTarget(entry.getValue());//請求方法所在controller對象handlerMapping.setMethodParams(methodParam);//請求方法的參數信息handlerMappingMap.put(requestUrl,handlerMapping);//存入hashmap}}}/***初始化類,並放入容器iocContainerMap內*/privatevoiddoInitializedClass(){if(classNameList.isEmpty()){return;}for(StringclassName:classNameList){if(StringUtils.isEmpty(className)){continue;}Classclazz;try{clazz=Class.forName(className);//反射獲取對象if(clazz.isAnnotationPresent(WolfController.class)){Stringvalue=((WolfController)clazz.getAnnotation(WolfController.class)).value();//如果直接指定了value則取value,否則取首字母小寫類名作為key值存儲類的實例對象iocContainerMap.put(StringUtils.isBlank(value)?toLowerFirstLetterCase(clazz.getSimpleName()):value,clazz.newInstance());}elseif(clazz.isAnnotationPresent(WolfService.class)){Stringvalue=((WolfService)clazz.getAnnotation(WolfService.class)).value();iocContainerMap.put(StringUtils.isBlank(value)?toLowerFirstLetterCase(clazz.getSimpleName()):value,clazz.newInstance());}else{System.out.println("不考慮其他註解的情況");}}catch(Exceptione){e.printStackTrace();System.out.println("初始化類失敗,className為"+className);}}}/***將首字母轉換為小寫*@paramclassName*@return*/privateStringtoLowerFirstLetterCase(StringclassName){if(StringUtils.isBlank(className)){return"";}StringfirstLetter=className.substring(0,1);returnfirstLetter.toLowerCase()+className.substring(1);}/***掃描包下所有文件獲取全限定類名*@parambasePackages*/privatevoiddoScanPacakge(StringbasePackages){if(StringUtils.isBlank(basePackages)){return;}//把包名的.替換為/StringscanPath="/"+basePackages.replaceAll("\\.","/");URLurl=this.getClass().getClassLoader().getResource(scanPath);//獲取到當前包所在磁盤的全路徑Filefiles=newFile(url.getFile());//獲取當前路徑下所有文件for(Filefile:files.listFiles()){//開始掃描路徑下的所有文件if(file.isDirectory()){//如果是文件夾則遞歸doScanPacakge(basePackages+"."+file.getName());}else{//如果是文件則添加到集合。因為上面是通過類加載器獲取到的文件路徑,所以實際上是class文件所在路徑classNameList.add(basePackages+"."+file.getName().replace(".class",""));}}}/***加載配置文件*@paramconfigPath-配置文件所在路徑*/privatevoiddoLoadConfig(StringconfigPath){InputStreaminputStream=this.getClass().getClassLoader().getResourceAsStream(configPath);Propertiesproperties=newProperties();try{properties.load(inputStream);}catch(IOExceptione){e.printStackTrace();System.out.println("加載配置文件失敗");}properties.forEach((k,v)->{try{Fieldfield=myConfig.getClass().getDeclaredField((String)k);field.setAccessible(true);field.set(myConfig,v);}catch(Exceptione){e.printStackTrace();System.out.println("初始化配置類失敗");return;}});}}
5、這個Servlet相比較於上面的HelloServlet多了一個init方法,這個方法中主要做了以下幾件事情:
(1)初始化配置文件,拿到配置文件中配置的參數信息(對應方法:doLoadConfig)。
(2)拿到第1步加載出來的配置文件,獲取到需要掃描的包路徑,然後將包路徑進行轉換成實際的磁盤路徑,並開始遍歷磁盤路徑下的所有class文件,最終經過轉換之後得到掃描路徑下的所有類的全限定類型,存儲到全局變量classNameList中(對應方法:doScanPacakge)。
(3)根據第2步中得到的全局變量classNameList中的類通過反射進行初始化(需要注意的是只會初始化加了指定註解的類)並將得到的對應關係存儲到全局變量iocContainerMap中(即傳說中的IOC容器),其中key值為註解中的value屬性,如value屬性為空,則默認取首字母小寫的類名作為key值進行存儲(對應方法:doInitializedClass)。
(4)這一步比較關鍵,需要對IOC容器中的所有類的屬性進行賦值並且需要對Controller中的請求路徑進行映射存儲,為了確保最後能順利調用Controller中的方法,還需要將方法的參數進行存儲 。對屬性進行映射時只會對加了註解的屬性進行映射,映射時會從IOC容器中取出第3步中已經初始化的實例對象進行賦值,最後將請求路徑和Controller中方法的映射關係存入變量handlerMappingMap,key值為請求路徑,value為方法的相關信息 (對應方法:doDependencyInjection)。
6、存儲請求路徑和方法的映射關係時,需要用到HandlerMapping類來進行存儲:
packagecom.lonely.wolf.mini.spring.v1;importjava.lang.reflect.Method;importjava.util.Map;//省略了getter/setter方法publicclassHandlerMapping{privateStringrequestUrl;privateObjecttarget;//保存方法對應的實例privateMethodmethod;//保存映射的方法privateMap<Integer,String>methodParams;//記錄方法參數}
7、初始化完成之後,因為攔截了/*,所以調用任意接口都會進入MyDispatcherServlet,而且最終都會執行方法doDispatch,執行這個方法時會拿到請求的路徑,然後和全局變量handlerMappingMap進行匹配,匹配不上則返回404,匹配的上則取出必要的參數進行賦值,最後通過反射調用到Controller中的相關方法。
8、新建一個HelloController和HelloService來進行測試:
packagecom.lonely.wolf.mini.spring.controller;importcom.lonely.wolf.mini.spring.annotation.WolfAutowired;importcom.lonely.wolf.mini.spring.annotation.WolfController;importcom.lonely.wolf.mini.spring.annotation.WolfGetMapping;importcom.lonely.wolf.mini.spring.annotation.WolfRequestParam;importcom.lonely.wolf.mini.spring.service.HelloService;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;@WolfControllerpublicclassHelloController{@WolfAutowiredprivateHelloServicehelloService;@WolfGetMapping("/hello")publicvoidquery(HttpServletRequestrequest,HttpServletResponseresponse,@WolfRequestParam("name")Stringname)throwsIOException{response.setContentType("text/html;charset=utf-8");response.getWriter().write("Hello:"+name);}}packagecom.lonely.wolf.mini.spring.service;importcom.lonely.wolf.mini.spring.annotation.WolfService;@WolfService(value="hello_service")//為了演示能否正常取value屬性publicclassHelloService{}
9、輸入測試路徑:http://localhost:8080////hello?name=雙子孤狼, 進行測試發現可以正常輸出:Hello:雙子孤狼。
上面這個例子只是一個簡單的演示,通過這個例子只是希望在沒有任何框架的情況下,我們也能知道如何完成一個簡單的應用開發。例子中很多細節都沒有進行處理,僅僅只是為了體驗一下Spring的核心思想,並了解Spring到底幫助我們做了什麼,實際上Spring能幫我們做的事情遠比這個例子中多得多,Spring體系龐大,設計優雅,經過了多年的迭代優化,是一款非常值得研究的框架。
總結
本文從介紹Spring核心功能開始入手,從如何利用Spring完成一個應用開發,講述到假如沒有Spring我們該如何基於Servlet進行開發,最後再通過一個簡單的例子體驗了Spring的核心思想。
END
關注後端面試那些事,回復【2022面經】
獲取最新大廠Java面經

