SpringMVC底層機制簡單實現-01 主要完成:核心分發控制器+Controller和Service註入容器+對象自動裝配+控制器方法獲取參數+視圖解析+返回JSON格式數據 1.搭建開發環境 創建 Maven 項目,File-New-Project-Maven 將 pom.xml 文件中的編譯 ...
SpringMVC底層機制簡單實現-01
主要完成:核心分發控制器+Controller和Service註入容器+對象自動裝配+控制器方法獲取參數+視圖解析+返回JSON格式數據
1.搭建開發環境
-
創建 Maven 項目,File-New-Project-Maven
-
將 pom.xml 文件中的編譯版本改為1.8
-
在 src 目錄下創建以下目錄:
java 代碼放在 java 目錄下,相關的資源文件放在 resource 目錄下,對 maven 的 web 項目而言,resource 就是類路徑。前端頁面放在 webapp 下,該目錄對應之前的 web 目錄。test/java 目錄用於存放測試文件,測試需要的資源文件放在 test/resource 目錄下。
-
在 pom.xml 中引入基本的 jar 包
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!--引入原生servlet依賴的jar包--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <!-- 1.scope表示引入jar包的作用範圍, 2.provided表示項目在打包放到生產環境時,不需要打上servlet-api.jar 3.因為 tomcat本身就有該jar包,使用tomcat的即可,防止版本衝突 --> <scope>provided</scope> </dependency> <!--引入dom4j,用於解析xml--> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <!--引入常用的工具類jar包,該jar含有很多常用的類--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.5</version> </dependency> </dependencies>
2.任務1-開發MyDispatcherServlet
說明:編寫 MyDispatcherServlet,充當原生的 DispatcherServlet(即核心控制器)
2.1分析
2.2代碼實現
-
創建 src/main/java/com/li/myspringmvc/servlet/MyDispatcherServlet.java,充當原生的前端控制器。
package com.li.myspringmvc.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author 李 * @version 1.0 * 1.MyDispatcherServlet 充當原生的 DispatcherServlet,它的本質就是一個Servlet * 因此繼承 HttpServlet */ public class MyDispatcherServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("MyDispatcherServlet doGet() 被調用.."); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("MyDispatcherServlet doPost() 被調用.."); } }
-
創建 src/main/resources/myspringmvc.xml,充當原生的 applicationContext-mvc.xml(即 spring 容器配置文件)
-
配置 src/main/webapp/WEB-INF/web.xml
<!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>MyDispatcherServlet</servlet-name> <servlet-class>com.li.myspringmvc.servlet.MyDispatcherServlet</servlet-class> <!--給前端控制器指定配置參數,指定要操作的spring容器文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:myspringmvc.xml</param-value> </init-param> <!--要求該對象在tomcat啟動時就自動載入--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>MyDispatcherServlet</servlet-name> <!--作為前端控制器,攔截所有請求--> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
-
配置 Tomcat,進行測試
-
瀏覽器訪問
http://localhost:8080/li_springmvc/aaa
3.任務2-實現客戶端/瀏覽器可以請求控制層
3.1分析
任務2的總目標是:
實現自己的 @Controller 註解和 @RequestMapping 註解,當瀏覽器訪問指定的 URL 時,由前端控制器,找到 Controller 的某個方法,然後通過 tomcat 將數據返回給瀏覽器。
3.2代碼實現
步驟一:兩個註解和測試Controller
(1)Controller 註解
package com.li.myspringmvc.annotation;
import java.lang.annotation.*;
/**
* @author 李
* @version 1.0
* 該註解用於標識一個控制器組件
* 1.@Target(ElementType.TYPE) 指定自定義註解可修飾的類型
* 2.@Retention(RetentionPolicy.RUNTIME) 作用範圍,RUNTIME使得可以通過反射獲取自定義註解
* 3.@Documented 在生成文檔時,可以看到自定義註解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
(2)RequestMapping 註解
package com.li.myspringmvc.annotation;
import java.lang.annotation.*;
/**
* @author 李
* @version 1.0
* RequestMapping 註解用於指定控制器-方法的映射路徑
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
(3)用於測試的控制器 MonsterController.java
package com.li.controller;
import com.li.myspringmvc.annotation.Controller;
import com.li.myspringmvc.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author 李
* @version 1.0
*/
@Controller
public class MonsterController {
//編寫方法,可以列出妖怪列表
//springmvc支持原生的servlet api,為了看到底層機制,這裡直接放入兩個參數
@RequestMapping(value = "/monster/list")
public void listMonster(HttpServletRequest request, HttpServletResponse response) {
//設置編碼
response.setContentType("text/html;charset=utf-8");
//獲取writer,返回提示信息
try {
PrintWriter printWriter = response.getWriter();
printWriter.print("<h1>妖怪列表信息</h1>");
} catch (IOException e) {
e.printStackTrace();
}
}
}
步驟二:配置容器文件 springmvc.xml,指定掃描的包
指定掃描的包是為了之後使用註解獲取需要反射的類
如果需要添加新的掃描包,在base-package添加包路徑,用逗號表示間隔。
<?xml version="1.0" encoding="utf-8" ?>
<beans>
<!--指定要掃描的包及其子包的java類-->
<component-scan base-package="com.li.controller,com.li.service"/>
</beans>
步驟三:編寫 XMLParse 工具類,用於解析 springmvc.xml,得到要掃描的包
package com.li.myspringmvc.xml;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
/**
* @author 李
* @version 1.0
* XMLParse用於解析spring配置文件
*/
public class XMLParse {
public static String getBasePackage(String xmlFile) {
SAXReader saxReader = new SAXReader();
//maven的類路徑是在target/li-springmvc/WEB-INF/classes/目錄下
//通過類的載入路徑-->獲取到spring配置文件[對應的資源流]
InputStream inputStream =
XMLParse.class.getClassLoader().getResourceAsStream(xmlFile);
try {
//得到配置文件的文檔
Document document = saxReader.read(inputStream);
Element rootElement = document.getRootElement();
Element componentScanElement = rootElement.element("component-scan");
Attribute attribute = componentScanElement.attribute("base-package");
String basePackage = attribute.getText();
return basePackage;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
步驟四:開發MyWebApplicationContext,充當原生Spring容器,得到掃描類的全路徑列表
即把指定目錄包括子目錄下的 java 類的全路徑掃描到 ArrayList 集合中,以便之後反射。是否需要反射,還要取決於類中是否添加了@Controller註解
(1)MyWebApplicationContext.java 實現自定義的 spring 容器,目前先完成掃描工作
package com.li.myspringmvc.context;
import com.li.myspringmvc.xml.XMLParse;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* @author 李
* @version 1.0
* MyWebApplicationContext 是我們自定義的spring容器
*/
public class MyWebApplicationContext {
//屬性classFullPathList用於保存掃描包/子包的類的全路徑
private List<String> classFullPathList = new ArrayList<>();
//該方法完成對自己的 spring容器的初始化
public void init() {
//返回的是我們在容器文件中配置的base-package的value
String basePackage = XMLParse.getBasePackage("myspringmvc.xml");
//這時你的 basePackage是像 com.li.controller,com.li.service 這樣子的
//通過逗號進行分割包
String[] basePackages = basePackage.split(",");
if (basePackages.length > 0) {
//遍歷這些包
for (String pack : basePackages) {
scanPackage(pack);
}
}
System.out.println("掃描後的路徑classFullPathList=" + classFullPathList);
}
/**
* 該方法完成對包的掃描
* @param pack 表示要掃描的包,如 "com.li.controller"
*/
public void scanPackage(String pack) {
//得到包所在的工作路徑[絕對路徑]
// (1)通過類的載入器,得到指定包的工作路徑[絕對路徑]
// (2)然後用斜杠代替點=>如 com.li.controller=>com/li/controller
URL url =
this.getClass().getClassLoader()
.getResource("/" + pack.replaceAll("\\.", "/"));
// url=file:/D:/IDEA-workspace/li-springmvc/target/li-springmvc
// /WEB-INF/classes/com/li/controller/
//System.out.println("url=" + url);
//根據得到的路徑,對其進行掃描,把類的全路徑保存到 classFullPathList屬性中
String path = url.getFile();
System.out.println("path=" + path);
//在io中,把目錄也視為一個文件
File dir = new File(path);
//遍歷 dir目錄,因為可能會有[多個文件/子目錄]
File[] files = dir.listFiles();
for (File file : files) {
if (file.isDirectory()) {
//如果是目錄,需要遞歸掃描
//pack加上下一級的目錄名繼續下一層的掃描
scanPackage(pack + "." + file.getName());
} else {
//這時得到的文件可能是.class文件,也可能是其他文件
//就算是class文件,還需要考慮是否要註入到容器的問題
//目前先把所有文件的全路徑都保存到集合中,後面註入對象到spring容器時再考慮過濾
String classFullPath =
pack + "." + file.getName().replaceAll(".class", "");
classFullPathList.add(classFullPath);
}
}
}
}
(2)通過 MyDispatcherServlet 前端控制器來調用並初始化 spring 容器
//添加init方法,用於初始化spring容器
@Override
public void init() throws ServletException {
MyWebApplicationContext myWebApplicationContext = new MyWebApplicationContext();
myWebApplicationContext.init();
}
(3)啟動tomcat,後臺成功獲取到了路徑,測試成功。
tomcat啟動--載入了MyDispatcherServlet--通過該Servlet的init()生命周期方法初始化自定義的 spring 容器,同時調用自定義 spring 容器的 init 方法去掃描包
步驟五:完善MyWebApplicationContext(自定義 spring 容器),實例化對象到容器中
將掃描到的類,在滿足添加了註解的情況下,通過反射註入到 ioc 容器
(1)部分代碼:在MyWebApplicationContext中添加新屬性 ioc 和新方法 executeInstance。
//定義屬性ioc,用於存放反射生成的 bean對象(單例的)
public ConcurrentHashMap<String, Object> ioc = new ConcurrentHashMap<>();
/**
* 該方法完成對自己的 spring容器的初始化
*/
public void init() {
//返回的是我們在容器文件中配置的base-package的value
String basePackage = XMLParse.getBasePackage("myspringmvc.xml");
//這時你的 basePackage是像 com.li.controller,com.li.service 這樣子的
//通過逗號進行分割包
String[] basePackages = basePackage.split(",");
if (basePackages.length > 0) {
//遍歷這些包
for (String pack : basePackages) {
scanPackage(pack);
}
}
System.out.println("掃描後的路徑classFullPathList=" + classFullPathList);
//將掃描到的類反射到ioc容器
executeInstance();
System.out.println("掃描後的ioc容器=" + ioc);
}
//...
/**
* 該方法將掃描到的類,在滿足條件的情況下進行反射,並放入到ioc容器中
*/
public void executeInstance() {
//是否掃描到了類
if (classFullPathList.size() == 0) {//沒有掃描到類
return;
}
//遍歷 classFullPathList,進行反射
try {
for (String classFullPath : classFullPathList) {
Class<?> clazz = Class.forName(classFullPath);
//判斷是否要進行反射(即是否添加了註解)
if (clazz.isAnnotationPresent(Controller.class)) {
Object instance = clazz.newInstance();
//獲取該對象的id,預設情況下為類名(首字母小寫)
String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase()
+ clazz.getSimpleName().substring(1);
String value = clazz.getAnnotation(Controller.class).value();
if (!"".equals(value)) {//如果註解的value指定了id
beanName = value;
}
ioc.put(beanName, instance);
}//如果有其他註解,可以進行擴展
}
} catch (Exception e) {
e.printStackTrace();
}
}
(2)啟動tomcat,反射成功。
步驟六:完成請求URL和控制器方法的映射關係