前言
磕磕碰碰,終於開始翻看spring源碼了。spring使用已經很多,之前一直想學習spring源碼,但是一直沒有開始。這篇博客開始學習spring源碼,手動實現一個簡單的基於servlet的mvc框架。
廣義的spring mvc調用流程
這個之前我們看過了很多資料,但是一直沒有真實理解,畢竟沒有一個實際的感受。MVC調用具體流程圖如下(個人理解)
要幹什麼
簡單點說,手寫一個mvc調用框架(超級簡單的版本)。更通俗點說,用代碼實現上述流程圖。
初始工作準備
新建一個web項目,這一步就不詳細說了。
1、模仿編寫controller,requestMapping,autowired,requestParam,service註解
自己的SelfController註解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfController {
String value() default "";
}
自己的RequestMapping註解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfRequestMapping {
String value() default "";
}
自己的RequestParam註解
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfRequestParam {
String value() default "";
}
自己的Autowired註解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfAutowired {
String value() default "";
}
自己的service註解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelfService {
String value() default "";
}
2、編寫自己的servlet
package com.learn.springmvc.servlet.V2;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* autor:liman
* comment: 自己的servlet
*/
public class SelfServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
/**
* 初始化方法
*
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
}
}
3、web.xml中配置servlet的映射
<servlet>
<servlet-name>selfServlet</servlet-name>
<!--<servlet-class>com.learn.springmvc.servlet.SelfServlet</servlet-class>-->
<servlet-class>com.learn.springmvc.servlet.V2.SelfServletV2</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>selfServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
其中的init-param是給servlet一個初始化的參數,這個後面會詳細談到。
開始有點複雜的代碼編寫
Servlet中的init方法中,會完成HandlerMapping,IOC相關內容的初始化,具體如下所示。
public void init(ServletConfig config) throws ServletException {
//1.加載配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2.掃描相關的類
doScanner(contextConfig.getProperty("scanPackage"));
//3.初始化掃描的類
doInstance();
//4.完成依賴注入
doAutowired();
//5.初始化handleMapping
initHandlerMapping();
System.out.println("自己寫的spring mvc 初始化完成");
}
1、加載配置文件
加載配置文件,在這裏面是最簡單的操作,只是將配置文件讀取到內存的Properties對象中即可。
//用於保存application.properties配置文件中的內容,這個實例使用properties文件代替xml文件
private Properties contextConfig = new Properties();
/**
* 加載配置文件
*/
private void doLoadConfig(String contextConfigLocation) {
InputStream fis = null;
fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(fis);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
這裏只是簡單的一個mvc簡單的示例,配置文件中只有一項配置:
scanPackage=com.learn.springmvc
上述的工作只是將該配置屬性讀取到屬性contextConfig中。該配置中指定了我們需要掃描的基礎包名
2、掃描出相關的類
//用於保存所有掃描出來的類名
private List<String> classNames = new ArrayList<String>();
/**
* 掃描出相關的類
*
* @param scanPackage
*/
private void doScanner(String scanPackage) {
//主要是將包路徑轉換爲文件路徑
URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classPath = new File(url.getFile());
for (File file : classPath.listFiles()) {
if (file.isDirectory()) {
doScanner(scanPackage + "." + file.getName());
} else {
if (!file.getName().endsWith(".class")) {
continue;
}
String className = (scanPackage + "." + file.getName().replace(".class", ""));
classNames.add(className);
}
}
}
掃描出所有的類名,將類名放入到List<String> 集合中。
3、初始化掃描的類
//傳說中的IOC容器,爲了簡化程序,這裏不用ConcurrentHashMap
private Map<String, Object> ioc = new HashMap<String, Object>();
/**
* 初始化,爲DI做準備
* 加了註解的類才能初始化,這裏只列出加了@Controller和@Service註解的類
*/
private void doInstance() {
if (classNames.isEmpty()) {
return;
}
try {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
//如果有Controller註解
if (clazz.isAnnotationPresent(SelfController.class)) {//如果是Controller
Object instance = clazz.newInstance();
//首字母小寫,並放入ioc容器中
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName, instance);
} else if (clazz.isAnnotationPresent(SelfService.class)) {//如果有Service註解
//獲取自定義的beanName——@Service("test")獲取其中的test
SelfService service = clazz.getAnnotation(SelfService.class);
String beanName = service.value();//獲得註解的值(一般自己制定service的名稱的時候)
if ("".equals(beanName.trim())) {
//如果沒有指定名稱,service 默認首字母小寫
beanName = toLowerFirstCase(clazz.getSimpleName());
}
Object instance = clazz.newInstance();
//放入IOC容器
ioc.put(beanName, instance);
//注入的時候是接口注入的方式,投機取巧,就將接口作爲key,實例作爲值
for (Class<?> i : clazz.getInterfaces()) {
if (ioc.containsKey(i.getName())) {//這樣就限制了,一個接口只有一個實現類
throw new Exception("the " + i.getName() + " is exists");
}
ioc.put(i.getName(), instance);
}
} else {
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 將首字母小寫
*
* @param simpleName
* @return
*/
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
//之所以加,是因爲大小寫字母的ASCII碼相差32,
// 而且大寫字母的ASCII碼要小於小寫字母的ASCII碼
//在Java中,對char做算學運算,實際上就是對ASCII碼做算學運算
chars[0] += 32;
return String.valueOf(chars);
}
這一步完成了IOC容器的初始化,這一步也算是瞭解了IOC的基本結構 ——一個類名與類實例的鍵值對。
4、完成依賴注入(DI)
/**
* 完成依賴注入
*/
private void doAutowired() {
if (ioc.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
//獲取本類中所有的字段
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(SelfAutowired.class)) {
continue;
}
//獲取有@Autowired註解的屬性上的註解對象
SelfAutowired selfAutowired = field.getAnnotation(SelfAutowired.class);
//獲取註解的值
String beanName = selfAutowired.value().trim();
if ("".equals(beanName)) {
//如果沒有自定義,默認根據類型注入
beanName = field.getType().getName();
}
//設置屬性的訪問屬性
field.setAccessible(true);
try {
//設置屬性的值
field.set(entry.getValue(), ioc.get(beanName));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
依賴注入其實也沒有想象中的複雜,只是遍歷IOC容器中的實體,並遍歷每個實體中的屬性,如果屬性上有註解,則從IOC中取出指定類型的實例,完成初始化。
5、 初始化HandlerMapping
這是最關鍵的一步,也是最複雜的一步,同時這一步也引出了Handler與HandlerMapping的關係,HandlerMapping其實就是一個映射,主要完成url與對應的controller中method的映射關係。
private List<HandlerMapping> handlerMapping = new ArrayList<HandlerMapping>();
/**
* 初始化HandlerMapping
* HandlerMapping——url和method一對一的映射
*/
private void initHandlerMapping() {
if (ioc.isEmpty()) {
return;
}
for(Map.Entry entry:ioc.entrySet()){
Class<?> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(SelfController.class)){
continue;
}
String baseUrl = "";
if(clazz.isAnnotationPresent(SelfRequestMapping.class)){
SelfRequestMapping selfRequestMapping = clazz.getAnnotation(SelfRequestMapping.class);
baseUrl = selfRequestMapping.value();
}
//默認獲取所有的public方法
for(Method method:clazz.getMethods()){
if(!method.isAnnotationPresent(SelfRequestMapping.class)){
continue;
}
SelfRequestMapping requestMapping = method.getAnnotation(SelfRequestMapping.class);
String url = ("/"+baseUrl+"/"+requestMapping.value()).replaceAll("/+","/");
this.handlerMapping.add(new HandlerMapping(url,method,entry.getValue()));
System.out.println("Mapped:"+url+":"+method);
}
}
}
爲了方便後面的操作,如果HandlerMapping中只有url屬性和Method屬性是遠遠不夠的,在實際編寫代碼過程中爲了方便通過反射獲取方法的參數,需要維護方法所在的類類型,因此最終確定的HandlerMapping的屬性如下:
/**
* 暫時將HandlerMapping寫成一個靜態內部類
*/
public static class HandlerMapping {
private String url;
private Method method;
private Object controller;
private Class<?>[] paramTypes;//參數類型列表
private Map<String, Integer> paramIndexMapping = new HashMap<>();//參數索引位置
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public Class<?>[] getParamTypes() {
return paramTypes;
}
/**
* 構造函數
*
* @param url
* @param method
* @param controller
*/
public HandlerMapping(String url, Method method, Object controller) {
this.url = url;
this.method = method;
this.controller = controller;
this.paramTypes = method.getParameterTypes();
putParamIndexMapping(method);
}
/**
* 初始化參數的索引位置
*
* @param method
* @return
*/
private void putParamIndexMapping(Method method) {
//提取方法中加了註解的參數
Annotation[][] pa = method.getParameterAnnotations();
for (int i = 0; i < pa.length; i++) {
for (Annotation a : pa[i]) {
if (a instanceof SelfRequestParam) {
String paramName = ((SelfRequestParam) a).value();
if (!"".equals(paramName.trim())) {
this.paramIndexMapping.put(paramName, i);
}
}
}
}
//提取方法中的request和response參數
Class<?>[] paramsTypes = method.getParameterTypes();
for (int i = 0; i < paramsTypes.length; i++) {
Class<?> type = paramsTypes[i];
if (type == HttpServletRequest.class
|| type == HttpServletResponse.class) {
this.paramIndexMapping.put(type.getName(), i);
}
}
}
}
paramIndexMapping中維護了參數與參數索引的對應關係。
/**
* 找到指定的一致的handlercMapping處理對象
* @param req
* @return
*/
private HandlerMapping getHandler(HttpServletRequest req){
if(handlerMapping.isEmpty()){
return null;
}
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath,"").replaceAll("/+","/");
for(HandlerMapping handler:this.handlerMapping){
if(handler.getUrl().equals(url)){
return handler;
}
}
return null;
}
上述其實就是從HandlerMapping集合中獲取指定的HandlerMapping。至此,上一步就完成Spring mvc的初始化,IOC容器,DI的注入,以及初始化HandlerMapping,接下來就差最後一步了,完成調用,Method對象的調用需要三個屬性:1、方法所在的對象,2、所有的參數值,3、對應的Method。
6、最後的調用邏輯
/**
* 開始調用自己寫的mvc框架
*
* @param req
* @param resp
*/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
//獲取指定的handler
HandlerMapping handler = getHandler(req);
if(handler == null){//沒有找到對應的處理器
resp.getWriter().write("404 Not Found!!!");
return;
}
//獲取方法的形參列表
Class<?> [] paramTypes = handler.getParamTypes();
Object [] paramValues = new Object[paramTypes.length];
Map<String,String[]> params= req.getParameterMap();
for(Map.Entry<String,String[]> param:params.entrySet()){
String value = Arrays.toString(param.getValue()).
replaceAll("\\[|\\]","").replaceAll("\\s",",");
if(!handler.paramIndexMapping.containsKey(param.getKey())){continue;}
int index = handler.paramIndexMapping.get(param.getKey());
paramValues[index] = convert(paramTypes[index],value);
}
if(handler.paramIndexMapping.containsKey(HttpServletRequest.class.getName())){
int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[reqIndex] = req;
}
if(handler.paramIndexMapping.containsKey(HttpServletResponse.class.getName())){
int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[respIndex] = resp;
}
//調用handler的方法,也就是目標方法
Object returnValue = handler.method.invoke(handler.controller,paramValues);
if(returnValue == null || returnValue instanceof Void){
return;
}
resp.getWriter().write(returnValue.toString());
}
走到最後一步,也就沒有什麼了,主要是組裝參數列表然後直接調用method對象的invoke方法即可。
7、servlet中的調用
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//6.調用
try {
doDispatch(req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
測試
對應的測試controller
package com.learn.springmvc.controller;
import com.learn.springmvc.Annotation.SelfAutowired;
import com.learn.springmvc.Annotation.SelfController;
import com.learn.springmvc.Annotation.SelfRequestMapping;
import com.learn.springmvc.Annotation.SelfRequestParam;
import com.learn.springmvc.service.IDemoService;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//雖然,用法一樣,但是沒有功能
@SelfController
@SelfRequestMapping("/demo")
public class DemoController {
@SelfAutowired
private IDemoService demoService;
@SelfRequestMapping("/query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@SelfRequestParam("name") String name){
String result = "My name is " + name;
try {
resp.getWriter().write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@SelfRequestMapping("/add")
public void add(HttpServletRequest req, HttpServletResponse resp,
@SelfRequestParam("a") Integer a, @SelfRequestParam("b") Integer b){
try {
resp.getWriter().write(a + "+" + b + "=" + (a + b));
} catch (IOException e) {
e.printStackTrace();
}
}
}
這個就是自己編寫的Controller,啓動之後在瀏覽器中輸入:localhost:8080/demo/add?name=test,可以得到以下結果
總結
在這個實例中反射用的非常多,無非就是利用反射獲取註解的值,然後利用反射獲取方法的參數列表,處理調用參數,整個過程不管如何總結,其實就是文章開頭的流程圖。每一步對應的就是指定的函數,但是函數中針對request和response都做了處理,顯得些許臃腫。代碼書寫完之後,博客整理的異常凌亂,一下沒理清整理的思路,具體源碼可以參見如下地址:一個簡單的mvc框架源碼地址