跟我一起造輪子 手寫springmvc
作為java程序員,項目中使用到的主流框架多多少少和spring有關聯,在面試的過程難免會問一些spring springmvc spring boot的東西,比如設計模式的使用、 怎麽實現springioc 怎麽實現springmvc諸如此類的問題,今天我們就來探尋spring mvc的實現,然後自己實現一個簡單的spring mvc
一. 了解spring mvc的基本運行流程
ps: 網上一大堆關於springmvc的詳細講解,在這裏就不累贅了
小結:spring mvc的核心是DispatcherServlet,DispatcherServlet繼承於HttpServlet,可以說spring mvc是基於Servlet的一個實現,DispatcherServlet負責協調和組織不同組件以完成請求處理並返回響應的工作,實現了MVC模式。
二. 梳理簡單SpringMVC的設計思路
1. 初始化容器
1.1 讀取配置文件
1.1.1.加載配置文件信息到DispatcherServlet
1.2 根據配置掃描包、初始化容器和組件
1.2.1.根據配置信息遞歸掃描包
1.2.2.把包下的類實例化 並且掃描註解
1.2.3.根據類的方法和註解,初始化HandlerMapping
2. 處理業務請求
2.1 處理請求業務
2.2.1 首先拿到請求URI
2.2.2 根據URI,在HandlerMapping中查找和URI對應的Handler
2.2.3 根據Handler裏面的method中的參數名稱和http中的請求參數匹配,填充method參數,反射調用
三. 沒時間解釋了,快上車
ps :環境基於maven idea tomat(端口8080) servlet
1.搭建一個基本web項目,並導入idea配置servlet 和javassist pom依賴 如下
創建命令: mvn archetype:generate -DgroupId=com.adminkk -DartifactId=adminkk-mvc -DpackageName=com.adminkk -Dversion=1.0
pom依賴
<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.0</modelVersion> <groupId>com.adminkk</groupId> <artifactId>adminkk-mvc</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> <packaging>war</packaging> <name>adminkk-mvc</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <!--servlet--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> <scope>provided</scope> </dependency> <!-- asm --> <dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3.1</version> </dependency> <!-- javassist --> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.23.1-GA</version> </dependency> </dependencies> </project>
2.創建mvc的註解 Controller RequestMapping 和統一異常處理類、方法參數工具類ParameterNameUtils
package com.adminkk.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Controller { public String value() default ""; public String description() default ""; }
package com.adminkk.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RequestMapping { public String value() default ""; public String method() default ""; public String description() default ""; }
package com.adminkk.exception; public final class MvcException extends RuntimeException{ public MvcException() { super(); } public MvcException(String message) { super(message); } }
package com.adminkk.tools; import javassist.*; import javassist.bytecode.CodeAttribute; import javassist.bytecode.LocalVariableAttribute; import javassist.bytecode.MethodInfo; import java.lang.reflect.Method; public final class ParameterNameUtils { public final static String[] getParameterNamesByJavassist(final Class<?> clazz, final Method method) { ClassPool pool = ClassPool.getDefault(); try { CtClass ctClass = pool.get(clazz.getName()); CtMethod ctMethod = ctClass.getDeclaredMethod(method.getName()); // 使用javassist的反射方法的參數名 MethodInfo methodInfo = ctMethod.getMethodInfo(); CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute .getAttribute(LocalVariableAttribute.tag); if (attr != null) { String[] rtv = new String[ctMethod.getParameterTypes().length]; int len = ctMethod.getParameterTypes().length; // 非靜態的成員函數的第一個參數是this int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1; for (int i = 0; i < len; i++) { rtv[i] = attr.variableName(i + pos); } return rtv; } } catch (NotFoundException e) { System.out.println("獲取異常"+ e.getMessage()); } return new String[0]; } }
3.創建 HandlerMapping類 主要是兩個方法 doInit初始化 doService處理請求 相關代碼如下
package com.adminkk.handler; import com.adminkk.scan.FileScaner; import com.adminkk.scan.Scaner; import com.adminkk.scan.XmlScaner; import com.adminkk.tools.ParameterNameUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public final class HandlerMapping { private static final Map<String,Handler> handlerMapping = new HashMap<String, Handler>(); private static final List<Scaner> scaners = new ArrayList<>(2); static { scaners.add(new XmlScaner()); scaners.add(new FileScaner()); } public static void scanPackage(String scanUrl) throws RuntimeException, IllegalAccessException, InstantiationException, ClassNotFoundException { for (Scaner scaner : scaners) { scaner.doScane(scanUrl); } } public static void doInit(String scanUrl) throws IllegalAccessException, ClassNotFoundException, InstantiationException { scanPackage(scanUrl); } public static void doService(HttpServletRequest request, HttpServletResponse response) { String requestURI = request.getRequestURI(); System.out.println("請求地址是="+ requestURI); Handler handler = handlerMapping.get(requestURI); if(handler == null){ System.out.println("請求地址是="+ requestURI+" 沒有配置改路徑"); return; } Method method = handler.getMethod(); Object instance = handler.getInstance(); response.setCharacterEncoding("UTF-8"); //response.setContentType("application/json; charset=utf-8"); PrintWriter writer = null; try { //這裏是簡單的解析 可以像springmvc那樣解析處理 Map<String, String[]> parameterMap = request.getParameterMap(); String[] parameters = ParameterNameUtils.getParameterNamesByJavassist(instance.getClass(),method); Object[] parameter = new Object[parameters.length]; if(parameters != null && parameters.length > 0){ for (int i = 0; i < parameters.length; i++) { final String simpleName = parameters[i]; StringBuilder parameterSb = new StringBuilder(); final String[] parameterStr = parameterMap.get(simpleName); if(parameterStr != null){ for (int j = 0; j < parameterStr.length; j++) { parameterSb.append(parameterStr[j]); } } parameter[i] = parameterSb.toString(); } } writer = response.getWriter(); String result = (String) method.invoke(instance,parameter); writer.print(result); } catch (Exception e) { e.printStackTrace(); System.out.println("請求地址是="+ requestURI+" 執行異常"); writer.print("業務執行異常"); }finally { writer.flush(); writer.close(); } } public static Handler addHandlerMapping(String url,Handler handler) { return handlerMapping.put(url,handler); } public static Handler getHandlerMapping(String url) { return handlerMapping.get(url); } }
掃描包
package com.adminkk.scan; public interface Scaner { void doScane(String scanUrl) throws IllegalAccessException, InstantiationException, ClassNotFoundException; }
package com.adminkk.scan;
import com.adminkk.exception.MvcException;
import com.adminkk.factory.BeanPostProcessor;
import com.adminkk.factory.MvcBeanPostProcessor;
import com.adminkk.factory.ServiceBeanPostProcessor;
import com.adminkk.handler.HandlerMapping;
import javassist.ClassClassPath;
import javassist.ClassPool;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public final class FileScaner implements Scaner{
public FileScaner() {
}
public static final List<BeanPostProcessor> beanPostProcessorList = new ArrayList<>();
static {
beanPostProcessorList.add(new MvcBeanPostProcessor());
beanPostProcessorList.add(new ServiceBeanPostProcessor());
}
@Override
public void doScane(String scanUrl) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
if(scanUrl == null || scanUrl.length() == 0){
throw new MvcException("容器基礎掃描路徑為空,請檢查參數配置");
}
String baseUrl = HandlerMapping.class.getResource("/").getPath();
String codeUrl = scanUrl.replaceAll("\\.", "/");
String path = baseUrl + codeUrl;
File file = new File(path);
if(file == null || !file.exists()){
throw new MvcException("找不到對應掃描路徑,請檢查參數配置");
}
recursionRedFile(scanUrl,file);
}
//遞歸讀取文件
private void recursionRedFile(String scanUrl,File file) throws MvcException, ClassNotFoundException, IllegalAccessException, InstantiationException {
if(!file.exists()){
return;
}
//讀取java文件
if(file.isFile()){
String beanName = scanUrl.replaceAll(".class","");
Class<?> forName = Class.forName(beanName);
//放到Javassist容器裏面
ClassPool pool = ClassPool.getDefault();
ClassClassPath classPath = new ClassClassPath(forName);
pool.insertClassPath(classPath);
if(forName.isAnnotation() || forName.isEnum() || forName.isInterface() ){
return;
}
Object newInstance = forName.newInstance();
//前置執行
for (int i = 0; i < beanPostProcessorList.size() ; i++) {
BeanPostProcessor beanPostProcessor = beanPostProcessorList.get(i);
beanPostProcessor.postProcessBeforeInitialization(newInstance,beanName);
}
//後置執行
for (int i = beanPostProcessorList.size()-1; i > 0 ; i++) {
BeanPostProcessor beanPostProcessor = beanPostProcessorList.get(i);
beanPostProcessor.postProcessAfterInitialization(newInstance,beanName);
}
return;
}
//文件夾下面的文件都遞歸處理
if(file.isDirectory()){
File[] files = file.listFiles();
if(files != null && files.length >0){
for (int i = 0; i < files.length; i++) {
File targetFile = files[i];
recursionRedFile(scanUrl+"."+targetFile.getName(),targetFile);
}
}
}
}
}
package com.adminkk.scan; public final class XmlScaner implements Scaner{ public XmlScaner() { } @Override public void doScane(String scanUrl) { //可自行擴展 } }
掃描bean
package com.adminkk.factory; import com.adminkk.exception.MvcException; public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object object, String beanName) throws MvcException; Object postProcessAfterInitialization(Object object, String beanName) throws MvcException; }
package com.adminkk.factory; import com.adminkk.annotation.Controller; import com.adminkk.annotation.RequestMapping; import com.adminkk.exception.MvcException; import com.adminkk.handler.Handler; import com.adminkk.handler.HandlerMapping; import java.lang.reflect.Method; public class MvcBeanPostProcessor implements BeanPostProcessor{ //掃描Controller業務 @Override public Object postProcessBeforeInitialization(Object object, String beanName) throws MvcException { Class<?> objectClass = object.getClass(); if(objectClass.getAnnotation(Controller.class) != null){ RequestMapping calssRequestMappingAnnotation = objectClass.getAnnotation(RequestMapping.class); StringBuilder urlSb = new StringBuilder(); if(calssRequestMappingAnnotation != null){ urlSb.append(calssRequestMappingAnnotation.value()); } Method[] methods = objectClass.getMethods(); if(methods != null && methods.length > 0 ){ for (int i = 0; i < methods.length; i++) { Method method = methods[i]; RequestMapping methodAnnotation = method.getAnnotation(RequestMapping.class); if(methodAnnotation != null){ String methodValue = methodAnnotation.value(); String url = new StringBuilder().append(urlSb).append(methodValue).toString(); Handler handler = HandlerMapping.getHandlerMapping(url); if(handler == null){ handler = new Handler(); handler.setMethod(method); handler.setInstance(object); HandlerMapping.addHandlerMapping(url,handler); }else { throw new MvcException("請求路徑"+ url + "已經存在容器中"); } } } } } return object; } @Override public Object postProcessAfterInitialization(Object object, String beanName) throws MvcException { return null; } }
package com.adminkk.factory; import com.adminkk.exception.MvcException; public class ServiceBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object object, String beanName) throws MvcException { //可自行擴展 return null; } @Override public Object postProcessAfterInitialization(Object object, String beanName) throws MvcException { //可自行擴展 return null; } }
5.創建 DispatcherServlet
package com.adminkk.servlet; import com.adminkk.handler.HandlerMapping; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet(name = "DispatcherServlet",loadOnStartup=1,urlPatterns={"/"}) public final class DispatcherServlet extends HttpServlet { public static final String BASE_SCAN_URL = "com.adminkk"; //初始化容器 @Override public void init() throws ServletException { doInit(); } //處理業務請求 @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doService(req,resp); } private void doService(HttpServletRequest req, HttpServletResponse resp) throws ServletException { try { HandlerMapping.doService(req,resp); }catch (Exception e){ e.printStackTrace(); throw new ServletException(e.getMessage()); } } private void doInit() throws ServletException { try { HandlerMapping.doInit(this.BASE_SCAN_URL); }catch (Exception e){ e.printStackTrace(); throw new ServletException(e.getMessage()); } } }
好了,目前為止我們就寫好了簡版的springmvc 下面開始測試
package com.adminkk.controller; import com.adminkk.annotation.Controller; import com.adminkk.annotation.RequestMapping; @Controller @RequestMapping("/mvc") public class MvcController { @RequestMapping("/index") public String index(){ return "adminkk-mvc system is running"; } @RequestMapping("/arg") public String parameter(String argOne, String argTwo){ return "argOne = " + argOne + " argTwo = " + argTwo; } }
訪問地址 http://localhost:8080/mvc/index
訪問地址: http://localhost:8080/mvc/arg?argOne=argOne&argTwo=argTwo
總結:整體實現簡單的springmvc,設計上還可以擴展更多,難點在於method 獲取方法上的參數名稱,由於jdk1.8以前是不支持的,需要借用第三方工具 比如 asm javassist黑科技工具包來幫助實現,spring-core使用的是LocalVariableTableParameterNameDiscoverer底層是調用asm,我們這裏使用的是javassist。延用這套思路還可以和spring項目結合,寫一個 基於spring的springmvc項目
源代碼 : https://gitee.com/chenchenche/mvc
寫博客不容易,希望大家多多提建議
下一篇預告 跟我一起造輪子 手寫分布式im系統(上)
跟我一起造輪子 手寫springmvc