版權聲明:本篇博客大部分代碼引用於公眾號:java團長,我只是在作者基礎上稍微修改一些內容,內容僅供學習與參考
前言:目前mvc框架經過大浪淘沙,由最初的struts1到struts2,到目前的主流框架SpringMvc,並逐漸區域占領市場主流穩定狀態,由於其背后強大的Spring家族提供了一系列高可用的組件和服務,SpringMvc在短時間內肯定是無法被超越的。公司里開發的項目第一首先框架就是SpringMvc,在市場上如火如荼,甚囂塵上。今天我們就來自己手動實現一個簡歷的閹割版SpringMvc,主要的目的就是體會SpringMvc的設計思路和理念,了解其幕后工作流程,當然需要注意的是本次實現的是簡易版,代碼不超過500行,所以本篇博客姑且只可領會設計思路,拋磚引玉,不可陷入細枝末節、吹毛求疵的死胡同里
本篇博客的目錄
一:springMvc的使用方法
二:項目開發結構
三:簡易版springmvc的實現
四:總結
一:springMvc的使用方法
1.1:開發基本過程
springmvc開發過程是典型的mvc模式,首先在xml里配置dispatcherServlet(假如沒有用到springboot的話),然后再配置掃描包,注解驅動配置,在代碼層面又用Controller上注解@RequestMapping映射請求,請求進入后,方法中用各種注解比如@pathvarible獲得url請求參數,再到業務方法中的調用service,service再調用dao層,實現數據庫的增刪改查的業務性操作,之后再返回視圖或者用@responseBoby修飾的返回數據,這已經成為一種典型的調用和開發模式!
1.2:基本原理
首先是xml配置,springmvc需要讀取xml里面的配置,然后放在簡單的一個集合數據模型里(Map),再然后就是裝載用戶的配置,掃描基准包,獲取用戶添加的注解,獲取注解信息和配置的參數,再用spring的IOC初始化配置的Bean實例,自動注入類實例(比如contoller的依賴service、dao層mappeer等,等容器啟動起來,如果有請求進來,就會按照路徑映射對應的controller,再選擇方法進行處理完成一系列操作后返回給客戶端。
二:項目開發結構
2.1:項目基本結構
這是一個簡易版的springmvc,其中包含了基本的使用注解自己實現,和后台的默認控制器DispatcherServlet,還有解析xml的工具,和默認xml配置資源,我們使用的serlvet是3.1版本,采用了對基本servlet進行了二次封裝,以下是基本的代碼:
2.2:具體的開發過程示例
@MyRequestMapping(name = "/orderApi") @MyController(name = "order") public class OrderController { @MyQualifer(name = "orderServiceImpl") private OrderServiceImpl orderServiceImPl; @MyRequestMapping(name = "/create") public void create() { orderServiceImPl.createOrder(); } }
@MyRepository(name = "orderDao")
public class OrderDao {
public void insert() {
System.out.println("數據庫中插入一條記錄");
}
}
public interface OrderServiceEx {
public void createOrder();
}
@MyService(name = "orderServiceImpl")
public class OrderServiceImpl implements OrderServiceEx {
@MyQualifer(name = "orderDao")
private OrderDao orderDao;
public void createOrder() {
}
}
三:簡易版springmvc的實現
3.1:讀取xml配置
解析xml采用的是dom4j的1.6.1版本,目的是模仿springmvc解析用戶的配置文件,讀取用戶配置的掃描包,以下是xmlUtil的源碼:
public class XmlUtil { public static String getNodeValue(String nodeName, String xmlPath) { try { // 將字符串轉為xml Document document = DocumentHelper.parseText(xmlPath); // 查找節點 Element element = (Element) document.selectSingleNode(nodeName); if (element != null) { return element.getStringValue(); } } catch (DocumentException e) { e.printStackTrace(); } return ""; } }
3.2:定義注解
3.2.1:模擬Reposity注解
作用就是告訴spring容器將依賴實例化注入到需要的調用類里面,其中用了name這個屬性,就是讓spring根據名字去尋找對應的bean進行依賴注入。並標識其運行在類上面(無法放在方法上)
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyRepository { public String name(); }
3.2.2:模擬controller注解
作用就是告訴spring容器這是一個控制器,並交給spring去實例化,可以運行在類和方法上
@Documented @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyController { public String name(); }
3.2.3:模擬Quanlifer注解
作用於成員變量上,主要的作用是讓spring根據配置的bean的名字進行注入:
@Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MyQualifer { public String name(); }
3.2.4:模擬@service注解
作用於類或者接口上,主要的作用就是標識一個類為service層,並交給spring去初始化!
@Documented @Target(ElementType.Type) @Retention(RetentionPolicy.RUNTIME) public @interface MyService { public String name(); }
3.2.5:MyDispatcherServlet
模擬DispatcherServlet,主要的作用就是作為請求控制器,處理客戶端請求,繼承自HttpServlet,我使用的版本是3.1.0,因此可以用注解來取得一些參數:
@WebServlet(name = "dispatherServlet", urlPatterns = "/", loadOnStartup = 1) public class MyDispatcherServlet extends HttpServlet { /** * 掃描的基准包 */ private String basePackage = ""; /** * 包名 */ private List<String> packageNames = new ArrayList<>(); /** * 注解名和類實例 */ private Map<String, Object> instanceMap = new HashMap<String, Object>(); /** * 包名和注解名 */ private Map<String, String> nameMap = new HashMap<>(); /** * url和方法 */ private Map<String, Method> urlMethodMap = new HashMap<>(); /** * Method和包名 */ private Map<Method, String> methodPackageMap = new HashMap<>(); @Override public void init(ServletConfig config) { try { getBasePackageFromConfigXml(); scanBasePackage(basePackage); instance(packageNames); springIoc(); handleUrlMethodMap(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } }
這里是一個流程處理類,第一步從xml里面獲取用戶配置的信息,這里的主要作用就是掃描出配置的包名,第二步就是掃描基准包的路徑然后放在一個List中,保存起來。第三步:根據掃描出來的包,獲取包里面的類class,然后利用反射獲取類上的class信息(不包含方法),然后把注解名和對應的類實例保存在map中,再把包名和注解的參數(這里主要是controller、service等在類上的)保存起來。第四步:模擬spring的IOC注入實例的過程,遍歷第三步中的注解名和類實例map,然后利用反射實例化字段!第五步:遍歷整個包下的類,然后獲取類中的所有方法,再取得MyRequestMapping的注解上的配置的參數,把它存在urlMethodMap中,這里的作用就是把類上的MyRequestingMapping的配置的參數和方法上的注解配置的參數組裝起來!最終在dopost方法中處理:
3.2.6:getBasePackFromConfig方法
整個方法的目的就是解析xml,獲取基准包,這是我們繼續往下的基礎,因為只有獲取包路徑,我們才能掃描到注解,才能知道自己要開發的類在哪個目錄
/** * 從xml中獲取basePackage * @return */ private void getBasePackageFromConfigXml() { final String basePackageName = XmlUtil.getNodeValue("scan-package-path", "springmvcConfig.xml"); basePackage=basePackageName; }
3.2.7:scanBasePackage
掃描基礎包:這是一個遞歸的方法,主要是解析路徑,獲取目錄下的文件,然后添加文件名到packageNames這個list,保存了我們掃描的路徑下的所有文件,接下來就是循環遍歷這個list,取出其中的class再進一步的處理
/** * 掃描基礎包,保存類路徑到list中 * @param basePackage */ private void scanBasePackage(String basePackage) { final URL url = this.getClass().getClassLoader().getResource(basePackage.replaceAll("\\.", "/")); final File basePackageFile = new File(url.getPath()); System.out.println("掃描到的文件是:" + basePackageFile.getName()); final File[] files = basePackageFile.listFiles(); for (File file : files) { if (file.isDirectory()) { scanBasePackage(basePackage + "." + file.getName()); } else if (file.isFile()) { packageNames.add(basePackage + "." + file.getName().split("\\.")[0]); } } }
3.2.8:instance(packageNames)
該方法的主要目的就是遍歷包下面的類,掃描類上面的注解,主要是@MyController、@MyService、@MyRepository,然后利用反射獲取類上的注解,取得注解上配置的參數(就是name上的值,在下面的實例中,比如:instanceMap將會存放("order",OrderController.class),nameMap將會存放("test.java.com.wyq","order")
/** * 初始化 * * @param packageNames */ private void instance(List<String> packageNames) throws ClassNotFoundException, IllegalAccessException, InstantiationException { if (packageNames.size() < 1) { return; } for (String packageName : packageNames) { Class<?> fileClass = Class.forName(packageName); if (fileClass.isAnnotationPresent(MyController.class)) { MyController myController = fileClass.getAnnotation(MyController.class); final String controllerName = myController.name(); instanceMap.put(controllerName, fileClass.newInstance()); nameMap.put(packageName, controllerName); System.out.println("Controller:" + packageName + "name" + controllerName); } else if (fileClass.isAnnotationPresent(MyService.class)) { final MyService service = fileClass.getAnnotation(MyService.class); final String serviceName = service.name(); instanceMap.put(serviceName, fileClass.newInstance()); nameMap.put(packageName, serviceName); } else if (fileClass.isAnnotationPresent(MyRepository.class)) { final MyRepository repository = fileClass.getAnnotation(MyRepository.class); final String repositoryName = repository.name(); instanceMap.put(repositoryName, fileClass.newInstance()); nameMap.put(packageName, repositoryName); } } }
3.2.9:springIoc
springIoc的注入:該方法的目的主要是遍歷instanceMap,找到有@MyController、@MyService、@MyRepository中的成員字段上有@Myqualifer的注解,(注:Myqualifer的作用類似於@AutoWired自動注入)然后獲取這個注解,利用反射機制,實例化這個成員變量。
/** * springIOC的注入 * * @throws IllegalAccessException */ private void springIoc() throws IllegalAccessException { for (Map.Entry<String, Object> annotationNameAndInstance : instanceMap.entrySet()) { final Field[] fields = annotationNameAndInstance.getValue().getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(MyQualifer.class)) { final String myQualiferName = field.getAnnotation(MyQualifer.class).name(); field.setAccessible(true); field.set(annotationNameAndInstance.getValue(), instanceMap.get(myQualiferName)); } } } }
3.3.0:handleUrlMethodMap
這個方法的主要作用就是獲取@MyRequestMapping中配置的url鏈接,然后將他們封裝到一個methodPackageMap的map中,其首先是取類上面的@MyRequestMapping配置的url,然后再取方法上的@MyRequestMapping配置的url,拼接在一起,作為鍵,映射其對應的方法,這樣當請求進來時,就可以直接根據url匹配到對應的處理的方法。向下面的實例:methodPackageMap將會存放的數據是("/orderApi/create",create(method)),而methodPackageMap將會存放的是(create,"com.wyq")
/** * 處理請求的url和method * @throws ClassNotFoundException */ private void handleUrlMethodMap() throws ClassNotFoundException { if (packageNames.size() < 1) { return; } for (String packageName : packageNames) { final Class<?> fileClass = Class.forName(packageName); final Method[] methods = fileClass.getMethods(); final StringBuffer baseUrl = new StringBuffer(); if (fileClass.isAnnotationPresent(MyRequestMapping.class)) { MyRequestMapping myRequest = (MyRequestMapping) fileClass.getAnnotation(MyRequestMapping.class); baseUrl.append(myRequest.name()); } for (Method method : methods) { if (method.isAnnotationPresent(MyRequestMapping.class)) { final MyRequestMapping myrequest = method.getAnnotation(MyRequestMapping.class); baseUrl.append(myrequest.name()); urlMethodMap.put(baseUrl.toString(), method); methodPackageMap.put(method, packageName); } } } }
3.3.1:doPost方法
這個方法的主要的思路就是把上面組裝的map數據然后再反向解析,首先就是獲取請求的url,然后獲取contextPath(項目的地址),比如請求http://172.123.344.122:8080/order/create,那么最終的path就是order/create,再根據請求的url獲取對應的處理方法,這里會找到create方法,再根據方法名找到類所在的包路徑,再根據包路徑獲取Controller的名字,再根據Controller的名字獲取具體的Controller(用戶寫的),再利用反射機制調用具體的Controller里的方法,這樣從請求進入到解析處理就完成了!
/** * 具體的處理請求的方法 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { final String requestURI = req.getRequestURI(); final String contextPath = req.getContextPath(); final String path = requestURI.replaceAll(contextPath, ""); //根據請求的路徑反向獲取方法 final Method method = urlMethodMap.get(path); if (null != method) { //獲取包名 final String packageName = methodPackageMap.get(method); //根據包名獲取到Controller的名字 final String controllerName = nameMap.get(packageName); //根據controller的名字得到控制器 Object orderController = instanceMap.get(controllerName); try { method.setAccessible(true); method.invoke(orderController); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
四:總結
本篇博客主要是講解了簡易版SpringMvc的實現,其中主要包括了基本注解定義,還有springmv的具體處理模式,當然真實的springmvc實現很復雜,這里只是冰山一角的簡單實現一個功能,不過舉一反三,基本思路我們要思考,雖然沒有搭建一個完成版springmvc的能力,不過從最簡單的實現開始,一步步的走,對我們的編程能力有推波助瀾的功效!加油