Spring學習之——手寫Spring原始碼V2.0(實現IOC、DI、MVC、AOP)
前言
在上一篇《Spring學習之——手寫Spring原始碼(V1.0)》中,我實現了一個Mini版本的Spring框架,在這幾天,博主又看了不少關於Spring原始碼解析的視訊,受益匪淺,也對Spring的各元件有了自己的理解和認識,於是乎,在空閒時間把之前手寫Spring的程式碼重構了一遍,遵循了單一職責的原則,使結構更清晰,並且實現了AOP,這次還是隻引用一個servlet包,其他全部手寫實現。
全部原始碼照舊放在文章末尾~
開發工具
環境:jdk8 + IDEA + maven
jar包:javax.servlet-2.5
專案結構
具體實現
配置檔案
web.xml 與之前一樣 並無改變
application.properties 增加了html頁面路徑和AOP的相關配置
#掃描路徑# scanPackage=com.wqfrw #模板引擎路徑# templateRoot=template #切面表示式# pointCut=public .* com.wqfrw.service.impl..*ServiceImpl..*(.*) #切面類# aspectClass=com.wqfrw.aspect.LogAspect #切面前置通知# aspectBefore=before #切面後置通知# aspectAfter=after #切面異常通知# aspectAfterThrowing=afterThrowing #切面異常型別# aspectAfterThrowingName=java.lang.Exception
IOC與DI實現
1.在DispatcherServlet的init方法中初始化ApplicationContent;
2.ApplicationContent是Spring容器的主入口,通過建立BeanDefintionReader物件載入配置檔案;
3.在BeanDefintionReader中將掃描到的類解析成BeanDefintion返回;
4.ApplicationContent中通過BeanDefintionMap這個快取來關聯BeanName與BeanDefintion物件之間的關係;
5.通過getBean方法,進行Bean的建立並封裝為BeanWrapper物件,進行依賴注入,快取到IoC容器中
/** * 功能描述: 初始化MyApplicationContext * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月03日 18:54:01 * @param configLocations * @return: **/ public MyApplicationContext(String... configLocations) { this.configLocations = configLocations; try { //1.讀取配置檔案並解析BeanDefinition物件 beanDefinitionReader = new MyBeanDefinitionReader(configLocations); List<MyBeanDefinition> beanDefinitionList = beanDefinitionReader.loadBeanDefinitions(); //2.將解析後的BeanDefinition物件註冊到beanDefinitionMap中 doRegisterBeanDefinition(beanDefinitionList); //3.觸發建立物件的動作,呼叫getBean()方法(Spring預設是延時載入) doCreateBean(); } catch (Exception e) { e.printStackTrace(); } }
/** * 功能描述: 真正觸發IoC和DI的動作 1.建立Bean 2.依賴注入 * * @param beanName * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月03日 19:48:58 * @return: java.lang.Object **/ public Object getBean(String beanName) { //============ 建立例項 ============ //1.獲取配置資訊,只要拿到beanDefinition物件即可 MyBeanDefinition beanDefinition = beanDefinitionMap.get(beanName); //用反射建立例項 這個例項有可能是代理物件 也有可能是原生物件 封裝成BeanWrapper統一處理 Object instance = instantiateBean(beanName, beanDefinition); MyBeanWrapper beanWrapper = new MyBeanWrapper(instance); factoryBeanInstanceCache.put(beanName, beanWrapper); //============ 依賴注入 ============ populateBean(beanName, beanDefinition, beanWrapper); return beanWrapper.getWrapperInstance(); }
/** * 功能描述: 依賴注入 * * @param beanName * @param beanDefinition * @param beanWrapper * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月03日 20:09:01 * @return: void **/ private void populateBean(String beanName, MyBeanDefinition beanDefinition, MyBeanWrapper beanWrapper) { Object instance = beanWrapper.getWrapperInstance(); Class<?> clazz = beanWrapper.getWrapperClass(); //只有加了註解的類才需要依賴注入 if (!(clazz.isAnnotationPresent(MyController.class) || clazz.isAnnotationPresent(MyService.class))) { return; } //拿到bean所有的欄位 包括private、public、protected、default for (Field field : clazz.getDeclaredFields()) { //如果沒加MyAutowired註解的屬性則直接跳過 if (!field.isAnnotationPresent(MyAutowired.class)) { continue; } MyAutowired annotation = field.getAnnotation(MyAutowired.class); String autowiredBeanName = annotation.value().trim(); if ("".equals(autowiredBeanName)) { autowiredBeanName = field.getType().getName(); } //強制訪問 field.setAccessible(true); try { if (factoryBeanInstanceCache.get(autowiredBeanName) == null) { continue; } //賦值 field.set(instance, this.factoryBeanInstanceCache.get(autowiredBeanName).getWrapperInstance()); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
MVC實現
1.在DispatcherServlet的init方法中呼叫initStrategies方法初始化九大核心元件;
2.通過迴圈BeanDefintionMap拿到每個介面的url、例項物件、對應方法封裝成一個HandlerMapping物件的集合,並建立HandlerMapping與HandlerAdapter(引數介面卡)的關聯;
3.初始化ViewResolver(檢視解析器),解析配置檔案中模板檔案路徑(即html檔案的路徑,其作用類似於BeanDefintionReader);
4.在執行階段,呼叫doDispatch方法,根據請求的url找到對應的HandlerMapping;
5.在HandlerMapping對應的HandlerAdapter中,呼叫handle方法,進行引數動態賦值,反射呼叫介面方法,拿到返回值與返回頁面封裝成一個MyModelAndView物件返回;
6.通過ViewResolver拿到View(模板頁面檔案),在View中通過render方法,通過正則將返回值與頁面取值符號進行適配替換,渲染成html頁面返回
/** * 功能描述: 初始化核心元件 在Spring中有九大核心元件,這裡只實現三種 * * @param context * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月04日 11:51:55 * @return: void **/ protected void initStrategies(MyApplicationContext context) { //多檔案上傳元件 //initMultipartResolver(context); //初始化本地語言環境 //initLocaleResolver(context); //初始化模板處理器 //initThemeResolver(context); //初始化請求分發處理器 initHandlerMappings(context); //初始化引數介面卡 initHandlerAdapters(context); //初始化異常攔截器 //initHandlerExceptionResolvers(context); //初始化檢視前處理器 //initRequestToViewNameTranslator(context); //初始化檢視轉換器 initViewResolvers(context); //快取管理器(值棧) //initFlashMapManager(context); }
/** * 功能描述: 進行引數適配 * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 19:41:38 * @param req * @param resp * @param mappedHandler * @return: com.framework.webmvc.servlet.MyModelAndView **/ public MyModelAndView handle(HttpServletRequest req, HttpServletResponse resp, MyHandlerMapping mappedHandler) throws Exception { //儲存引數的名稱和位置 Map<String, Integer> paramIndexMapping = new HashMap<>(); //獲取這個方法所有形參的註解 因一個引數可以新增多個註解 所以是一個二維陣列 Annotation[][] pa = mappedHandler.getMethod().getParameterAnnotations(); /** * 獲取加了MyRequestParam註解的引數名和位置 放入到paramIndexMapping中 */ for (int i = 0; i < pa.length; i++) { for (Annotation annotation : pa[i]) { if (!(annotation instanceof MyRequestParam)) { continue; } String paramName = ((MyRequestParam) annotation).value(); if (!"".equals(paramName.trim())) { paramIndexMapping.put(paramName, i); } } } //方法的形參列表 Class<?>[] parameterTypes = mappedHandler.getMethod().getParameterTypes(); /** * 獲取request和response的位置(如果有的話) 放入到paramIndexMapping中 */ for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; if (parameterType == HttpServletRequest.class || parameterType == HttpServletResponse.class) { paramIndexMapping.put(parameterType.getName(), i); } } //拿到一個請求所有傳入的實際實參 因為一個url上可以多個相同的name,所以此Map的結構為一個name對應一個value[] //例如:request中的引數t1=1&t1=2&t2=3形成的map結構: //key=t1;value[0]=1,value[1]=2 //key=t2;value[0]=3 Map<String, String[]> paramsMap = req.getParameterMap(); //自定義初始實參列表(反射呼叫Controller方法時使用) Object[] paramValues = new Object[parameterTypes.length]; /** * 從paramIndexMapping中取出引數名與位置 動態賦值 */ for (Map.Entry<String, String[]> entry : paramsMap.entrySet()) { //拿到請求傳入的實參 String value = entry.getValue()[0]; //如果包含url引數上的key 則動態轉型賦值 if (paramIndexMapping.containsKey(entry.getKey())) { //獲取這個實參的位置 int index = paramIndexMapping.get(entry.getKey()); //動態轉型並賦值 paramValues[index] = caseStringValue(value, parameterTypes[index]); } } /** * request和response單獨賦值 */ if (paramIndexMapping.containsKey(HttpServletRequest.class.getName())) { int index = paramIndexMapping.get(HttpServletRequest.class.getName()); paramValues[index] = req; } if (paramIndexMapping.containsKey(HttpServletResponse.class.getName())) { int index = paramIndexMapping.get(HttpServletResponse.class.getName()); paramValues[index] = resp; } //方法呼叫 拿到返回結果 Object result = mappedHandler.getMethod().invoke(mappedHandler.getController(), paramValues); if (result == null || result instanceof Void) { return null; } else if (mappedHandler.getMethod().getReturnType() == MyModelAndView.class) { return (MyModelAndView) result; } return null; } /** * 功能描述: 動態轉型 * * @param value String型別的value * @param clazz 實際物件的class * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月04日 16:34:40 * @return: java.lang.Object 實際物件的例項 **/ private Object caseStringValue(String value, Class<?> clazz) throws Exception { //通過class物件獲取一個入參為String的構造方法 沒有此方法則丟擲異常 Constructor constructor = clazz.getConstructor(new Class[]{String.class}); //通過構造方法new一個例項返回 return constructor.newInstance(value); }
/** * 功能描述: 對頁面內容進行渲染 * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月04日 17:54:40 * @param model * @param req * @param resp * @return: void **/ public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse resp) throws Exception { StringBuilder sb = new StringBuilder(); //只讀模式 讀取檔案 RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r"); String line = null; while ((line = ra.readLine()) != null) { line = new String(line.getBytes("ISO-8859-1"), "utf-8"); //%{name} Pattern pattern = Pattern.compile("%\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(line); while (matcher.find()) { String paramName = matcher.group(); paramName = paramName.replaceAll("%\\{|\\}", ""); Object paramValue = model.get(paramName); line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString())); matcher = pattern.matcher(line); } sb.append(line); } resp.setCharacterEncoding("utf-8"); resp.getWriter().write(sb.toString()); }
html頁面
404.html
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>頁面沒有找到</title> </head> <body> <font size="25" color="red">Exception Code : 404 Not Found</font> <br><br><br> @我恰芙蓉王 </body> </html>
500.html
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>伺服器崩潰</title> </head> <body> <font size="25" color="red">Exception Code : 500 <br/> 伺服器崩潰了~</font> <br/> <br/> <b>Message:%{message}</b> <br/> <b>StackTrace:%{stackTrace}</b> <br/> <br><br><br> @我恰芙蓉王 </body> </html>
index.html
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>自定義SpringMVC模板引擎Demo</title> </head> <center> <h1>大家好,我是%{name}</h1> <h2>我愛%{food}</h2> <font color="red"> <h2>時間:%{date}</h2> </font> <br><br><br> @我恰芙蓉王 </center> </html>
測試介面呼叫返回頁面
404.html 介面未找到
500.html 伺服器錯誤
index.html 正常返回頁面
AOP實現
1.參照IOC與DI實現第五點,在物件例項化之後,依賴注入之前,將配置檔案中AOP的配置解析至AopConfig中;
2.通過配置的pointCut引數,正則匹配此例項物件的類名與方法名,如果匹配上,將配置的三個通知方法(Advice)與此方法建立聯絡,生成一個 Map<Method, Map<String, MyAdvice>> methodCache 的快取;
3.將原生物件、原生物件class、原生物件方法與通知方法的對映關係封裝成AdviceSupport物件;
4.如果需要代理,則使用JdkDynamicAopProxy中getProxy方法,獲得一個此原生物件的代理物件,並將原生物件覆蓋;
5.JdkDynamicAopProxy實現了InvocationHandler介面(使用JDK的動態代理),重寫invoke方法,在此方法中執行切面方法與原生物件方法。
/** * 功能描述: 反射例項化物件 * * @param beanName * @param beanDefinition * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月03日 20:08:50 * @return: java.lang.Object **/ private Object instantiateBean(String beanName, MyBeanDefinition beanDefinition) { String className = beanDefinition.getBeanClassName(); Object instance = null; try { Class<?> clazz = Class.forName(className); instance = clazz.newInstance(); /** * ===========接入AOP begin=========== */ MyAdviceSupport support = instantiateAopConfig(beanDefinition); support.setTargetClass(clazz); support.setTarget(instance); //如果需要代理 則用代理物件覆蓋目標物件 if (support.pointCutMatch()) { instance = new MyJdkDynamicAopProxy(support).getProxy(); } /** * ===========接入AOP end=========== */ factoryBeanObjectCache.put(beanName, instance); } catch (Exception e) { e.printStackTrace(); } return instance; }
/** * 功能描述: 解析配置 pointCut * * @param * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 11:20:21 * @return: void **/ private void parse() { String pointCut = aopConfig.getPointCut() .replaceAll("\\.", "\\\\.") .replaceAll("\\\\.\\*", ".*") .replaceAll("\\(", "\\\\(") .replaceAll("\\)", "\\\\)"); //public .*.com.wqfrw.service..*impl..*(.*) String pointCutForClassRegex = pointCut.substring(0, pointCut.lastIndexOf("\\(") - 4); this.pointCutClassPattern = Pattern.compile(pointCutForClassRegex.substring(pointCutForClassRegex.lastIndexOf(" ") + 1)); methodCache = new HashMap<>(); //匹配方法的正則 Pattern pointCutPattern = Pattern.compile(pointCut); //1.對回撥通知進行快取 Map<String, Method> aspectMethods = new HashMap<>(); try { //拿到切面類的class com.wqfrw.aspect.LogAspect Class<?> aspectClass = Class.forName(this.aopConfig.getAspectClass()); //將切面類的通知方法快取到aspectMethods Stream.of(aspectClass.getMethods()).forEach(v -> aspectMethods.put(v.getName(), v)); //2.掃描目標類的方法,去迴圈匹配 for (Method method : targetClass.getMethods()) { String methodString = method.toString(); //如果目標方法有丟擲異常 則擷取 if (methodString.contains("throws")) { methodString = methodString.substring(0, methodString.lastIndexOf("throws")).trim(); } /** * 匹配目標類方法 如果匹配上,就將快取好的通知與它建立聯絡 如果沒匹配上,則忽略 */ Matcher matcher = pointCutPattern.matcher(methodString); if (matcher.matches()) { Map<String, MyAdvice> adviceMap = new HashMap<>(); //前置通知 if (!(null == aopConfig.getAspectBefore() || "".equals(aopConfig.getAspectBefore()))) { adviceMap.put("before", new MyAdvice(aspectClass.newInstance(), aspectMethods.get(aopConfig.getAspectBefore()))); } //後置通知 if (!(null == aopConfig.getAspectAfter() || "".equals(aopConfig.getAspectAfter()))) { adviceMap.put("after", new MyAdvice(aspectClass.newInstance(), aspectMethods.get(aopConfig.getAspectAfter()))); } //異常通知 if (!(null == aopConfig.getAspectAfterThrowing() || "".equals(aopConfig.getAspectAfterThrowing()))) { MyAdvice advice = new MyAdvice(aspectClass.newInstance(), aspectMethods.get(aopConfig.getAspectAfterThrowing())); advice.setThrowingName(aopConfig.getAspectAfterThrowingName()); adviceMap.put("afterThrowing", advice); } //建立關聯 methodCache.put(method, adviceMap); } } } catch (Exception e) { e.printStackTrace(); } }
/** * 功能描述: 返回一個代理物件 * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 14:17:22 * @param * @return: java.lang.Object **/ public Object getProxy() { return Proxy.newProxyInstance(this.getClass().getClassLoader(), this.support.getTargetClass().getInterfaces(), this); } /** * 功能描述: 重寫invoke * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 20:29:19 * @param proxy * @param method * @param args * @return: java.lang.Object **/ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Map<String, MyAdvice> advices = support.getAdvices(method, support.getTargetClass()); Object result = null; try { //呼叫前置通知 invokeAdvice(advices.get("before")); //執行原生目標方法 result = method.invoke(support.getTarget(), args); //呼叫後置通知 invokeAdvice(advices.get("after")); } catch (Exception e) { //呼叫異常通知 invokeAdvice(advices.get("afterThrowing")); throw e; } return result; } /** * 功能描述: 執行切面方法 * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 11:09:32 * @param advice * @return: void **/ private void invokeAdvice(MyAdvice advice) { try { advice.getAdviceMethod().invoke(advice.getAspect()); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
/** * @ClassName LogAspect * @Description TODO(切面類) * @Author 我恰芙蓉王 * @Date 2020年08月05日 10:03 * @Version 2.0.0 **/ public class LogAspect { /** * 功能描述: 前置通知 * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 17:24:30 * @param * @return: void **/ public void before(){ System.err.println("=======前置通知======="); } /** * 功能描述: 後置通知 * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 17:24:40 * @param * @return: void **/ public void after(){ System.err.println("=======後置通知=======\n"); } /** * 功能描述: 異常通知 * * @建立人: 我恰芙蓉王 * @建立時間: 2020年08月05日 17:24:47 * @param * @return: void **/ public void afterThrowing(){ System.err.println("=======出現異常======="); } }
執行結果
總結
以上只貼出了部分核心實現程式碼,有興趣的童鞋可以下載原始碼除錯,具體的註釋我都在程式碼中寫得很清楚。
程式碼已經提交至Git : https://github.com/wqfrw/HandWritingSpring