中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

記事本做網(wǎng)站怎么調(diào)整圖片間距seo優(yōu)化團隊

記事本做網(wǎng)站怎么調(diào)整圖片間距,seo優(yōu)化團隊,中國擬在建項目網(wǎng)官網(wǎng),oss 阿里云wordpress這里續(xù)寫上一章博客(110章博客): 現(xiàn)在我們來學習一下高級的技術(shù),前面的mvc知識,我們基本可以在67章博客及其后面相關(guān)的博客可以學習到,現(xiàn)在開始學習精髓: Spring MVC 高級技術(shù): …
這里續(xù)寫上一章博客(110章博客):
現(xiàn)在我們來學習一下高級的技術(shù),前面的mvc知識,我們基本可以在67章博客及其后面相關(guān)的博客可以學習到,現(xiàn)在開始學習精髓:
Spring MVC 高級技術(shù):
攔截器(Inteceptor)使用:
監(jiān)聽器、過濾器和攔截器對?(前面兩個在53章博客可以學習到,后面只是名稱上的解釋,如果可以,那么前面兩個也可以說成是攔截器,所以存在很多框架的攔截器):
Servlet:處理Request請求和Response響應
過濾器(Filter):對Request請求起到過濾的作用,作用在Servlet之前,如果配置為/*可以對所 有的資源訪問(servlet、js/css靜態(tài)資源等)進行過濾處理
監(jiān)聽器(Listener):實現(xiàn)了javax.servlet.ServletContextListener 接?的服務器端組件,它隨Web應用的啟動?啟動,只初始化一次,然后會一直運行監(jiān)視,隨Web應用的停??銷毀
監(jiān)聽器的作用與過濾器雖然都有攔截的意思,但是偏重不同
監(jiān)聽器可以選擇對一些數(shù)據(jù)或者說數(shù)據(jù)變化進行監(jiān)聽以及攔截,而過濾則是對過來的請求直接攔截,而不是更加里面的數(shù)據(jù)攔截,所以一般情況下,過濾器通常是在監(jiān)聽器之前進行攔截的
那么說監(jiān)聽器一般有如下的作用:
作用一:做一些初始化工作,web應用中spring容器啟動ContextLoaderListener
作用?:監(jiān)聽web中的特定事件,?如HttpSession,ServletRequest的創(chuàng)建和銷毀,變量的創(chuàng)建、 銷毀和修改等,可以在某些動作前后增加處理,實現(xiàn)監(jiān)控,?如統(tǒng)計在線?數(shù),利?HttpSessionLisener等
攔截器(Interceptor):是SpringMVC、Struts等表現(xiàn)層框架自己的,不會攔截jsp/html/css/image的訪問等,只會攔截訪問的控制器方法(Handler),一般來說,這個攔截在一定程度上使用了過濾器以及監(jiān)聽器,因為需要確定攔截的數(shù)據(jù),通常需要先獲得,所以mvc的攔截器的實現(xiàn)方式通常在于過濾器或者說監(jiān)聽器(注意,只是因為他需要對應的數(shù)據(jù),所以他才會在于其他的器,如過濾和監(jiān)聽,否則攔截器一般只是攔截指定數(shù)據(jù)的處理,而不是在于什么)
根據(jù)上面的說明,其實從配置的?度也能夠總結(jié)發(fā)現(xiàn):serlvet、?lter、listener是配置在web.xml中的,?interceptor是 配置在表現(xiàn)層框架自己的配置?件中的,所以Interceptor一般是框架自己的
根據(jù)前面的說明,可以知道如下:
攔截器會在如下的情況可能會發(fā)生攔截:
Handler業(yè)務邏輯執(zhí)行之前攔截一次(操作url)
在Handler邏輯執(zhí)行完畢但未跳轉(zhuǎn)??之前攔截一次(操作轉(zhuǎn)發(fā),如果沒有,那么一般操作響應數(shù)據(jù),如果也沒有,那么雖然攔截,但并未做什么)
在跳轉(zhuǎn)??之后攔截一次(比如對應的json的處理,或者并未處理)

在這里插入圖片描述

前面我們知道了這個圖:

在這里插入圖片描述

以及他的說明:
流程說明:
第一步:用戶發(fā)送請求?前端控制器DispatcherServlet
第?步:DispatcherServlet收到請求調(diào)?HandlerMapping處理器映射器(一般是map保存的)
第三步:處理器映射器根據(jù)請求Url找到具體的Handler(后端控制器,可以根據(jù)xml配置、注解進行查找,因為查找,所以是映射),?成處理器對象及處理器攔截器(如果有則?成)一并返回DispatcherServlet,他負責創(chuàng)建
第四步:DispatcherServlet調(diào)?HandlerAdapter處理器適配器去調(diào)?Handler
第五步:處理器適配器執(zhí)?Handler(controller的方法,生成對象了,這里相當于調(diào)用前面的handle01方法,他負責調(diào)用)
第六步:Handler執(zhí)行完成給處理器適配器返回ModelAndView,即處理器適配器得到返回的ModelAndView,這也是為什么前面我們操作方法時,是可以直接操作他并返回的,而返回給的人就是處理器適配器,就算你不返回,那么處理器適配器或者在之前,即他們兩個中間,可能會進行其他的處理,來設置ModelAndView,并給處理器適配器
第七步:處理器適配器向前端控制器返回 ModelAndView(因為適配或者返回數(shù)據(jù),所以是適配),ModelAndView 是SpringMVC 框架的一個 底層對 象,包括 Model 和 View
第?步:前端控制器請求視圖解析器去進行視圖解析,根據(jù)邏輯視圖名來解析真正的視圖(加上前后的補充,即前面的配置視圖解析器)
第九步:視圖解析器向前端控制器返回View
第?步:前端控制器進行視圖渲染,就是將模型數(shù)據(jù)(在 ModelAndView 對象中)填充到 request 域,改變了servlet,最終操作servlet來進行返回
第?一步:前端控制器向用戶響應結(jié)果(jsp的)
即可以理解:請求找路徑并返回(1,2,3),給路徑讓其判斷路徑并返回且獲得對應對象(4,5,6,7),變成參數(shù)解析(如拼接) 進行轉(zhuǎn)發(fā)(8,9),然后到jsp(10),最后渲染(11)
所以說:
第一次攔截:在1和5中間處理
第二次攔截:在6到8中間處理
第三次攔截:在9到11中間處理
其實通過圖片,我們應該知道,第一次攔截應該是在3和4中在前端控制器旁邊處理,而7和8就是第二次攔截,10到11則是第三次攔截(這里也可能是9到10)
所以可以知道,其實mvc自帶的有一些攔截,這也是對應注解,比如@RequestBody或者@ResponseBody可以操作的原因
當然,我們也可以進行添加攔截,這在后面會說明的
為了更加的知道攔截器的處理,我們直接來進行實戰(zhàn):
注意:mvc的攔截器是里面的,也就是說,servlet原本的過濾器必然先處理或者后處理
實際上我們學習源碼很大程度是必須要有實戰(zhàn)的,因為一個框架的源碼我們基本是不可能全部讀完的,這取決于一個框架是由很長時間的迭代,以及很多人一起開發(fā)完成的,當然,如果你的框架夠小,那么可以是單獨完成,在這種情況下,學習框架中,用閱讀源碼來學習,我們只能知道他的一點實現(xiàn)方式,所以學習框架通常需要直接的實戰(zhàn)來進行學習,來直接的確定他的作用,而不是單獨看源碼來確定作用(你怎么知道他有沒有其他關(guān)聯(lián),并且要知道這個關(guān)聯(lián)需要看更多的源碼),也就是說,實際上源碼的解析大多數(shù)是讓你知道他的實現(xiàn)方式,而不是具體細節(jié)(比如,他為什么這樣定義變量等等),當然,除了實現(xiàn)方式有時候也需要學習設計模式,這個在以后會單獨給一個博客來進行處理的,先了解一些框架的設計模式再說
那么,既然要實戰(zhàn),我們首先需要操作一個項目,項目如下:

在這里插入圖片描述

對應的依賴,在前面我們已經(jīng)給過多次了,這里我們繼續(xù)給出吧
 <packaging>war</packaging><dependencies><dependency><!--mvc需要的依賴,即有前端控制器DispatcherServlet--><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.1.5.RELEASE</version></dependency><!--servlet坐標,若不使用對應的類,如HttpServletRequest的話,可以不加--><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.8</version><!--這個必須要,后面的可以不寫(后面兩個),但最好寫上,防止其他情況--></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.9.8</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId><version>2.9.0</version></dependency></dependencies>
如果需要補充,自行補充吧
對應的web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><servlet><servlet-name>dispatcherServlet</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:springmvc.xml</param-value></init-param></servlet><servlet-mapping><servlet-name>dispatcherServlet</servlet-name><url-pattern>/</url-pattern></servlet-mapping></web-app>
index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<button id="btn">ajax提交</button>
<script>$("#btn").click(function () {let url = 'test/in';let data = '[{"id":1,"username":"張三"},{"id":2,"username":"李四"}]';$.ajax({type: 'POST',//大小寫可以忽略url: url,data: data,contentType: 'application/json;charset=utf-8',success: function (data) {console.log(data);alert(data)}})})
</script>
</body>
</html>
springmvc.xml:
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc.xsd"><context:component-scan base-package="com.controller"/><mvc:annotation-driven></mvc:annotation-driven>
</beans>
entity里面的User類:
package com.entity;public class User {String id;String username;public String getId() {return id;}public void setId(String id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}@Overridepublic String toString() {return "User{" +"id='" + id + '\'' +", username='" + username + '\'' +'}';}
}
test類:
package com.controller;import com.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;import java.util.List;@Controller
@RequestMapping("/test")
public class test {@RequestMapping("in")@ResponseBodypublic List<User> ajax(@RequestBody List<User> list) {System.out.println(list);return list;}
}
自行配置tomcat,啟動,運行看看結(jié)果,那么我們基礎操作搭建完畢,現(xiàn)在我們來操作一下攔截:
在com包下創(chuàng)建Interceptor包,然后創(chuàng)建MyInterceptor類:
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;public class MyInterceptor implements HandlerInterceptor {
}
其中HandlerInterceptor接口如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.web.servlet;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;public interface HandlerInterceptor {//默認的不要求強制被實現(xiàn)default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {return true; //默認是true的}default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}
這三個代表了三個地方,前面的三個攔截,比如:
preHandle是第一個攔截,postHandle是第二個攔截,afterCompletion是第三個攔截,實際上一個框架的編寫,除了給出一些功能外,還需要存在擴展功能,有的話最好,而攔截的處理基本就是框架基本的擴展了(所以在spring中也存在多個攔截的處理,包括mybatis,當然,他們可能并沒有特別的說明是攔截,但是你也或多或少可以知道,可以操作一些類來實現(xiàn)在中間進行處理的方式,這其實也算是一種攔截,因為是我們自行處理的,所以攔截器在某些方面可以是這樣的認為:框架自身給出可以擴展的方式都可以稱為攔截器)
實際上通過前面我們也明白,前端控制器底層基本上也是操作get和post,而servlet也是,但是mvc是建立在servlet上的,所以前端控制器通常也是生成了servlet,在前面我們學習了,前端控制器只是生成一個servlet(一般也可以是他自己),其中只是操作了攔截進行的處理,這個攔截或多或少使用了過濾或者監(jiān)聽,所以說,具體的是否監(jiān)聽或者過濾的處理,可能也是對應的配置導致進行的某些配置再處理(因為你刪除了配置前端控制器的,攔截也就不復存在了),那么在這種情況下,我們的三次攔截,也只是其中多個攔截的擴展,那么如果這個時候,你操作了傳統(tǒng)的攔截,那么就需要看配置的先后順序
我們繼續(xù)修改MyInterceptor類的內(nèi)容:
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor implements HandlerInterceptor {//第一次(個)/*Handler業(yè)務邏輯執(zhí)行之前攔截一次*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//程序先執(zhí)?preHandle()方法,如果該方法的返回值為true,則程序會繼續(xù)向下執(zhí)行處理器中的方法,否則將不再向下執(zhí)行//也就是說,這個攔截器擁有著結(jié)束執(zhí)行的能力,并不是攔截器只能操作擴展,實際上過濾或者監(jiān)聽也可以稱為攔截器的System.out.println(handler);System.out.println("Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次");return true;//我們可以看到,handler這個變量,實際上他就是準備執(zhí)行對應的handler方法的,但是需要看看是否返回true//默認情況下HandlerInterceptor里面是返回true的,自己可以看一看就知道了//由于他是在執(zhí)行具體方法之前的攔截,所以一般來說,我們會使用他來完成一些權(quán)限的處理//實際上你也可以使用過濾器來完成,但是需要配置,所以使用mvc封裝的比較方便}//第二次(個)/*在Handler邏輯執(zhí)行完畢但未跳轉(zhuǎn)??之前攔截一次看參數(shù)就知道,除了當前的方法外,還有對應的視圖和數(shù)據(jù)(modelAndView,因為是之后的嗎)所以在沒有進行渲染時,你可以選擇針對數(shù)據(jù)進行某些修改,這個在spring中也存在這樣的處理,在一個bean進行生成時,你也可以進行攔截進行某些處理或者修改*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println(handler);System.out.println(modelAndView);System.out.println("在Handler邏輯執(zhí)行完畢但未跳轉(zhuǎn)頁面之前攔截一次,我是第二次");}//第三次(個)/*在跳轉(zhuǎn)??之后攔截一次由于具體的數(shù)據(jù)和視圖都操作完畢,那么這里只能獲取對應的方法,以及你需要的異常信息了一般來說跳轉(zhuǎn)頁面就是將內(nèi)容響應給前端,所以一般渲染視圖中10到11一般是這里了(渲染視圖一般是需要根據(jù)文件,如jsp進行替換數(shù)據(jù)的過程,其中對數(shù)據(jù)的替換使用,這樣才能進行響應)即jsp是一個視圖,那么通過轉(zhuǎn)譯編譯就是對應的第10步的處理了,10前面是根據(jù)視圖對象找到該文件,10后面是轉(zhuǎn)譯編譯,中間考慮攔截(因為中間的處理都在前端控制器,所以或多或少他們的數(shù)據(jù)在某個情況下是共享的,所以可以替換的處理,一般情況下,是在jsp替換后,準備響應時,進行的攔截,也就是說11步就是最后的響應,比如,如果你在jsp中操作了輸出語句,那么這個值輸出后,這個方法才會進行處理,其實在68章博客中(一般是最后)也說明了這三個攔截方法,可以選擇去看一下)*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println(handler);System.out.println(ex);System.out.println("在跳轉(zhuǎn)頁面之后攔截一次,我是第三次");}
}
我們寫好了攔截器,自然需要進行使用,在mvc中使用,是必須需要配置的,并且他是mvc的,所以是需要在mvc的對應的配置文件中進行配置:
對應的springmvc.xml的補充:
 <!--配置攔截器--><mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><!--項目路徑下的對應路徑,一個*代表一個路徑下,但不代表路徑下的路徑下,兩個基本就是所有--><!--對所有的controller類里面的所有方法都進行攔截(因為是/**)--><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptor></mvc:interceptors>
實際上mvc:interceptors代表可以配置多個,在配置多個時,一般需要操作路徑,防止都進行攔截的處理(這個時候不寫可能報錯,具體可以測試),而不是指定的攔截(因為默認是/**),所以存在mvc:interceptors中存在mvc:interceptor,所以如果你只有一個并且需要是/ * *,那么可以這樣寫:
   <mvc:interceptors><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptors>
<!--
其中<mvc:mapping path="/**"/>也可以進行去掉,因為默認是/**
直接省略<mvc:interceptor>,代表只有你這一個的意思,但是由于只有一個,通常情況下,默認是/**的,而不會給你進行路徑的設置,所以如果你只有一個并且需要是/**,那么可以這樣寫
-->
一般我們建議使用這樣的方式來進行編寫:
 <!--配置攔截器--><mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><!--項目路徑下的對應路徑,一個*代表一個路徑下,但不代表路徑下的路徑下,兩個基本就是所有--><!--對所有的controller類里面的所有方法都進行攔截(因為是/**)--><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptor></mvc:interceptors>
<!--一般來說,/**的/代表從端口開始,這里了解即可,所以大多數(shù)訪問也會進行攔截處理(但是也要知道,一個端口基本只有一個服務器占用,所以并不會影響其他服務器的,所以不需要考慮其他服務器的影響問題(除非是業(yè)務邏輯上的影響,比如文件的操作))
雖然大多數(shù)訪問都會進行攔截處理,但是要知道,他是在需要執(zhí)行對應的方法執(zhí)行進行的攔截,也就是說,如果你并沒有需要執(zhí)行對應的方法,那么就不會執(zhí)行,而不會也自然不會出現(xiàn)true,這里我們需要注意的是:只有當前面的攔截的返回值或者說,第一個攔截的返回值為true時,后面的兩個攔截才會進行處理,這也是為什么如果你訪問index.jsp,由于第一個攔截沒有執(zhí)行(他并不會操作對應的方法,自然也就不會經(jīng)過這個攔截方法了,一般情況下,沒有經(jīng)過這個方法時,對應進行操作的變量是默認為false,但是只要你操作了這個攔截,或者就算你不進行重寫,他默認放行,因為這個時候他是默認的方法是true的,前面的HandlerInterceptor接口可以看到返回值的),那么其他的攔截,比如第三個攔截沒有起作用的原因,默認情況下,如果你不執(zhí)行第一個攔截,那么就是false的這里的true是否看起來相當于過濾器中的放行的意思呢,之所以需要這樣的規(guī)定,是保證其他的,如index.jsp不會操作攔截的原因(因為是直接的訪問,并不需要操作攔截,而減低操作的可能,從而使得服務器可以接收更多請求,或者減少內(nèi)存損耗)
-->
當然其實還有功能,這是mvc設計出來的功能,他也可以進行去掉,比如他可以選擇性的不攔截一個路徑的請求:
  <mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><mvc:exclude-mapping path="/demo/**"/> <!--選擇不攔截demo對應路徑下面的請求--><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptor></mvc:interceptors>
這樣就能不攔截demo開頭的處理了,一般情況下,servlet或者mvc的普通攔截都是在項目路徑下的直接攔截,比如前面的測試的路徑test/in,就是放在項目路徑后面,而不是端口路徑
編寫好后,我們執(zhí)行前面的代碼,看看后端的結(jié)果:
打印如下:
/*
public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次[User{id='1', username='張三'}, User{id='2', username='李四'}]public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
null
在Handler邏輯執(zhí)行完畢但未跳轉(zhuǎn)頁面之前攔截一次,我是第二次public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
null
在跳轉(zhuǎn)頁面之后攔截一次,我是第三次
*/
這個時候,你可以選擇將返回值變成false,那么我們看看這個打印結(jié)果:
/*
public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次
*/
我們可以發(fā)現(xiàn),他甚至連對應的方法都不執(zhí)行了,而方法都不執(zhí)行,默認情況下,是沒有響應體信息的,那么在前端顯示的就是空白(這個時候,甚至都不會操作視圖,也就是單純的返回空響應),也就是說,這個true也決定了對應的controller的方法的執(zhí)行,這也是為什么默認情況下,對應的HandlerInterceptor方法的返回值是true的一個原因
現(xiàn)在我們添加一個前端jsp,test.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<%System.out.println(1);
%>
1
</script>
</body>
</html>
上面的這個地方加上代碼:
<%System.out.println(1);
%>
這個是jsp的語法(在51章博客有說明),在轉(zhuǎn)譯和編譯的情況下,會進行處理的,最終操作攔截,那么我們在springmvc.xml中加上如下的配置:
<bean id="viewResolver"class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/"></property><property name="suffix" value=".jsp"></property></bean>
然后再test類中加上這個代碼:
 @RequestMapping("ix")public String ix() {return "test";}
首先還是false的返回值,然后直接的在url中后面加上test/ix訪問即可,這個時候我們查看后端的打印:
/*
public java.lang.String com.controller.test.ix()
Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次
*/
因為false不會經(jīng)過方法,所以在前端是顯示空白的
經(jīng)過這兩次的測試,可以發(fā)現(xiàn)handler的打印信息包含了,訪問權(quán)限,返回值,對有包路徑的方法及其參數(shù)列表等等,我們給ix方法加上兩個參數(shù)列表,即:
public String ix(String a,Integer b) {
對應的打印信息如下:
/*
public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次
*/
即也的確如此,現(xiàn)在我們給false,變成true的返回值,然后看看打印結(jié)果:
/*
public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
ModelAndView [view="test"; model={}]
在Handler邏輯執(zhí)行完畢但未跳轉(zhuǎn)頁面之前攔截一次,我是第二次1public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
null
在跳轉(zhuǎn)頁面之后攔截一次,我是第三次*/
也驗證了前面注釋中的:如果你在jsp中操作了輸出語句,那么這個值輸出后,這個方法才會進行處理
但是還有一個問題,如果controller對應的方法沒有返回值呢,因為沒有返回值說明他不會經(jīng)過視圖,而不經(jīng)過視圖,那么第三個攔截是否不會進行了,所以我們修改ix方法:
  @RequestMapping("ix")public void ix(String a,Integer b) {}
因為當我們沒有給出視圖名時,會將請求(參數(shù))進行拼接,一般是@RequestMapping的整個拼接
我們還是true,因為false修改與不修改是一樣的,反正都不執(zhí)行,那么他的修改沒有意義,我們看看執(zhí)行后的打印結(jié)果:
/*
public void com.controller.test.ix(java.lang.String,java.lang.Integer) throws java.io.IOException
Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次public void com.controller.test.ix(java.lang.String,java.lang.Integer) throws java.io.IOException
ModelAndView [view="test/ix"; model={}]
在Handler邏輯執(zhí)行完畢但未跳轉(zhuǎn)頁面之前攔截一次,我是第二次public void com.controller.test.ix(java.lang.String,java.lang.Integer) throws java.io.IOException
null
在跳轉(zhuǎn)頁面之后攔截一次,我是第三次
*/
但是這里還存在一些細節(jié),這是前面并沒有說明的,我們看如下:
我們繼續(xù)修改:
 @RequestMapping("ix")public String ix(String a, Integer b) throws IOException {return null;}
執(zhí)行之后,結(jié)果與上面的一樣,也就證明了,其實對應的視圖默認是null(是默認,雖然會根據(jù)組件得到路徑的視圖),所以當你返回類型為void時,他的視圖結(jié)果與返回null值是一樣的,然而,雖然根據(jù)路徑得到了視圖,但是其實在沒有視圖時也會得到結(jié)果,為什么,我們看這個代碼:
@RequestMapping("ix")public void ix(String a, Integer b, HttpServletResponse mm) throws IOException {mm.setContentType("text/html;charset=utf-8");PrintWriter writer = mm.getWriter();writer.println("哈哈哈");}
后端打印:
/*
public void com.controller.test.ix(java.lang.String,java.lang.Integer,javax.servlet.http.HttpServletResponse) throws java.io.IOException
Handler業(yè)務邏輯執(zhí)行之前攔截一次,我是第一次public void com.controller.test.ix(java.lang.String,java.lang.Integer,javax.servlet.http.HttpServletResponse) throws java.io.IOException
null
在Handler邏輯執(zhí)行完畢但未跳轉(zhuǎn)頁面之前攔截一次,我是第二次public void com.controller.test.ix(java.lang.String,java.lang.Integer,javax.servlet.http.HttpServletResponse) throws java.io.IOException
null
在跳轉(zhuǎn)頁面之后攔截一次,我是第三次
*/
可以發(fā)現(xiàn)沒有視圖了,沒有視圖相當于直接的返回的操作(直接操作響應體信息了),但是為什么加上這些就會沒有呢,這是因為如果你要自己加上響應信息的話,那么必然會與原來的如jsp而發(fā)生沖突(一般來說io并不能覆蓋,只是增加),導致對應的jsp信息出現(xiàn)問題,所以他們是需要分開的,所以只要你存在要自行操作響應體信息的,那么視圖就會為null,通過測試,只要你在參數(shù)列表中加上HttpServletResponse mm,比如:
 @RequestMapping("ix")public void ix(String a, Integer b, HttpServletResponse mm) throws IOException {
//        mm.setContentType("text/html;charset=utf-8");
//        PrintWriter writer = mm.getWriter();
//        writer.println("哈哈哈");}
那么視圖就為null,即執(zhí)行的操作響應體信息,如果你沒有設置,自然前端顯示空白
而打印信息中,第二次和第三次無論是否操作了HttpServletResponse或者無論是否報錯,都會出現(xiàn),也就是說,后面兩個并沒有互相聯(lián)系的不能讓對方放行的處理,所以他們基本必然都會打印了,但是報錯的話,視圖是找不到了,那么就不會出現(xiàn)對應的轉(zhuǎn)譯編譯才對,為什么第三次攔截也會出現(xiàn)呢,在前面我們說過了"10到11則是第三次攔截(這里也可能是9到10)",雖然在第8次報錯,但是第9次的操作需要返回報錯信息,這個時候是會經(jīng)過第三次攔截的(簡單來說,有返回,就算是空的,他通常也會操作)
至此,我們的細節(jié)基本說明完畢
攔截器的執(zhí)行流程:
在運行程序時,攔截器的執(zhí)行是有一定順序的,該順序與配置?件中所定義的攔截器的順序相關(guān),單個 攔截器,在程序中的執(zhí)行流程如下圖所示:

在這里插入圖片描述

1:程序先執(zhí)?preHandle()方法,如果該方法的返回值為true,則程序會繼續(xù)向下執(zhí)行處理器中的方 法,否則將不再向下執(zhí)行
2:在業(yè)務處理器(即控制器Controller類)處理完請求后,會執(zhí)?postHandle()方法,然后會通過DispatcherServlet向客戶端返回響應
3:在DispatcherServlet處理完請求后,才會執(zhí)?afterCompletion()方法
再結(jié)合這個圖吧:

在這里插入圖片描述

根據(jù)前面的說明,他的流程也可以說是對的
總結(jié)一下:

在這里插入圖片描述

上面是單個攔截器的流程,那么多個攔截器的執(zhí)行流程呢:
多個攔截器(假設有兩個攔截器Interceptor1和Interceptor2,并且在配置?件中, Interceptor1攔截 器配置在前),在程序中的執(zhí)行流程如下圖所示:

在這里插入圖片描述

從圖可以看出,當有多個攔截器同時工作時,它們的preHandle()方法會按照配置?件中攔截器的配置 順序執(zhí)行,?它們的postHandle()方法和afterCompletion()方法則會按照配置順序的反序執(zhí)行
我們來看例子:
首先創(chuàng)建兩個類在Interceptor包中:
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor1 implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Objecthandler) throws Exception {System.out.println("preHandle1....");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {System.out.println("postHandle1....");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Objecthandler, Exception ex) throws Exception {System.out.println("afterCompletion1....");}
}
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor2 implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Objecthandler) throws Exception {System.out.println("preHandle2....");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {System.out.println("postHandle2....");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Objecthandler, Exception ex) throws Exception {System.out.println("afterCompletion2....");}
}
我們進行配置(去掉原來的):
<mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor1"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor2"></bean></mvc:interceptor></mvc:interceptors><bean id="viewResolver"class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/"></property><property name="suffix" value=".jsp"></property></bean>
后端(之前處理的):
@RequestMapping("ix")public String ix(String a, Integer b) throws IOException {return "test";}
現(xiàn)在我們執(zhí)行(操作上面的test.jsp)看看打印信息:
/*
preHandle1....
preHandle2....
postHandle2....
postHandle1....
1
afterCompletion2....
afterCompletion1....
*/
也的確是第一個是順序(后配置的在后面,你修改配置中攔截的順序即可),后面兩個是反序,現(xiàn)在我們來測試,讓其中一個
配置放在前面:
 <mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor2"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor1"></bean></mvc:interceptor></mvc:interceptors>
打印結(jié)果:
/*
preHandle2....
preHandle1....
postHandle1....
postHandle2....
1
afterCompletion1....
afterCompletion2....
*/
也的確如此,那么如果其中一個不放行呢,我們將preHandle2…的對應方法的返回值設置為false看看,上面的配置不變,執(zhí)行后,看看打印結(jié)果:
/*
preHandle2....
*/
只有這一個,那么我們將配置順序改變回來,執(zhí)行看看結(jié)果:
/*
preHandle1....
preHandle2....
afterCompletion1....
*/
感覺到了,如果你的返回值是false,那么你后面的攔截都不能進行處理,包括后面的第一次攔截,但是,如果存在第一次攔截執(zhí)行完畢,那么允許他執(zhí)行第三次攔截,雖然是這樣說,但是也只是一個結(jié)論,我們選擇繼續(xù)添加一個類來進行處理:
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor3 implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Objecthandler) throws Exception {System.out.println("preHandle3....");return false;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {System.out.println("postHandle3....");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Objecthandler, Exception ex) throws Exception {System.out.println("afterCompletion3....");}
}
將preHandle2…的對應方法的返回值設置為true(修改回來),然后配置如下:
 <mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor1"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor3"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor2"></bean></mvc:interceptor></mvc:interceptors>
執(zhí)行看看結(jié)果:
/*
preHandle1....
preHandle3....
afterCompletion1....
*/
可以發(fā)現(xiàn),結(jié)論正確,但是為什么會這樣,按道理說,postHandle1…應該也會執(zhí)行啊,為什么沒有呢,實際上如果沒有false,那么結(jié)果應該是:
/*
preHandle1....
preHandle3....
preHandle2....
postHandle2....
postHandle3....
postHandle1....
1
afterCompletion2....
afterCompletion3....
afterCompletion1....
*/
其中中間的處理基本都是方法的處理,方法沒有進行處理,自然沒有視圖,沒有視圖,前端自然返回空白信息(響應體沒有信息),那么這個false會導致后面的不進行處理,但是afterCompletion1…對應的方法的攔截是在響應哪里,而這個由于preHandle1…是返回true的,他的方法是你造成的不處理,而不是我自己,所以說afterCompletion1…會進行打印,那么這個攔截的關(guān)系就有意思了,其中preHandle1…(以后就這樣說明了,一般這樣說是說他對應的方法,注意即可)的返回值的確影響后面的放行,但是對后面兩個方法的放行的影響是不同的,postHandle1…是根據(jù)true造成的放行來決定執(zhí)行的,也就是說,只要你放行了(true影響放行的參數(shù)),那么我就會執(zhí)行,但是如果你沒有放行,那么我自然也執(zhí)行不了,而afterCompletion1…只看你的返回值,而不看你是否放行,所以在單獨的攔截器的時候,true和false的結(jié)果都是他們兩個執(zhí)行或者不執(zhí)行,但是存在其他的攔截時,那么放行這個處理和判斷true的處理的區(qū)別就出現(xiàn)了,這也是為什么afterCompletion1…會執(zhí)行,但是postHandle1…不會執(zhí)行
為了驗證這樣的結(jié)果,我們修改配置文件:
 <mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor1"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor2"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor3"></bean></mvc:interceptor></mvc:interceptors>
執(zhí)行看看結(jié)果:
/*
preHandle1....
preHandle2....
preHandle3....
afterCompletion2....
afterCompletion1....
*/
可以發(fā)現(xiàn),之前的結(jié)論是正確的(后面先打印afterCompletion2…,是反序的原因)
那么還有個問題,為什么mvc的攔截器中,第一次攔截是順序的,其他兩個是反序的或者說倒序的:
解釋如下:
這個默認規(guī)則的原因是為了在執(zhí)行攔截器時提供更多的靈活性和可能性,考慮以下情況:
第一個攔截器通常用于做一些準備工作,如日志記錄、身份驗證等,按順序執(zhí)行有助于確保這些準備工作在控制器方法之前完成
控制器方法執(zhí)行后,倒序執(zhí)行其他攔截器可以用于清理工作、日志記錄和一些其他操作,這確保了在請求處理完畢后執(zhí)行這些操作,因為這些工作一般先創(chuàng)建的后清理
但是實際上順序的問題,大多數(shù)我們并不需要注意,并且,也存在可以修改順序的處理,只是可能需要某些版本(好像現(xiàn)在基本都不行了)
處理multipart形式的數(shù)據(jù):
前面雖然我們使用mvc處理過問題,但是還是有些操作我們沒有說明,比如方便我們操作的api,現(xiàn)在來補充一下:
首先加上依賴:
 <dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.1</version></dependency>
配置上傳文件解析器:
<bean id="multipartResolver"class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maxUploadSize" value="1000000000"/></bean>
<!--
MultipartResolver的組件在前面我們說過是操作上傳的,而:
public class CommonsMultipartResolver extends CommonsFileUploadSupport implements MultipartResolver, ServletContextAware {
即CommonsMultipartResolver是MultipartResolver的子類
-->
對應的前端(index.jsp):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="demo/upload"><input type="file" name="uploadFile"/><input type="submit" value="上傳"/>
</form>
</body>
</html>
對應的后端(我們創(chuàng)建FileController類):
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;@Controller
@RequestMapping("/demo")
public class FileController {@RequestMapping("upload")public String upload(MultipartFile uploadFile, HttpServletRequest request) throws IOException {// 文件原名,如xxx.jpgString originalFilename = uploadFile.getOriginalFilename();// 獲取文件的擴展名,如jpgString extendName =originalFilename.substring(originalFilename.lastIndexOf(".") + 1, originalFilename.length());String uuid = UUID.randomUUID().toString();// 新的文件名字(大多數(shù)服務器基本都是如此,這是保證文件不重復)String newName = uuid + "." + extendName;//String getRealPath(String path),返回包含給定虛擬路徑的實際路徑的字符串//這里的參數(shù)是/,那么代表就是當前項目的地址,假如你的項目名稱是test,那么就算test的絕對路徑String realPath =request.getSession().getServletContext().getRealPath("/");System.out.println(realPath);// 后面解決文件夾存放文件數(shù)量限制,所以按日期存放(文件名稱主要是UUID),因為不同的操作系統(tǒng)對一個目錄里面的文件數(shù)量的多少是有限的,一般情況下存在如下的限制(網(wǎng)上搜索到的):/*FAT32:FAT32 文件系統(tǒng)(常見于舊的 Windows 版本)對每個目錄有文件數(shù)目的限制,通常約為 65534 個文件NTFS:NTFS 文件系統(tǒng)(常見于現(xiàn)代 Windows 版本)支持更多的文件數(shù)目,通常數(shù)以百萬計ext4:在類Unix/Linux系統(tǒng)上,ext4 文件系統(tǒng)支持大量的文件,通常在數(shù)百萬到數(shù)十億文件之間,具體取決于文件系統(tǒng)的配置HFS+:蘋果的HFS+文件系統(tǒng)支持大量文件,數(shù)以百萬計APFS:蘋果的APFS文件系統(tǒng)支持更多文件,也在數(shù)百萬到數(shù)十億之間上面的這些數(shù)字代表一個目錄中可以存放的文件數(shù)量而正是由于有這些限制,所以一般情況下,我們的文件會放在不同目錄中,而按照日期的存放就非常好的(即存放一天中產(chǎn)生的文件數(shù)量)*/String datePath = new SimpleDateFormat("yyyy-MM-dd").format(new Date());//對應的項目路徑,加上日期的總體名稱,那么就是一個在項目里面的文件夾了File floder = new File(realPath + "/" + datePath);if (!floder.exists()) {  //判斷是否存在,如果存在那么就不用創(chuàng)建該目錄了floder.mkdirs();}//使用他的api,來操作,用一個新的File接收對應的File,組成一個路徑,然后加上對應的基本不重復的文件名稱,最終目錄和文件都存在了//并且將uploadFile對應的文件信息(他存在字節(jié)的,前面我們處理過了,比如byte[] bytes = file.getBytes();)給這個新的文件,或者說完成復制//一個保存文件信息的,基本必然是保存了字節(jié),所以其他的我們要保存文件進行封裝時,一般也與這里一樣,保存字節(jié)的uploadFile.transferTo(new File(floder, newName));return "success";}
}
然后如果你按照流程來的話,對應的視圖解析器應該是配置的,所以我們在index.jsp同級別創(chuàng)建一個success.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
1
</body>
</html>
執(zhí)行后,我們上傳一個文件或者說給一個文件,然后看看當前項目是否出現(xiàn)對應的文件或者目錄,然而并沒有,為什么
這里就需要考慮maven的操作與傳統(tǒng)的web的區(qū)別了,首先,我們可以選擇實現(xiàn)一個傳統(tǒng)的web處理(這里可以選擇看看第50章博客):
為此這里我也不得不給出三種創(chuàng)建web的方式,空項目,傳統(tǒng)web,maven項目
首先我們從空項目來進行處理,由于空項目并沒有使用到maven或者并沒有進行指定使用maven,所以一般空項目只能到傳統(tǒng)的web(即空項目到web一般與傳統(tǒng)的web項目是一樣的),然而這是一般的說法,基本上任何形式都可以從空項目變化而來,因為他就是在idea中,所以這里應該有五種方式:空項目(操作java程序),空項目到web,空項目到maven,(傳統(tǒng))web,maven等等
創(chuàng)建ser1空項目:

在這里插入圖片描述

然后進入如下:

在這里插入圖片描述

空項目是什么都沒有,有時也會包括模塊,一般情況下,一個項目是對應一個模塊的,如果沒有模塊,我們可以創(chuàng)建模塊:

在這里插入圖片描述

這里了解即可,創(chuàng)建模塊一般是如下:

在這里插入圖片描述

創(chuàng)建后,選擇關(guān)閉idea,打開我們創(chuàng)建好模塊的這個目錄即可,然后可以這樣:

在這里插入圖片描述

從上面我們可以看到,這個目錄后面有個sources root,這個再后面說明,他代表這個目錄下面是專門寫java的,所以這個時候創(chuàng)建的目錄是包的意思(否則不是)
直接執(zhí)行看看結(jié)果即可(一般sources root的出現(xiàn)除了配置外,執(zhí)行java代碼也會出現(xiàn),在項目中,他雖然也是文件夾,但是他也由于是項目,所以可以直接創(chuàng)建java文件(雖然不能再其里面的文件夾里面創(chuàng)建java文件),右鍵可以看到與其里面的目錄有是不同的選項的),但是一般情況下,我們需要src這個目錄,但是要明白src這個目錄其實也是認為創(chuàng)造的,在maven或者web中,使用而已,而單純的java并不一定使用他,但是為了統(tǒng)一所以我們可以選擇創(chuàng)建src這個包括來操作也就是:

在這里插入圖片描述

至此可以說空項目(操作java程序)操作完畢,現(xiàn)在我們來完成空項目到web:
空項目到web,需要這樣的處理,一般情況下,是需要很多的東西,當然我們也可以選擇創(chuàng)建一個web項目來觀察一下,然后再從空項目到web,歸根揭底,web和空項目的區(qū)別就是對應目錄賦予一些操作屬性,讓他作為資源文件,以及相關(guān)web中的對應文件也賦予屬性(由于是賦予的,所以對應的文件名稱并非需要固定,比如web對應的資源文件可以變成webb,只是有些插件或者說idea的某些自動處理(比如maven的依賴自動判斷文件名稱來自動配置)會處理這些文件名稱,所以我們大多數(shù)都會配置對應的固定文件,比如web中,配置war包,那么對應的文件若是web或者webapp通常會自動配置,這里也要注意,隨著時間的推移這些名稱可能會變,所以注意即可,一般好像只有webapp了,web已經(jīng)沒有了,如果在以前博客中有說明,那么大概是以前的某個版本或者是以前的操作(以前的版本也是可能會發(fā)生改變的,因為官方也是維護的,除非是固定的版本)),這些操作再老版本的idea中可能需要手動的處理,但是新版的一般并不存在這樣的處理了,或者忽略一些處理,所以這里我們來操作一些賦予這個操作:
其他的刪除,回到這里:

在這里插入圖片描述

在這里插入圖片描述

然后這樣:

在這里插入圖片描述

在這里插入圖片描述

這樣src就可以操作java代碼,而再xianmu的直接目錄里面就不行了,很明顯,這個選項是賦予這個目錄存放java或者被編譯器編譯的地方,更加可以說,編譯器只會去這個屬性中操作java代碼,自然導致操作發(fā)生改變(如這個時候右鍵xianmu這個目錄時,出現(xiàn)的選項發(fā)生變化了),這些操作之所以會這樣,其實是idea軟件自身處理的,或者idea也處理了jdk的某些配置,以及或者說idea他手動的幫我們選擇編譯以及執(zhí)行,然后將結(jié)果放在控制臺中(這個的顯示自然也可以認為是一個文件中,或者直接的顯示而不是文件中,這些操作都可以被我們處理,因為二進制就是可以這樣,二進制出現(xiàn)顯示還是復雜的,就算在操作系統(tǒng)中,顯示的處理一般也并沒有具體說明,因為是需要通過硬件完成對規(guī)定排列的映射的,這里了解即可)
好了,既然src的情況我們說明完畢,但是空項目操作(到)web還不知道如何操作,現(xiàn)在我們來操作一下:

在這里插入圖片描述

然后操作如下:

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

這樣對應的前面就會出現(xiàn)如下:

在這里插入圖片描述

這樣你就可以操作啟動服務器了(具體自行配置,可以看50章博客),如果啟動后,出現(xiàn)對應的數(shù)據(jù),那么操作成功,這就是空項目到web的處理,那么傳統(tǒng)web是如何處理(實際上也就是一次性到這里):
其中idea或者說web項目,通常會有隱藏的設置,即自動讀取在WEB-INF/lib/下面的jar包,這里了解即可,當然,其實我們也可以手動的指定,但是一般情況下,新版的可能沒有這樣的設置的,所以建議指定
一般情況下,傳統(tǒng)項目的處理基本只會在老版本的idea中,新版的沒有,具體在50章博客中可以看到,所以這里我們就不多說了
現(xiàn)在我們先操作一下這個(前面的"這里就需要考慮maven的操作與傳統(tǒng)的web的區(qū)別了"):

在這里插入圖片描述

先加上這個,具體的下載地址是如下:
鏈接:https://pan.baidu.com/s/1Yyd662YY99X7wEGIZ4FN7w
提取碼:alsk
然后配置如下:

在這里插入圖片描述

在這里插入圖片描述

點擊這個,在目錄中選擇lib文件或者直接選擇jar包都可,選擇文件說明是操作里面的所有jar包的
然后總體操作是如下:

在這里插入圖片描述

在web.xml中配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><servlet><servlet-name>ConfigServlet</servlet-name><servlet-class>servlet</servlet-class></servlet><servlet-mapping><servlet-name>ConfigServlet</servlet-name><url-pattern>/config</url-pattern></servlet-mapping>
</web-app>
補充對應的servlet的部分內(nèi)容:
  @Overridepublic void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {String realPath = servletRequest.getServletContext().getRealPath("/");System.out.println(realPath);}
啟動,訪問對應的config(記得加上),看看打印結(jié)果(這里就是我們需要查看的區(qū)別)
F:\xianmu\out\artifacts\xianmu_Web_exploded\
即他操作的是編譯后最終的結(jié)果,其實也可以看出來,他的操作在很大程度上是對應的這個結(jié)果:
我們可以在啟動時(查看下方啟動日志即可),查看臨時的tomcat(簡稱為臨時處理,或者說tomcat副本),比如我的就是:
C:\Users\33988\AppData\Local\JetBrains\IntelliJIdea2021.3\tomcat\062b022d-1657-4d8b-b4e9-da3eeae3d227\conf\Catalina\localhost
里面的配置文件就是這個:F:\xianmu\out\artifacts\xianmu_Web_exploded",即他還是操作臨時的tomcat
那么現(xiàn)在就剩下兩個了,即空項目到maven,以及直接的maven,空項目到maven其實也并不是很難:
我們創(chuàng)建如下的空項目:

在這里插入圖片描述

然后我們操作如此:

在這里插入圖片描述

配置這些:

在這里插入圖片描述

那么怎么變成maven項目呢,我們可以思考,要變成對應的這個項目,必然是需要使用到maven,這就需要我們配置使用了,具體如何配置看如下:
先創(chuàng)建pom.xml(與src同級別):
<?xml version="1.0" encoding="UTF-8"?>
<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>org.example</groupId><artifactId>testma</artifactId> <!--這里建議寫上與項目名稱一樣的,也就是maven--><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties></project>
但是他并沒有進行處理,我們需要如下的配置(maven本身也可以看成一個框架,而idea需要被他支持,就如空項目到web時需要指定jar包一樣,當然,也可以添加框架(實際上也就是對應的jar包),但是由于maven的jar比較多,所以這里我們就選擇添加框架了):

在這里插入圖片描述

一般來說,這個文件添加好后,右下角就會出現(xiàn)這個,當然,一般都會出現(xiàn)了,否則可能你需要重新的刪除在創(chuàng)建(因為基本沒有其他辦法來構(gòu)建maven,因為功能也并非都會提供了)
之后的選擇,基本選擇第一個即可,當然,還是需要看具體情況,最好翻譯一下
然后你可以選擇在其內(nèi)容加上:
<packaging>war</packaging>
一般這個時候,由于刷新了,那么他基本就出現(xiàn)了對應的顯示(M)
某種程度上,點擊右下角后,把這里出現(xiàn)的這個勾勾去掉也行:

在這里插入圖片描述

而maven的創(chuàng)建操作,我們就不處理了,因為我們操作過很多次了
至此,對應的五個方式(空項目(操作java程序),空項目到web,空項目到maven,(傳統(tǒng))web,maven)我們操作完畢,其中
maven操作web我們就不處理了,因為我們可以只設置打包方式即可,比較簡單
但是為了看看前面的問題,所以我們還是需要進行處理的,首先創(chuàng)建webapp文件:
如下:
如果前面再pom.xml中沒有寫上maven,而是testma,那么項目名稱后面一般會有[testma]

在這里插入圖片描述

依賴如下:
  <dependencies><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope></dependency></dependencies>
即對應的配置與前面的一樣,我們看看maven與傳統(tǒng)web的區(qū)別是什么:
我們經(jīng)過測試:
當操作方式為如下時會出現(xiàn)不同的結(jié)果(配置tomcat時,會出現(xiàn)的):
/*
操作方式是:
maven:war exploded:F:\maven\target\maven-1.0-SNAPSHOT\
maven:war:F:\Program Files\apache-tomcat-8.5.50\webapps\maven\
xianmu:Web exploded(之前的xianmu這個項目):F:\xianmu\out\artifacts\xianmu_Web_exploded\
對應與(下面如圖,以后看到這個,不要以為是后面沒有數(shù)據(jù),而是在下一行了):
*/
如圖:

在這里插入圖片描述

在這里插入圖片描述

至于他們里面的配置,你可以選擇的再引入或者添加時,點擊來自誰即可,也可以手動處理(一般exploded的基本相同,可能部分不同,但是好像也并不影響,注意即可,在選項中點擊+號一般就會知道了)
所以maven:war exploded和xianmu:Web exploded由于對應的類型的對象基本是一樣的是臨時的,我們可以測試看看:
首先進入maven:war時,對應的臨時處理,看看其配置文件的結(jié)果:
<Context path="/maven" docBase="F:\maven\target\maven-1.0-SNAPSHOT.war" />
F:\Program Files\apache-tomcat-8.5.50\webapps\maven\ //項目下面(maven一般是項目名稱的)
進入maven:war exploded,看看其結(jié)果:
<Context path="/maven" docBase="F:\maven\target\maven-1.0-SNAPSHOT" />
F:\maven\target\maven-1.0-SNAPSHOT\
進入之前測試的xianmu:Web exploded,看看其結(jié)果:
<Context path="/xianmu" docBase="F:\xianmu\out\artifacts\xianmu_Web_exploded" />
F:\xianmu\out\artifacts\xianmu_Web_exploded\
很明顯,帶有exploded的結(jié)果與配置文件一致,而沒有的,則是操作本來的tomcat,并且放在里面,為什么,這就需要一些隱藏的配置處理了,在明顯的指定war時,那么他的操作就會自動在本來的tomcat中處理,而不是臨時的處理目錄,這也可以說是默認的處理,具體情況還是看tomcat的源碼了,可能是因為需要這樣的情況才會弄出來吧
如果沒有值呢,是空值呢,如對應的代碼:
String realPath = servletRequest.getServletContext().getRealPath("/");
上面中的"/"不加,而是變成:
String realPath = servletRequest.getServletContext().getRealPath("");
結(jié)果如何:
經(jīng)過測試,默認的結(jié)果還是加上"/“的結(jié)果一樣,所以默認加上”/“,并且經(jīng)過測試,所以如果是getRealPath(“a”);或者getRealPath(”/a");,他們的結(jié)果都是"/a",這種情況得到的結(jié)果就是在路徑后面加上a(所以這個a我們最好寫成一下比較好的目錄,比如文件文件相關(guān)的目錄,比如uploads),所以這里參數(shù)的意思也就是加上參數(shù)得到的整體路徑的意思了,所以大多數(shù)為了得到完整的路徑,一般都會顯示的操作"/"即可
當然,為了在后面解決很多的疑問或者說以前的疑問,我決定統(tǒng)一的將web的mvc相關(guān)對于路徑的說法以及傳統(tǒng)web關(guān)于路徑的說法進行處理:
一般在web中關(guān)于路徑的說明存在多種,一般主要是四種,比如:轉(zhuǎn)發(fā),重定向,xml的路徑,注解的路徑等等
然而這些的說明在以前也說明了,這里就不多說,比如可以到67章博客看看(若有遺漏,也可以選擇到50章博客補充,如果還有,那么可以自己進行測試,一般加上50章博客的話是沒有遺漏的)
所以關(guān)于getRealPath路徑的區(qū)別我們說明完畢,回到之前的操作:
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;@Controller
@RequestMapping("/demo")
public class FileController {@RequestMapping("upload")public String upload(MultipartFile uploadFile, HttpServletRequest request) throws IOException {// 文件原名,如xxx.jpgString originalFilename = uploadFile.getOriginalFilename();// 獲取文件的擴展名,如jpgString extendName =originalFilename.substring(originalFilename.lastIndexOf(".") + 1, originalFilename.length());String uuid = UUID.randomUUID().toString();// 新的文件名字(大多數(shù)服務器基本都是如此,這是保證文件不重復)String newName = uuid + "." + extendName;//String getRealPath(String path),返回包含給定虛擬路徑的實際路徑的字符串//這里的參數(shù)是/,那么代表就是當前項目的地址,假如你的項目名稱是test,那么就算test的絕對路徑String realPath =request.getSession().getServletContext().getRealPath("/");System.out.println(realPath);// 后面解決文件夾存放文件數(shù)量限制,所以按日期存放(文件名稱主要是UUID),因為不同的操作系統(tǒng)對一個目錄里面的文件數(shù)量的多少是有限的,一般情況下存在如下的限制(網(wǎng)上搜索到的):/*FAT32:FAT32 文件系統(tǒng)(常見于舊的 Windows 版本)對每個目錄有文件數(shù)目的限制,通常約為 65534 個文件NTFS:NTFS 文件系統(tǒng)(常見于現(xiàn)代 Windows 版本)支持更多的文件數(shù)目,通常數(shù)以百萬計ext4:在類Unix/Linux系統(tǒng)上,ext4 文件系統(tǒng)支持大量的文件,通常在數(shù)百萬到數(shù)十億文件之間,具體取決于文件系統(tǒng)的配置HFS+:蘋果的HFS+文件系統(tǒng)支持大量文件,數(shù)以百萬計APFS:蘋果的APFS文件系統(tǒng)支持更多文件,也在數(shù)百萬到數(shù)十億之間上面的這些數(shù)字代表一個目錄中可以存放的文件數(shù)量而正是由于有這些限制,所以一般情況下,我們的文件會放在不同目錄中,而按照日期的存放就非常好的(即存放一天中產(chǎn)生的文件數(shù)量)*/String datePath = new SimpleDateFormat("yyyy-MM-dd").format(new Date());//對應的項目路徑,加上日期的總體名稱,那么就是一個在項目里面的文件夾了File floder = new File(realPath + "/" + datePath);if (!floder.exists()) {  //判斷是否存在,如果存在那么就不用創(chuàng)建該目錄了floder.mkdirs();}//使用他的api,來操作,用一個新的File接收對應的File,組成一個路徑,然后加上對應的基本不重復的文件名稱,最終目錄和文件都存在了//并且將uploadFile對應的文件信息(他存在字節(jié)的,前面我們處理過了,比如byte[] bytes = file.getBytes();)給這個新的文件,或者說完成復制//一個保存文件信息的,基本必然是保存了字節(jié),所以其他的我們要保存文件進行封裝時,一般也與這里一樣,保存字節(jié)的uploadFile.transferTo(new File(floder, newName));return "success";}
}
執(zhí)行后,我們上傳一個文件或者說給一個文件,然后看看當前項目是否出現(xiàn)對應的文件或者目錄,然而并沒有,為什么,就是因為由于其操作的是不帶有exploded的,所以到原本的tomcat中里面生成了,然而一般在idea中可能并不會顯示,因為他可能是只會顯示在起始的目錄(如target里面,也可能還要里面)或者其他的隱藏(F:\Program Files\apache-tomcat-8.5.50\webapps\maven\),這里了解即可
但是上面的操作中,由于沒有到當前項目的路徑,那么他是不是有問題的,實際上一般情況下,這只是不同系統(tǒng)中的tomcat的處理而已,而當我們發(fā)版(也就是部署到服務器提供給用戶使用時)時,一般在linux中,而這個系統(tǒng)下,一般都是當前項目所在,即指向的是當前項目里面(類似于前面的F:\Program Files\apache-tomcat-8.5.50\webapps\maven\),所以我們這個代碼是沒有問題的,只是環(huán)境不同而已,這個時候如果是沒有exploded的,就會到當前項目中(實際上發(fā)版的也就是這個),所以一般的,我們并不考慮在開發(fā)中的路徑處理,因為最后一定是沒有exploded的,即路徑在生產(chǎn)中是基本對的(開發(fā)代表在idea中操作(寫)代碼,生產(chǎn)代表已經(jīng)發(fā)版),當然idea中也可以考慮,只是需要一些設置而已(如路徑的處理,以及tomcat選擇的處理),這些可以百度查看,這里就不說明了
至此,對處理multipart形式的數(shù)據(jù)的一些補充,我們補充完畢
實際上對于UUID來說,也是可能出現(xiàn)重復,因為他存在如下的情況:
/*
UUID(通用唯一標識符)通常由以下元素組成:
時間戳:UUID 的一部分通常包含與其生成時間相關(guān)的信息,這有助于確保 UUID 在大多數(shù)情況下是唯一的,不同的 UUID 版本可以包含不同的時間戳信息
時鐘序列:UUID 可能包含一個時鐘序列,以防止在同一時刻生成多個相同的 UUID
節(jié)點標識符:在某些情況下,UUID 可能包含節(jié)點標識符,用于標識生成 UUID 的計算機或設備
版本號:UUID 包含一個版本號,指示生成 UUID 的算法或標準
變體號:UUID 包含一個變體號,表示 UUID 結(jié)構(gòu)的變體
隨機或偽隨機位:UUID 包含一些隨機或偽隨機位,以提高唯一性
不同的 UUID 版本具有不同的組成方式,但它們都旨在生成全局唯一標識符,最常見的 UUID 版本是基于時間的版本(例如,UUIDv1 和UUIDv2)和隨機版本(例如,UUIDv4),不同的應用程序和系統(tǒng)可以根據(jù)需求選擇適當?shù)?UUID 版本,UUID的常見表示形式是一個由32個十六進制字符組成的字符串,例如:"550e8400-e29b-41d4-a716-446655440000"
總之,UUID由多個組成部分組成,這些部分的結(jié)構(gòu)和含義取決于UUID的版本和標準,不過,UUID的主要目標是在全球范圍內(nèi)確保唯一性
UUID(通用唯一標識符)的設計目的是確保在理論上,生成的UUID在全球范圍內(nèi)是唯一的,UUID標準規(guī)定了生成UUID的方法,通常結(jié)合時間戳、節(jié)點標識符、時鐘序列和隨機數(shù)等元素來創(chuàng)建UUID,這些因素的組合使得生成相同UUID的機會非常低,以至于可以被認為是可以忽略不計的
然而,要理解的是,由于UUID的唯一性是基于生成UUID的算法和環(huán)境的,所以在極少數(shù)情況下,可能會出現(xiàn)重復的UUID,這種情況通常出現(xiàn)在以下情況下:
UUID生成算法不按標準實現(xiàn):某些自定義或不符合標準規(guī)范的UUID生成算法可能會導致UUID的重復
生成UUID的設備或系統(tǒng)出現(xiàn)問題:如果設備或系統(tǒng)在生成UUID時發(fā)生錯誤或故障,也可能導致UUID的重復
非標準UUID版本:某些應用程序或系統(tǒng)可能會實現(xiàn)自定義的UUID版本,這些版本可能不遵循標準規(guī)范,從而導致重復的UUID
特別的,如果是專門依靠操作時間的UUID,那么修改系統(tǒng)時間容易重復,就算不是也會提高重復的可能性,所以我們會認為修改系統(tǒng)時間會使得UUID重復的說法并不是錯誤的
盡管存在極少數(shù)情況下可能出現(xiàn)UUID重復的可能性,但在大多數(shù)情況下,UUID仍然是可靠的全局唯一標識符,適用于各種應用程序和系統(tǒng),特別是需要唯一性標識的情況,如數(shù)據(jù)庫記錄、分布式系統(tǒng)中的節(jié)點標識等,如果需要更高級別的唯一性保證,可以考慮結(jié)合其他標識方法,如數(shù)據(jù)庫自增主鍵或全局唯一的命名空間標識符(Namespace Identifier)
*/
在控制器中處理異常:
在前面我們知道有HandlerExceptionResolver這樣的組件,是來處理異常的,我們自然也會圍繞這個來處理(一般來說,對應注解的識別就是由他來處理的)
在controller包下創(chuàng)建GlobalExceptionResolver類:
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;// 可以讓我們優(yōu)雅的捕獲所有Controller對象handler?法拋出的異常
//@ControllerAdvice
@Controller
public class GlobalExceptionResolver {//mvc的異常處理機制(異常處理器)//當報錯后,會自動找到這個注解的方法,然后將錯誤信息給exception這個參數(shù)變量//這個里面的值一般代表了我們需要創(chuàng)建什么異常對象,將錯誤信息給這個對象,然后賦值給下面的參數(shù)變量//這里就可能有疑問了,為什么不自動識別類型來創(chuàng)建對象呢,因為怕他是接口,保證多態(tài)的@ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView = new ModelAndView();modelAndView.addObject("msg", exception.getMessage());modelAndView.setViewName("error");return modelAndView;}@RequestMapping("err")public String ix(String a, Integer b) throws IOException {System.out.println(1);int i = 1 / 0;return "err";}
}
對應的jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
錯誤是:${msg}
</body>
</html>
一般情況下,我們訪問err(具體訪問自己應該知道了),一般來說異常不會經(jīng)過第二個攔截,這是因為出現(xiàn)異常自然不會考慮數(shù)據(jù)或者視圖的問題,所以一般就會規(guī)定不會經(jīng)過第二次攔截,只會依靠固有的數(shù)據(jù)或者視圖進行處理異常,即:
modelAndView.addObject("msg", exception.getMessage());
modelAndView.setViewName("error");
所以注釋這個:
//    @ExceptionHandler(ArithmeticException.class)
自然第二個攔截沒有,且第三個攔截打印出對應的信息(我們寫上的),所以他的作用只是在出現(xiàn)異常時,進一步的處理視圖操作,否則按照默認的異常視圖進行處理,你可以在瀏覽器看看注釋后的界面就知道了
一般情況下,寫在當前controller中的對應的異常處理,只會對自身進行生效,并且,如果存在多個,比如:
 @ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView = new ModelAndView();modelAndView.addObject("msg", exception.getMessage());modelAndView.setViewName("error");return modelAndView;}@ExceptionHandler(ArithmeticException.class)public ModelAndView handleException1(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView = new ModelAndView();modelAndView.addObject("msg", exception.getMessage()+"22");modelAndView.setViewName("error");return modelAndView;}
那么會報錯,由上所述,我們應該需要統(tǒng)一的異常處理,所以我們需要一個全局的處理,那么我們再次的創(chuàng)建一個類:
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletResponse;@Controller
public class Global {@ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView = new ModelAndView();modelAndView.addObject("msg", exception.getMessage());modelAndView.setViewName("error");return modelAndView;}
}
然后把GlobalExceptionResolver類中的異常處理都進行刪除,然后啟動看看,是否在這個類里面處理了異常,發(fā)現(xiàn)并沒有,說明的確對應的異常只能操作自身的controller的,那么怎么將這個異常處理變成全局呢,我們操作如下:
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletResponse;@ControllerAdvice
@Controller
public class Global {@ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView = new ModelAndView();modelAndView.addObject("msg", exception.getMessage());modelAndView.setViewName("error");return modelAndView;}
}
現(xiàn)在我們繼續(xù)執(zhí)行,會發(fā)現(xiàn)他處理異常了,也就是說,@ControllerAdvice可以使得這個類里面或者說這個controller里面的異常的那個處理變成全局的處理,當然,并不必須需要他是controller(寫上也沒有關(guān)系),首先controller的主要作用是定位(如果不作為全局異常的話,兩個都可以單獨寫上進行處理),而@ControllerAdvice也是算一個定位,只不過他是異常的定位而已,當然,你寫上controller也沒有關(guān)系,可以在不處理異常時,作為controller使用也行,而在處理異常時,@ControllerAdvice可以作為controller使用的(雖然他并不是),所以這個時候可以不加對應的controller,照樣的可以進行操作(因為@ExceptionHandler只有一個,也只能有一個,否則報錯)
那么如果存在多個全局呢,誰先使用,答:看下圖

在這里插入圖片描述

經(jīng)過大量的測試,發(fā)現(xiàn),由于windows中,A和a是一樣的,所以可以得到:當字母的數(shù)量或者說文件名長度相等時,按照Ascii來決定,越小那么就越優(yōu)先(上面沒有測試數(shù)字,實際上數(shù)字也是的,由于1<a,那么Gloaa1優(yōu)先于Gloaaa),否則的話,長度越長越優(yōu)先
當然,上面的說明并不重要,因為全局的一個就夠了
基于Flash屬性的跨重定向請求數(shù)據(jù)傳遞:
在前面我們知道一個組件FlashMapManager,一般就用于這里
重定向時請求參數(shù)會丟失,我們往往需要重新攜帶請求參數(shù),我們可以進行?動參數(shù)拼接如下:
我們創(chuàng)建一個類:
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
public class Redirect {@RequestMapping("/re")public String re() {return "redirect:ree?name=" + 1;
//        return "redirect://ree?name=" + 1; 并不到http,這是一個區(qū)別(雖然傳統(tǒng)重定向會,但是這里是經(jīng)過項目(或者說框架)的處理的)
//        return "redirect:/ree?name=" + 1; 當前和到項目的區(qū)別(并不到端口,一個區(qū)別)}@RequestMapping("/ree")public String ree(String name) {System.out.println(name);System.out.println(1);return "ree";}
}
先訪問re,看看結(jié)果吧
上述拼接參數(shù)的方法屬于get請求,攜帶參數(shù)?度有限制,參數(shù)安全性也不?(因為在url上),此時,我們可以使?SpringMVC提供的?ash屬性機制,向上下?中添加?ash屬性,框架會在session中記錄該屬性值,當 跳轉(zhuǎn)到??之后框架會?動刪除?ash屬性,不需要我們?動刪除,通過這種方式進行重定向參數(shù)傳遞, 參數(shù)?度和安全性都得到了保障,如下:
繼續(xù)在上面的類中加上方法:
 @RequestMapping("/reee")public String reee(RedirectAttributes redirectAttributes,HttpSession session) {// addFlashAttribute?法設置了一個flash類型屬性,該屬性會被暫存到session中(注意,他不是session的存儲的說明,只是在這個會話的其他區(qū)域而已,所以后面的name為null)// 但它不會永久保留,這個屬性的存儲時間很短暫,通常只會在兩次請求之間有效// 所以也可以說在跳轉(zhuǎn)到??之后該屬性銷毀(雖然是兩次請求之間),并且他的銷毀在于重定向參數(shù)賦值或者說處理后進行的//也就是說,下面的reeee方法的name2的結(jié)果就是nullredirectAttributes.addFlashAttribute("name", "22");Object name = session.getAttribute("name");System.out.println(name); //null,只是在session中的對應區(qū)域,而不是session的專門區(qū)域,雖然都在session中,但是是不同的區(qū)域的,這里需要注意(實際上session只是一個范圍,而flash通常在這個范圍里面,所以并不能說明flash就一定在session中,只是基本在里面而已(因為你可以修改源碼而進行處理,雖然大多數(shù)并不會這樣做,且沒有意義))return "redirect:reeee?name=" + 1;}@RequestMapping("/reeee")//@ModelAttribute可以獲取存在model數(shù)據(jù)的存在,類似于jsp的${varName}方式,具體可以在第52章博客中知道
//但是如果你并沒有處理這些數(shù)據(jù),那么他自然按照原始的處理,也就是String na的處理,相當于不加這個注解(其實他默認處理了,但是由于這個注解的存在,導致操作覆蓋了)public String reeee(String name, HttpSession session, @ModelAttribute("name") String na, ModelAndView modelAndView) {System.out.println(modelAndView);System.out.println(na); //22,得到了String name2 = (String) session.getAttribute("name");System.out.println(name2); //nullSystem.out.println(44444444);System.out.println(name);Object name1 = session.getAttribute("name");System.out.println(name1);return "ree";}
執(zhí)行后,測試一下吧,但是為什么flash(英文是顯示的意思)類型屬性只會在兩次請求之間有效,解釋如下:
/*Flash類型屬性通常是在Web開發(fā)中用于在一次HTTP請求中傳遞數(shù)據(jù)到下一次HTTP請求的一種機制,這種屬性只在兩次請求之間有效的原因涉及到Web應用程序的工作原理和HTTP協(xié)議的無狀態(tài)性
HTTP協(xié)議是一種無狀態(tài)協(xié)議,每個HTTP請求都是獨立的,服務器不會在不同請求之間保存客戶端的狀態(tài)信息,這意味著每個HTTP請求都是相互獨立的,服務器不會自動記住之前請求的信息,Flash屬性的作用就是在這種無狀態(tài)協(xié)議下,將數(shù)據(jù)從一個請求傳遞到下一個請求,實現(xiàn)一種有狀態(tài)的體驗
Flash屬性的工作原理通常如下:
1:在第一次HTTP請求中,服務器設置Flash屬性,將數(shù)據(jù)存儲在它里面(如前面的redirectAttributes.addFlashAttribute("name", "22");),這個時候數(shù)據(jù)還是在第一次請求中的,這是存放的位置是專門用來進行后面操作的位置或者對象的
2:服務器將Flash屬性(專門的數(shù)據(jù))中的數(shù)據(jù)發(fā)送給客戶端作為響應
3:客戶端收到響應后,可以從Flash屬性中提取數(shù)據(jù)并在下一次HTTP請求中發(fā)送回服務器(也可以認為是專門給下一個請求的,也放在一個專門的區(qū)域,當給下一個請求后,或者賦值后,自動清空這個區(qū)域的對應數(shù)據(jù)(并且也并不是地址的操作,只是數(shù)據(jù)而已))
4:在第二次HTTP請求中,服務器可以讀取Flash屬性中的數(shù)據(jù)(前端給的,在http協(xié)議中,是可以選擇的進行處理,一般來說前端給后端是放在請求信息中的,而后端給前端是放在響應信息的),完成數(shù)據(jù)的傳遞,這個時候在特定時候(前面說明了)進行清空對應在服務端的Flash屬性中的數(shù)據(jù)我們需要注意的是:雖然他們存在響應信息或者請求信息中,但是一般瀏覽器并不會讓我們查看到對應的請求信息或者響應信息(除了一些其key-value或者字符串外,基本上一些二進制數(shù)據(jù)不會顯示,這里可以參照前面的文件上傳的那個請求體,當然,響應體一般可以看到,但是中間是否省略了什么就不確定了)
反正不管怎么樣,前端和后端,在按照合理的方式不會讓我們用戶看到Flash屬性的數(shù)據(jù),并且滿足二次請求銷毀的成果
當然,如果技術(shù)夠高,可以選擇破解瀏覽器來獲取這個數(shù)據(jù),所以盡管可能有安全措施來保護你(比如瀏覽器自身),但是網(wǎng)絡中的絕對安全性是不存在的,但采取適當?shù)陌踩胧┛梢燥@著提高您的個人信息和數(shù)據(jù)的安全性,這也強調(diào)了個人隱私和安全的重要性,以及采取適當?shù)拇胧﹣肀Wo自己的信息
*/
至此解釋完畢,現(xiàn)在我們來?寫 MVC 框架,上面雖然說明了很多知識,但是基本只有手寫出來,我們才能知道更加深層次的東西,現(xiàn)在開始手寫MVC框架:
回顧SpringMVC執(zhí)行的?致原理,后續(xù)根據(jù)這個模仿?寫自己的mvc框架

在這里插入圖片描述

spring的維護是必須的,因為他最終保存的對象(數(shù)據(jù),內(nèi)容)是用來判斷映射的,難道是憑空保存或者直接保存嗎,總需要一個總地方吧(spring的地方)
具體的說明就是:前端控制器進行初始化的配置,而掃描交給spring來處理(springmvc有spring的),然后根據(jù)前端控制器初始化的信息(在一個servlet中,是可以操作同一個請求頭和響應頭的,或者說請求信息和響應信息),來決定調(diào)用誰,也就是映射,后面進行一系列的處理(如jsp的響應),最終得到我們的結(jié)果,具體說明看后面就知道了,我們手寫一個類似的即可
手寫MVC框架之注解開發(fā)(也可以存在對應的xml開發(fā),只是對與mvc來說,通常是需要注解的,單純的xml并不好處理,具體可以百度,一般mybatis,spring,springmvc基本都是需要xml和注解一起的,反正那個方便使用那個):
通常來說,注解的性能會比xml慢點(通常指啟動的時候,有時候運行時也會,但是方便許多,特別的是考慮到注解的掃描(當包非常多時,有些不需要的可能也會掃描到))
在手寫之前,我們創(chuàng)建一個項目:

在這里插入圖片描述

web.xml中是:
<!--
<?xml version="1.0" encoding="UTF-8"?>
-->
<!--
大多數(shù)情況下:<?xml version="1.0" encoding="UTF-8"?>的操作是可以選擇不寫的,但是,大多數(shù)關(guān)于xml解析的代碼,或多或少都會處理這個,雖然大多數(shù)默認都是這樣的處理,也就是<?xml version="1.0" encoding="UTF-8"?>,只是我們還是建議寫上,那么這里操作了xml,自然最終也會操作xml解析(如果可以你也能自己寫一個),并且由于是服務器的處理,其中自然有內(nèi)部的關(guān)于web.xml的讀取方式,現(xiàn)在先這樣寫上,后面會補充
-->
<web-app></web-app>
現(xiàn)在開始編寫,首先,我們創(chuàng)建com.mvc.framework包,然后再該包下創(chuàng)建DispatcherServlet類:
上面是需要服務器的包的,自然需要引入,也就是:
   <dependencies><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope> <!--開發(fā)時操作,可以驗證前面的tomcat有且自動給出對應的依賴--></dependency></dependencies>
然后我們創(chuàng)建這個類(包名自行創(chuàng)建,可以看下面的這個:package com.mvc.framework;來創(chuàng)建):
package com.mvc.framework;import javax.servlet.http.HttpServlet;//唯一的一個servlet
public class DispatcherServlet extends HttpServlet {
}
然后修改web.xml(補充服務器的讀取方式,這是固定的,除非你修改服務器,也就是tomcat的處理(servlet),當然,由于框架是建立在這個上面的,所以自然是保留的):
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><display-name>create</display-name><servlet><servlet-name>mvc</servlet-name><servlet-class>com.mvc.framework.DispatcherServlet</servlet-class></servlet><servlet-mapping><servlet-name>mvc</servlet-name><url-pattern>/*</url-pattern></servlet-mapping>
</web-app>
<!--寫上后,這個/*,就會操作這個DispatcherServlet的servlet的處理-->
這個超級熟悉了吧,但是我們需要明白,這里是操作原生的servlet的(我們mvc是建立在他之上的,具體到50章博客學習),我們在操作映射之前,首先需要得到對應的對象來操作方法,從而操作映射,所以我們需要定義一些注解,或者說,完成Spring相關(guān)的操作
現(xiàn)在,我們在framework包下創(chuàng)建servlet包,將DispatcherServlet移動到這個包里面,然后再在framework包下創(chuàng)建annotations包,然后在該包下創(chuàng)建幾個注解:
package com.mvc.framework.annotations;import java.lang.annotation.*;@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {String value() default "";
}
package com.mvc.framework.annotations;import java.lang.annotation.*;@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {String value() default "";
}
package com.mvc.framework.annotations;import java.lang.annotation.*;@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {String value() default "";
}
package com.mvc.framework.annotations;import java.lang.annotation.*;@Documented
@Target(ElementType.FIELD )
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {String value() default "";
}
很明顯,我們需要操作Controller到Service中進行處理的,注解我們定義好了,現(xiàn)在我們開始進行開發(fā)
為了操作到Service的注解的處理,我們需要在framework包下,創(chuàng)建service包,再創(chuàng)建如下的兩個類或者接口:
package com.mvc.framework.service;public interface DemoService {String get(String name);
}
package com.mvc.framework.service.impl;import com.mvc.framework.service.DemoService;public class DemoServiceImpl implements DemoService {@Overridepublic String get(String name) {System.out.println("打印:"+name);return name;}
}
回到DispatcherServlet,在這里自然是需要進行包的掃描的,并且這個掃描需要知道往那里進行掃描(這里考慮在mvc相關(guān)xml中進行處理),然后Spring管理掃描后的結(jié)果,最后進行映射的處理,現(xiàn)在首先是處理掃描:
補充DispatcherServlet:
package com.mvc.framework.servlet;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;//唯一的一個servlet
public class DispatcherServlet extends HttpServlet {//可以選擇加載配置文件,而得到一些相關(guān)的信息,來給后面的內(nèi)容進行數(shù)據(jù)的判斷(比如注解中,只能是post等等,而判斷是否報錯)@Overridepublic void init(ServletConfig config) throws ServletException {//加載配置文件//掃描相關(guān)的類,有時候mvc的保存實例的對象通常與單純的spring的實例對象(這里是說明的ioc容器對象,而不是單獨的一個里面的對象,因為"有時候mvc的保存實例的對象"中有"保存"二字)是不同一樣,雖然他們可能存在上下級(在67章博客可能有具體的說明)//但是尋常來說,這種上下的級別,一般也是代碼層面的,如果可以,自行編寫時,可以選擇忽略這種情況,只是比較麻煩,這是由于mvc在處理時//一般來說,無論這些解釋說的在怎么的花里胡哨,底層的原因,只是是否可以訪問到對方而已,一般來說mvc比較慢,并且他可能是一個新的實例//所以mvc在操作spring時,可能會拿到他已經(jīng)操作好的實例對象(就如在一個方法中,后面的變量得到訪問變量的值,但是前面的變量不能訪問后面的,而可以訪問,自然可以完成賦值(即mvc可以注入spring的對象)),反過來就不行,但是我們也可以手動的進行給spring進行注冊//這就會涉及到底層代碼之間的修改,因為他們的問題是順序和訪問權(quán)限(不能直接訪問對象,只能得到,并且設置的,也是在對應中才能操作,受訪問權(quán)限影響)的問題,受框架自身的影響,所以只能是底層代碼之間的修改了//但是也要明白,由于容器是同一個,所以如果不考慮過程中的賦值的話,其實他們并沒有上下級別,只是既然使用了spring的話,那么我們自然會在容器處理后,直接的操作,這是為什么spring和mvc雖然是同一個容器,但是存在上下級的原因//比如說,現(xiàn)在spring會操作兩個對象,假設是a,b,ioc容器我們假設為ioc,mvc也存在對象,我們認為是c//所以手寫spring將a,b加入到ioc中,那么ioc有a,b了,這個時候,像一些注入,會到ioc中的信息//這個時候并沒有c,所以spring不能訪問c,處理了這些操作后,mvc才會將c給ioc,這個時候mvc在處理這些時,可以得到a,b,因為ioc有這個,所以上下級關(guān)系只是順序問題,但是這個上下級別的關(guān)系也只是體現(xiàn)在初始化的處理中,最終他們都是在ioc中,如果可以的話,當你拿取ioc容器,那么就可以得到這些信息,這個在某種情況下,由于順序問題的,我們會稱為父子容器,并在這種考慮下,我們會認為他們是不同容器,雖然本質(zhì)是同一個,但是單純以數(shù)據(jù)來說的話,他們是不同的,所以如果一些博客說明是不同的,那么大多數(shù)是建立在數(shù)據(jù)不同,而不是容器本身}//處理請求的地方,一般情況下mvc中g(shù)et和post是操作同一個方法,而該方法是統(tǒng)一進行處理的,為了方便這樣的處理,我們就在get中調(diào)用post即可,雖然之前根據(jù)請求方式來決定調(diào)用誰,這樣處理就會使得無論是何種方式,都是同一個方法,由于參數(shù)互通,自然可以選擇得到請求方式來判斷與注解的是否一致,而不是看方法名使得他的請求方式就會變(這自然是不合常理的)//原來mvc中是:public class DispatcherServlet extends FrameworkServlet {/*他里面存在:protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.processRequest(request, response); //都是這個方法}protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.processRequest(request, response);}*///重寫一下HttpServlet對應的兩個方法@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req,resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
}
由于這個是主要的操作,所以相關(guān)的代碼就應該寫在對應的doGet或者doPost的,具體細節(jié)在前面可能有些許的說明,但是還是需要看這里怎么處理
我們繼續(xù)補充或者修改(可以將get也到post,因為post可以包含get的處理,一般來說mvc也是如此,但是他一般是get操作get,post操作post(但是是同一個方法),與HttpServlet相關(guān),具體是操作了service,只是在中間可能會操作注解(前提是設置了),判斷是否是對應的請求方式而進行報錯的,比如進入到了get,那么在里面判斷注解是否是get相關(guān),可以選擇再次的得到請求,而進行補充判斷):
 @Overridepublic void init(ServletConfig config) throws ServletException {//加載配置文件//掃描相關(guān)的類//初始化bean對象(ioc容器)//實現(xiàn)依賴注入//構(gòu)造一個HandlerMapping處理器映射器,來完成映射關(guān)系//映射關(guān)系創(chuàng)建好,那么根據(jù)請求查看映射,來決定調(diào)用誰}
在資源文件夾下補充mvc的配置文件mvc.xml:
<beans>
<component-scan base-package="com.mvc.framework"/>
</beans>
在web.xml中加上如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><display-name>create</display-name><servlet><servlet-name>mvc</servlet-name><servlet-class>com.mvc.framework.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><!--編寫方式受我們源碼的處理,由于是服務器的處理,那么這里我們就這樣寫
當然,由于只是值,這里也可以寫成任何只要你可以得到路徑的地址即可,比如甚至可以寫絕對路徑,這都看你如何處理了,到時候如何處理和使用什么方式來操作都需要自行處理的,當然,大多數(shù)我們并不會記住關(guān)于讀取文件或者說讀取項目或者找當前項目路徑的相關(guān)api,這個時候可以選擇網(wǎng)上查找,這并不難以找到
--><param-value>mvc.xml</param-value><!--直接這樣寫可能會報錯,這是可能idea判斷的問題(認為你要加上classpath:,即classpath:mvc.xml),實際上這里可以隨便寫數(shù)值,所以忽略即可,idea的提示也并不是萬能的--></init-param></servlet><servlet-mapping><servlet-name>mvc</servlet-name><url-pattern>/*</url-pattern></servlet-mapping>
</web-app>
回到后端初始化,在這里寫上如下:
package com.mvc.framework.servlet;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;//唯一的一個servlet
public class DispatcherServlet extends HttpServlet {@Overridepublic void init(ServletConfig config) throws ServletException {//加載配置文件//手寫獲取對應的指定的值(如果說mvc中這個名稱不可改變,那么說明這里是寫死的)//這個是讀取web.xml的,雖然web.xml是固定的,但是里面的數(shù)據(jù)是mvc進行處理的,比如這里//因為我們需要時刻記住,mvc是建立在servlet之上的,而不是單獨的框架,所以需要servlet,否則mvc是自然處理不了的String contextConfigLocation = config.getInitParameter("contextConfigLocation");//得到之后,需要根據(jù)這個來確定路徑,使得加載配置文件String s = doLoadconfig(contextConfigLocation);//掃描相關(guān)的類doScan(s);//初始化bean對象(ioc容器),也就是根據(jù)掃描得到的全限定名創(chuàng)建對象并保存doInstance();//實現(xiàn)依賴注入//維護和處理(操作)依賴注入關(guān)系doAutoWired();//上面三個在上一章博客中,或者說108章博客中就有操作,當然,這里可以選擇操作一樣的,也可以不一樣,只需要實現(xiàn)即可(雖然基本存在多種方式,但是一般是108章那一種,還有spring的那一種(三級緩存))//這里我們還是選擇考慮108章博客的處理//構(gòu)造一個HandlerMapping處理器映射器,來完成映射關(guān)系initHandlerMapping();//當然,在上一章博客時,對應的map是一個全局的,所以都是同一個容器的(也說明了是同一個,只是順序問題而已)System.out.println("初始化完成...,等待請求與映射匹配了");//映射關(guān)系創(chuàng)建好,那么根據(jù)請求查看映射,來決定調(diào)用誰}//構(gòu)造一個HandlerMapping處理器映射器private void initHandlerMapping() {}//實現(xiàn)依賴注入private void doAutoWired() {}//初始化類private void doInstance() {}//掃描類,或者掃描到注解,參數(shù)是在那里掃描private void doScan(String path) {}//加載配置文件,得到信息,這里比較簡單,只需要得到地址即可private String doLoadconfig(String contextConfigLocation) {return "";}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
}
實際上在上面使用靜態(tài)塊也可以操作,只是沒有對應的請求數(shù)據(jù),所以我們使用初始化,一般情況下,我們需要xml相關(guān)依賴來讀取對應的xml信息:
那么我們加上如下的依賴:
 <dependency><groupId>dom4j</groupId><artifactId>dom4j</artifactId><version>1.6.1</version></dependency><dependency><groupId>jaxen</groupId><artifactId>jaxen</artifactId><version>1.1.6</version></dependency>
在真正編寫之前,我們需要補充一些代碼,在framework包下,創(chuàng)建controller包,然后在里面創(chuàng)建如下的類:
package com.mvc.framework.controller;import com.mvc.framework.annotations.Autowired;
import com.mvc.framework.annotations.Controller;
import com.mvc.framework.annotations.RequestMapping;
import com.mvc.framework.service.DemoService;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Controller
@RequestMapping("/demo")
public class DemoController {@Autowiredprivate DemoService demoService;@RequestMapping("/query")public String query(HttpServletRequest request, HttpServletResponse response, String name) {String s = demoService.get(name);return s;}}
然后在DispatcherServlet中添加如下的代碼:
package com.mvc.framework.servlet;import com.mvc.framework.annotations.Autowired;
import com.mvc.framework.annotations.Controller;
import com.mvc.framework.annotations.RequestMapping;
import com.mvc.framework.annotations.Service;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;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.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;//唯一的一個servlet
//@WebServlet(name = "1", urlPatterns = "/a")
public class DispatcherServlet extends HttpServlet {//這里也可以選擇不加靜態(tài),具體看你自己//存放每個類對應的全限定名(這里其實可以不用靜態(tài)的,因為他只是在這里處理而已,而幾乎不會給其他對象或者說類來使用),最后統(tǒng)一操作他來創(chuàng)建對象,如果是同一個容器,那么這個應該不是同一個,他只是記錄當前掃描的,也就是說,對應的容器應該是更加上層的,當然//這里是以mvc為住,所以對應的容器,或者說map就放在這里了private static List<String> classNames = new ArrayList<>();//需要一個bean的存放(ioc容器)private static Map<String, Object> map = new HashMap<>();//緩存已經(jīng)進行過依賴注入的信息private static List<String> fieldsAlreayProcessed = new ArrayList<>();//url和method的映射關(guān)系private static Map<String, Method> handlerMapping = new HashMap<>();@Overridepublic void init(ServletConfig config) {//加載配置文件//手寫獲取對應的指定的值(如果說mvc中這個名稱不可改變,那么說明這里是寫死的)//這個是讀取web.xml的,雖然web.xml是固定的,但是里面的數(shù)據(jù)是mvc進行處理的,比如這里//因為我們需要時刻記住,mvc是建立在servlet之上的,而不是單獨的框架,所以需要servlet,否則mvc是自然處理不了的String contextConfigLocation = config.getInitParameter("contextConfigLocation");//得到之后,需要根據(jù)這個來確定路徑,使得加載配置文件String s = doLoadconfig(contextConfigLocation);//掃描相關(guān)的類doScan(s);//初始化bean對象(ioc容器),也就是根據(jù)掃描得到的全限定名創(chuàng)建對象并保存doInstance();//實現(xiàn)依賴注入//維護和處理(操作)依賴注入關(guān)系doAutoWired();//上面三個在上一章博客中,或者說108章博客中就有操作,當然,這里可以選擇操作一樣的,也可以不一樣,只需要實現(xiàn)即可(雖然基本存在多種方式,但是一般是108章那一種,還有spring的那一種(三級緩存))//構(gòu)造一個HandlerMapping處理器映射器,來完成映射關(guān)系initHandlerMapping();//當然,在上一章博客時,對應的map是一個全局的,所以都是同一個容器的(也說明了是同一個,只是順序問題而已)System.out.println("初始化完成...,等待請求與映射匹配了");//映射關(guān)系創(chuàng)建好,那么根據(jù)請求查看映射,來決定調(diào)用誰}//方法的位置,也建議從上到下,這里卻反過來了,當然,這只是模擬mvc框架的操作,并不會有很大影響,以后注意即可//構(gòu)造一個HandlerMapping處理器映射器private void initHandlerMapping() {//這里基本上是比spring的依賴操作更加的有難度(在前一章博客中,我們幾乎學習過spring的源碼,所以后面的依賴操作我們幾乎可以明白)//但是這里還是第一次,所以需要多次的理解(雖然對只看前一章來說也是第一次,但是對比這里的第一次,難度還是較小的,這里的難道比較大)//因為依賴的處理主要是死循環(huán)的解決,在這里的,我們判斷當前類全限定名,加上變量的總名稱,判斷是否賦值過即可,賦值過,那么不賦值,直接退出,然后根據(jù)遞歸,自然都會處理完畢//遞歸在中途也會順便的解決過程中的對象,這樣也會使得后續(xù)更加的方便(當然,那基本也是類似三級緩存的處理,都是找到對應的類型,賦值后結(jié)束)//其中spring在三級緩存拿取結(jié)束,而這里也是在對應的map拿取結(jié)束,只是spring的是當時創(chuàng)建然后賦值后刪除的,所以并不需要考慮是否可以賦值的問題(賦值就結(jié)束了,也不會到里面去的,因為當時創(chuàng)建,他們必然只需要賦值即可),而是直接的結(jié)束//而spring沒有,所以需要考慮是否可以賦值,即判斷名稱來結(jié)束//上面的看看就行,這是說明為什么spring是較小難道的原因,因為只是一個循環(huán)依賴的問題的處理(其實也正是他,所以是較小難度,否則是沒有難度的)//現(xiàn)在我們開始操作//最關(guān)鍵的環(huán)節(jié)//將url和method建立關(guān)聯(lián)//這里首先需要大致吃透前面的mvc的知識的細節(jié),并且我們也需要觀看27章博客的最后一部分,這樣才基本可以明白,甚至更加的可以看懂//當然了,我也會寫上注釋的,讓你更加的看得懂//首先我們需要思路,也就是我們現(xiàn)在已經(jīng)有了對于的對象實例了,我們自然可以通過該實例得到里面的方法上面的相應注解//然后根據(jù)當前類實例的上面的注解進行拼接路徑,得到需要的url的路徑攔截,然后我們可以創(chuàng)建map保存攔截(路徑)和方法的對應的映射關(guān)系//這樣在真正操作請求時,判斷請求路徑與路徑攔截,而進行執(zhí)行哪個方法,具體思路是這樣的,但是實現(xiàn)需要看后面的處理//當然,這些是初始化操作,所以前面說明組件時,第一步是調(diào)用處理器映射器,而不是創(chuàng)建(因為已經(jīng)初始化了)if (map.isEmpty()) { //如果map為空,自然直接的退出return;}for (Map.Entry<String, Object> entry : map.entrySet()) {//獲取這個實例的ClassClass<?> aClass = entry.getValue().getClass();//如果包含這個注解,那么操作他,否則不會(只有這個注解才操作路徑),當然,我們一般建議先操作不存在時直接的結(jié)束當前循環(huán)//這樣其實更加的好維護,防止后續(xù)的代碼執(zhí)行不對(這是一個寫法),當然,只要合理都可,只是建議而已//所以我們以后需要這樣的規(guī)范,即所有需要結(jié)束的判斷寫在前面,正確的處理寫在后面,盡量將可以操作結(jié)束的判斷(比如這里的不存在)提取出來寫在前面//因為這樣就可以很明顯的,知道一個方法里面需要滿足什么條件,而不是需要去在后面翻找(正常情況下,對應后面代碼也是較好處理的,所以不考慮代碼無效定義的說明,只考慮找到的問題),這里由于比較少的代碼,所以我們并不是特別在意//但是在以后開發(fā)大代碼時,建議這樣哦(在任何的作用域都希望如此)if (aClass.isAnnotationPresent(Controller.class)) {String baseUrl = "";//看看有沒有對應的路徑的注解if (aClass.isAnnotationPresent(RequestMapping.class)) {//拿取對應的值String value = aClass.getAnnotation(RequestMapping.class).value();//按照對應已經(jīng)寫好的DemoController,那么這里的value相當于拿到了@RequestMapping("/demo")中的"/demo"這個字符串值baseUrl += value; //拿取前綴}//當然,如果上面的沒有拿取前綴,自然還是""//現(xiàn)在我們拿取方法//輸入itar,一般會出現(xiàn)下面的,相當于快捷鍵吧(具體可能是自定義的,且根據(jù)的上面的第一個數(shù)組來處理的)Method[] methods = aClass.getMethods();for (int j = 0; j < methods.length; j++) {Method method = methods[j];//大多數(shù)的Class操作的,基本都可以操作isAnnotationPresent以及getAnnotationif (method.isAnnotationPresent(RequestMapping.class)) {RequestMapping annotation = method.getAnnotation(RequestMapping.class);String value = annotation.value(); //按照之前寫好的,這里相當于拿到了"/query"//定義臨時url,因為前面的baseUrl可能存在值,而他需要為所有的方法進行拼接的,所以不能給他進行賦值String url = baseUrl;url += value;//操作url和method的映射關(guān)系,關(guān)系使用map保存起來handlerMapping.put(url, method);//你會發(fā)現(xiàn)并能沒有難度,難在哪里,實際上難在后面最終到的doPost請求方法的處理//我們到后面去看看吧}}}}}//實現(xiàn)依賴注入private void doAutoWired() {if (map.isEmpty()) { //如果map為空,自然直接的退出return;}// 遍歷map中所有對象,查看對象中的字段,是否有@Autowired注解,如果有需要操作依賴注入關(guān)系for (Map.Entry<String, Object> entry : map.entrySet()) {try {//將對象作為參數(shù)傳遞doObjectDependancy(entry.getValue());} catch (Exception e) {e.printStackTrace();}}}//開始操作依賴注入關(guān)系,傳遞實例對象private static void doObjectDependancy(Object object) {//Field[] getDeclaredFields(),用于獲取此Class對象所表示類中所有成員變量信息Field[] declaredFields = object.getClass().getDeclaredFields();//沒有成員,那么退出if (declaredFields == null || declaredFields.length == 0) {return;}for (int i = 0; i < declaredFields.length; i++) {//拿取第一個成員變量的信息,如成員變量是public int i = 0;,那么得到public int com.she.factory.bb.i//其中值不會操作,在結(jié)構(gòu)中,我們并不能操作其值,最多只能通過這個結(jié)構(gòu)去設置創(chuàng)建的對象的值Field declaredField = declaredFields[i];//判斷是否存在對應的注解,如果不存在,那么結(jié)束當前循環(huán),而不是結(jié)束循環(huán),看看下一個成員變量if (!declaredField.isAnnotationPresent(Autowired.class)) {continue;}//判斷當前字段是否處理過,如果已經(jīng)處理過則continue,避免嵌套處理死循環(huán),這里我們的實現(xiàn)是與spring是不同的//在spring中,我們創(chuàng)建一個類的實例時,會順便在注入時判斷對方是否存在而創(chuàng)建對方的實例(是否可以創(chuàng)建實例,而不是直接創(chuàng)建,我們基本都處理了判斷的(如配置或者注解)),從而考慮循環(huán)依賴//但是這里我們首先統(tǒng)一創(chuàng)建實例了,然后在得到對應的實例后,繼續(xù)看看該實例里面是否也存在對應的注解,以此類推,直到都進行設置//但是如果存在對應的實例中,操作時是自身或者已經(jīng)操作的實例(這個自身代表的是自己,而不是其他類的自己類的類型),那么就會出現(xiàn)死循環(huán)(如果我中有你,你中有我,且你已經(jīng)操作的我不退出,這不是死循環(huán)是什么),所以就會直接的退出(退出當前循環(huán),即continue;)//這里與Spring是極為不同的,Spring是報錯,而這里是不會賦值,即退出,使得不會出現(xiàn)死循環(huán)(雖然我們也可以使得報錯),即也就不會報錯了,所以這里不會出現(xiàn)循環(huán)依賴的問題,因為我們對應的類就已經(jīng)創(chuàng)建好了,就與你的三級緩存一樣,雖然三級緩存是在需要的時候也保存了對應的實例,使得賦值,只是我首先統(tǒng)一操作的,那么我這里的緩存與三級緩存的本質(zhì)還是一樣的,都是保存實例,只是這里是保存id,因為實例都是創(chuàng)建好的,就不需要三級緩存將實例移動了//但是由于我們是統(tǒng)一處理的,而不是與spring一樣,所以在一定程度上需要更多的空間,如果項目非常的大,那么這可能是一個不好的情況,要不然為什么spring是使用時創(chuàng)建呢,而由于這里是測試,所以我們使用這種方式,就不用考慮三級緩存的處理了//boolean contains(Object o),判斷是否包含指定對象//判斷當前類的全限定名加上該變量名稱組成的字符串是否存在//如果存在,說明已經(jīng)注入了if (fieldsAlreayProcessed.contains(object.getClass().getName() + "." + declaredField.getName())) {continue;}//這里也會操作技巧,一般情況下,我們都會判斷是否可行才會操作真正的代碼,而不是先操作真正的代碼然后處理是否可行(所以上面的結(jié)束循環(huán)先處理)//當然我們基本都會意識到這樣的問題的,這里只是提醒一下Object dependObject = null;Autowired annotation = declaredField.getAnnotation(Autowired.class);String value = annotation.value();if ("".equals(value.trim())) { //清空兩邊空格,防止你加上空格來混淆//先按照聲明的是接口去獲取,如果獲取不到再按照首字母小寫//拿取對應變量類型的全限定名,若是基本類型,那么就是本身,如int就是int//然而int基本操作的全限定名中,不可能作為類使用,所以在int中處理該注解是沒有意義的,在spring中基本也是如此,除非是以后的版本可能會有dependObject = map.get(declaredField.getType().getName());//如果沒有獲取,那么根據(jù)當前變量類型的首字母小寫去獲取(上面是默認處理接口的)//然而,大多數(shù)按照規(guī)范的話,基本都有接口,但是是按照規(guī)范,防止沒有規(guī)范的,所以使用下面的處理if (dependObject == null) {//getType是得到了Field的ClassdependObject = map.get(lowerFirst(declaredField.getType().getSimpleName()));}} else {//如果是指定了名稱,那么我們選擇操作拼接全限定名來處理dependObject = map.get(value+declaredField.getType().getName());}//正好,如果不匹配的話,為null,那么防止遞歸出現(xiàn)問題,所以操作跳過//在spring中是操作報錯的,而導致不能運行程序,這里我們跳過,順便設置null吧(雖然在成員變量中,默認的對象基本都是null)//上面正好對應之前操作的接口和類的操作,當然,在Spring中,是查詢?nèi)?/span>//來找到對應類型的實例(getBean的),這里我們是自定義的,自然不會相同//一般來說Class的getName是全限定名,而其他的就是對應的類名稱//而Class的getSimpleName則是類名稱,其他的并沒有g(shù)etSimpleName方法// 記錄下給哪個對象的哪個屬性設置過,避免死循環(huán)(遞歸的死循環(huán))fieldsAlreayProcessed.add(object.getClass().getName() + "." + declaredField.getName());//遞歸if (dependObject != null) {doObjectDependancy(dependObject);}//全部設置好后,我們進行設置//設置可以訪問private變量的變量值,在jdk8之前可能不用設置//但是之后(包括jdk8)不能直接的訪問私有屬性了(可能隨著時間的推移,也會改變),因為需要進行設置這個,所以不能直接訪問私有屬性了declaredField.setAccessible(true);//給對應的對象的該成員變量設置這個值try {declaredField.set(object, dependObject);} catch (Exception e) {e.printStackTrace();}}}//初始化類private void doInstance() {//如果,沒有全限定名,說明對應的指定掃描的地方,不存在任何的文件處理if (classNames.size() == 0) return;if (classNames.size() <= 0) return; //其實,就算一個程序中,其可能不存在負數(shù),但是加個意外的條件還是比較好的,所以上面的代碼可以注釋掉,雖然他也沒有錯(因為也基本不會出現(xiàn)負數(shù))try {for (int i = 0; i < classNames.size(); i++) {//拿取對應的全限定名(稱)String className = classNames.get(i);// 通過對應的全限定名稱,拿取其Class對象Class<?> aClass = Class.forName(className);//接下來判斷Controller和Service的注解的區(qū)別,一般情況下,mvc的這里通常只會判斷Controller以及依賴注入的情況,而不會處理Service//其實這也是順序出現(xiàn)的底層原因,但是由于這里是統(tǒng)一處理的,所以這里的ioc容器,是mvc和spring共有的,這里需要注意一下//判斷對應的類上是否存在對應的注解if (aClass.isAnnotationPresent(Controller.class)) {//既然存在對應的注解,那么進入//Controller一般并不操作value,所以不考慮//獲取類名稱String simpleName = aClass.getSimpleName();String s = lowerFirst(simpleName);//創(chuàng)建實例,實際上這個實例由于是根據(jù)對應的Class(他也有具體的類來得到或者全限定名,所以創(chuàng)建的實例相當于我們在main中操作創(chuàng)建(實際上在main創(chuàng)建也是需要導入或者當前目錄下完成))//所以就是合理的Object o = aClass.newInstance();map.put(s, o);}//這里就有一個想法,好像使用if-else也是可行的,的確,他就是可行的,但是這樣的好處是,在中間可以繼續(xù)處理,并且也可以自定義結(jié)束方案//而不是利用if-else中最后的處理才進行,如果比靈活性,那么多個if就是好的,如果比穩(wěn)定,那么if-else是好的,但是在代碼邏輯非常正確的情況下,那么多個if就是好的//也就是多個if上限高,下限低,當然,多個if由于下限低,所以在某些情況下,可能并不好處理,比如存在兩個判斷,但是后面都需要他們的數(shù)據(jù),而兩個if一般基本只能重復了(比如這里的依賴注入的方法(即指定名稱并不友好這里))if (aClass.isAnnotationPresent(Service.class)) {String beanName = aClass.getAnnotation(Service.class).value();//創(chuàng)建實例Object o = aClass.newInstance();int ju = 0;if ("".equals(beanName.trim())) {//如進入這里,那么說明對應注解我們沒有進行設置value,那么默認是""(我們注解設置的默認的),操作首字母小寫//Class的getSimpleName方法是獲取類名稱beanName = lowerFirst(aClass.getSimpleName());}else {ju=1;}//放入map,因為id(beanName)有了,對應的實例也有了,自然放入//id作為類名稱首字母小寫或者指定的類名稱id//然而指定名稱并不友好,因為可能存在多個不同的類是相同的名稱,所以這個名稱需要與全限定名進行拼接(也就是上面的beanName += aClass.getName();,這里操作兩個if并不友好,因為需要前面的數(shù)據(jù)),但是這個全限定名可能也是對應的接口而不是類(到這里你是否理解了,為什么spring在如果容器中存在多個相同的實例時,會報錯了吧,因為判斷這個的話,非常麻煩,所以spring只能存在一個實例(當然,也可以存在多個,只是他需要對應一些信息,比如也操作這樣的名稱,或者變量名稱等等),且他是利用遍歷來賦值的,這樣就非常簡單了,當然,如果你考慮了所有情況,那么在獲取時,自然比spring快,只是存在沒有必要的空間,所以互有好處,spring,節(jié)省空間,但是獲取時需要性能,而這里需要空間,但是獲取時性能更快)if(ju==1){UtilGetClassInterfaces.getkeyClass(beanName,aClass,map,o);}else{map.put(beanName, o);}//當然,你也可以這樣,由于我們的類通常有接口(在controller中通常沒有接口,所以不考慮他),所以在一定程度上是可以給接口id的,雖然spring并沒有這樣的操作,但并不意味著我們不能//操作如下://Class<?>[] getInterfaces(),獲取實現(xiàn)的所有接口Class<?>[] interfaces = aClass.getInterfaces();if (interfaces != null && interfaces.length > 0) {for (int j = 0; j < interfaces.length; j++) {//如果你實現(xiàn)的是java.io.Serializable接口,那么打印這個anInterface時,結(jié)果是:interface java.io.Serializable//如果接口是java.io.Serializable接口,那么對應的getName就是java.io.SerializableClass<?> anInterface = interfaces[j];// 以接口的全限定類名作為id放入(如果想要名稱,那么可以通過其他api,具體可以百度,一般getSimpleName好像可以),這里我們繼續(xù)創(chuàng)建一個吧,在map中我們通常是不會指向同一個的map.put(anInterface.getName(), aClass.newInstance());//為什么全限定名是對應的整個路徑呢,解釋如下://"全限定名"這個術(shù)語的名稱來自于它的作用:它提供了完整的、唯一的類名標識,以避免命名沖突,"全"表示完整性,"限定"表示唯一性,因此全限定名是一個完整且唯一的標識//也的確,對應的全限定名,比如com.mvc.framework.servlet.DispatcherServlet,在項目中也的確是完整且唯一的,要不然也不會作為生成Class對象的參數(shù)}}}}} catch (Exception e) {e.printStackTrace();}}//將str首字母進行小寫private static String lowerFirst(String str) {char[] chars = str.toCharArray();if ('A' <= chars[0] && chars[0] <= 'Z') {chars[0] += 32; //在ASCII中a是97,A是65,相差32}return String.valueOf(chars); //將字符變成String}//掃描類,或者掃描到注解,參數(shù)是在那里掃描private void doScan(String scanPackage) {try {//獲取對應包所在的絕對路徑String scanPackagePath = Thread.currentThread().getContextClassLoader().getResource("").getPath() + scanPackage.replaceAll("\\.", "/");scanPackagePath = URLDecoder.decode(scanPackagePath, StandardCharsets.UTF_8.toString());File pack = new File(scanPackagePath); //參數(shù)可以是以"/"開頭的File[] files = pack.listFiles();for (File file : files) {//如果是一個目錄,那么為了保證是下一個目錄,所以我們以當前路徑加上這個目錄名稱if (file.isDirectory()) {//這樣可以使得繼續(xù)處理,知道找到里面的文件doScan(scanPackage + "." + file.getName());//對應的目錄處理完了,那么應該要下一個文件或者目錄了continue;}//找到是一個文件,且是class,那么進行處理if (file.getName().endsWith(".class")) {//當前的包名,加上文件名,就是全限定名String className = scanPackage + "." + file.getName().replaceAll(".class", "");classNames.add(className); //保存好}}} catch (Exception e) {e.printStackTrace();}}//加載配置文件,得到信息,這里比較簡單,只需要得到地址即可private String doLoadconfig(String contextConfigLocation) {//加載xmlInputStream resourceAsStream = DispatcherServlet.class.getClassLoader().getResourceAsStream(contextConfigLocation);//使用讀取xml的依賴來進行處理//獲取XML解析對象SAXReader saxReader = new SAXReader();try {//解析XML,獲取文檔對象documentDocument document = saxReader.read(resourceAsStream);//getRootElement():獲得根元素Element rootElement = document.getRootElement();//直接找到這個標簽,得到他的信息對象,但是其父類只能操作標簽自身信息,所以這里需要強轉(zhuǎn)Element element = (Element) rootElement.selectSingleNode("//component-scan");//獲得對應屬性的值String attribute = element.attributeValue("base-package");return attribute;} catch (Exception e) {e.printStackTrace();}return "";}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//我們幾乎保存了method和url的映射,所以開始進行處理//獲取url//String getRequestURI(),返回此請求的資源路徑信息//如果請求是:http://localhost:8080/springmvc_xie/demo/querySystem.out.println("請求資源的路徑為:" + req.getRequestURI()); //請求資源的路徑為:/springmvc_xie/demo/query//StringBuffer getRequestURL(),返回此請求的完整路徑信息System.out.println("請求資源的完整路徑為:" + req.getRequestURL()); //請求資源的完整路徑為:http://localhost:8080/springmvc_xie/demo/query//一般來說,完整的路徑的前面的信息對訪問資源來說,通常并不需要,所以我們獲取這個路徑即可String requestURI = req.getRequestURI();//獲取一個反射的方法Method method = handlerMapping.get(requestURI);//不對勁,我們發(fā)現(xiàn),他存在項目名稱,因為總路徑是/springmvc_xie/demo/query//這個在發(fā)版到服務器時,基本也是如此,雖然在本地我們也可以去掉,但是這是名稱是可能或者發(fā)版時必然存在的//這個我們就需要進行解決}
}
上面的代碼在保存實例時,保存了與spring完全不一樣的模式,是保存相關(guān)的任何處理:
在給出之前,先看看如下:
package com.mvc.framework.servlet;public class a extends b implements f {public static void main(String[] args) {getkeyClass(a.class);}private static void getkeyClass(Class a) {Class superclass = a.getSuperclass();if ("java.lang.Object".equals(superclass.getName())) {System.out.println(a.getName());} else {System.out.println(a.getName());getkeyClass(superclass);}Class[] interfaces = a.getInterfaces();for (Class anInterface : interfaces) {getkeyInterface(anInterface);}}private static void getkeyInterface(Class a) {Class[] interfaces1 = a.getInterfaces();if (interfaces1.length <= 0) {System.out.println(a.getName());return;}System.out.println(a.getName());Class aClass = interfaces1[0];getkeyInterface(aClass);}
}
package com.mvc.framework.servlet;public class b implements c{
}
package com.mvc.framework.servlet;public interface c extends d{
}
package com.mvc.framework.servlet;public interface d {
}
package com.mvc.framework.servlet;public interface f extends g{
}
package com.mvc.framework.servlet;public interface g {
}
對應執(zhí)行的結(jié)果是拿取其所有父類情況的全限定名,這個時候我們看這里:
//如果是指定了名稱,那么我們選擇操作拼接全限定名來處理                
dependObject = map.get(value+declaredField.getType().getName());
他是根據(jù)對應的類型來的,而由于多態(tài),那么可能存在非常多的類型,這也是我們這里的處理方式,而不是spring的遍歷,或者指定名稱(注意:在spring中,指定多個相同實例名稱時也會報錯,在id哪里,你也會或多或少的知道什么)
那么在保存時應該是在這里:
在這之前,我們首先在framework包下,創(chuàng)建util包,然后在里面創(chuàng)UtilGetClassInterfaces類加上如下:
package com.mvc.framework.util;import java.util.Map;public class UtilGetClassInterfaces {public static void getkeyClass(String beanName, Class a, Map map, Object o) {Class superclass = a.getSuperclass();if ("java.lang.Object".equals(superclass.getName())) {map.put(beanName + a.getName(), o);} else {map.put(beanName + a.getName(), o);getkeyClass(beanName, superclass, map, o);}Class[] interfaces = a.getInterfaces();for (Class anInterface : interfaces) {getkeyInterface(beanName, anInterface, map, o);}}private static void getkeyInterface(String beanName, Class a, Map map, Object o) {Class[] interfaces1 = a.getInterfaces();if (interfaces1.length <= 0) {map.put(beanName + a.getName(), o);return;}map.put(beanName + a.getName(), o);Class aClass = interfaces1[0];getkeyInterface(beanName, aClass, map, o);}}
在對應的代碼中可以看到:
 if(ju==1){UtilGetClassInterfaces.getkeyClass(beanName,aClass,map,o);}else{map.put(beanName, o);}
這里就是解決的辦法,但是如果這種辦法是好的,為什么spring不使用呢,其實我們也可以看到,他們是各有利弊的,但是spring并沒有隱患,或者隱患很小,而這種有隱患,取決于對應保存的相同的對象非常多,那么出現(xiàn)操作相同對象時也會出現(xiàn)共享的問題,當然,這些問題并不大,因為實例通常只是提供方法操作而已,所以這也算是解決的辦法
上面的代碼,需要仔細看看,當然,如果出現(xiàn)問題,后面也會給出的,通過上面的說明,可以發(fā)現(xiàn),在doPost方法中,出現(xiàn)問題了,其中就是路徑的問題,一般我們可以這樣的解決:
//一般來說,完整的路徑的前面的信息對訪問資源來說,通常并不需要,所以我們獲取這個路徑即可String requestURI = req.getRequestURI();String contextPath = req.getContextPath();/*/springmvc_xie*/System.out.println("項目名稱:" + contextPath); // /springmvc_xieString substring = requestURI.substring(contextPath.length(), requestURI.length());System.out.println("拿取的路徑:" + substring); // /demo/query//獲取一個反射的方法Method method = handlerMapping.get(substring);
當然,一般來說,對應的/demo/query中,可能存在就算你不寫/demo,只是寫demo的情況(只需要判斷開頭的情況,因為其他情況會默認看成路徑的),而在mvc中,通常是默認加上的,所以我們可以修改initHandlerMapping方法:
在這之前,我們需要考慮一件事,你可能也從來沒有考慮過,也就是說,不在方法上加上RequestMapping注解,只在類上加上,并且單純的訪問類上的路徑,那么他會訪問到這個方法嗎,答:并不會,一般會報錯,在mvc中就是如此,而這里,我們可以看到,如果類上的注解有,那么繼續(xù)往下走,只有存在對應的注解(方法上)的才會進行保存映射,然而這里我們還沒有進行處理是否報錯的問題,因為我們還沒有寫上,這里我們后面考慮,先考慮"/"的情況:
 String value = aClass.getAnnotation(RequestMapping.class).value();//判斷是否存在,不存在加上,否則不做處理if ("/".equals(value.substring(0, 1)) == false) {//默認加上/value = "/" + value;}
  String value = annotation.value(); //按照之前寫好的,這里相當于拿到了"/query"//同樣的也會操作默認加上/if ("/".equals(value.substring(0, 1)) == false) {//默認加上/value = "/" + value;}
這兩個地方寫上,來完成默認加上"/"的處理
針對上面的考慮的一件事的問題,其實最終的得到的值是null(因為只有存在對應的注解(方法上)的才會進行保存映射,而map在沒有對應的key時,返回值的value自然就是null),考慮null是否報錯即可(后面會給出的)
通過上面我們得到了一個Method,我們先看一個案例:
package com.mvc.framework.servlet;import java.lang.reflect.Method;public class a {public void fa(String a, Integer b) {System.out.println(1 + a + b);}public void fb() {System.out.println(1);}public static void main(String[] args) {try {Class<a> aClass = a.class;a a = aClass.newInstance();Method[] methods = aClass.getMethods();for (int i = 0; i < methods.length; i++) {System.out.println(methods[i].getName());//里面有很多方法,遍歷所有,而我們寫的mvc是指定保存的,所以可以不用獲取Method(里面的參數(shù)也是得到的,Method一定對應的,因為是直接獲取,而不是我們選擇的獲取),即直接后面的處理}Method fa = aClass.getMethod("fb");fa.invoke(a);Method fb = aClass.getMethod("fa", String.class, Integer.class);fb.invoke(a, "2", 1);} catch (Exception e) {e.printStackTrace();}}
}//其實到這里,我們可以知道,反射就是可以在運行時,拿取其結(jié)構(gòu)信息的,只要你想,幾乎都可以拿到,并且可以根據(jù)結(jié)構(gòu)信息來實現(xiàn)方法或者賦值,前提是需要指定存在的對象,而不能單純的操作,但是如果我有這些對象,為什么不直接處理方法呢,但是也要明白,如果這些方法沒有呢,并且,如果他是私有的不能直接改變的呢,這個時候,就需要反射了
我們可以發(fā)現(xiàn),要執(zhí)行invoke的處理來調(diào)用類方法,我們需要一個類對象,也就是當前的對應類對象,對應與mvc中的DemoController類對象,并且有時候也需要參數(shù),所以我們需要思考這里,由于map中,幾乎只能保存一個信息,那么這里很明顯,我們需要創(chuàng)建一個(實體)類:
我們在framework包下,創(chuàng)建pojo包,然后創(chuàng)建Handler類:
package com.mvc.framework.pojo;import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;public class Handler {//保存的對應的類對象private Object controller;//保存對應的Method的對象,用來調(diào)用private Method method;//操作正則表達式的,其存在方法Pattern.matches(regex, this);,左邊是正則表達式,右邊this是調(diào)用者,看后面的main即可private Pattern pattern; //這個具體的作用和賦值在后面會知道了private Map<String, Integer> paramIndexMapping; //參數(shù)順序,key是參數(shù)名,value是代表第幾個參數(shù)//這個具體如何使用和操作獲取,看后面的處理//對應于query(HttpServletRequest request, HttpServletResponse response, String name)//那么map中保存的key=name,value=2(這里我們規(guī)定下標從0開始)public Handler(Object controller, Method method, Pattern pattern) {this.controller = controller;this.method = method;this.pattern = pattern;//這個不需要手動的處理,所以參數(shù)三個即可this.paramIndexMapping = new HashMap<>();}public Object getController() {return controller;}public void setController(Object controller) {this.controller = controller;}public Method getMethod() {return method;}public void setMethod(Method method) {this.method = method;}public Pattern getPattern() {return pattern;}public void setPattern(Pattern pattern) {this.pattern = pattern;}public Map<String, Integer> getParamIndexMapping() {return paramIndexMapping;}public void setParamIndexMapping(Map<String, Integer> paramIndexMapping) {this.paramIndexMapping = paramIndexMapping;}
}
現(xiàn)在我們修改initHandlerMapping方法的內(nèi)容(我這里將注釋去掉了,自己對應一下):
前提,我們將之前創(chuàng)建的private static Map<String, Method> handlerMapping = new HashMap<>();注釋掉,因為不使用這個了,而是使用這個:
private List<Handler> handlerMapping = new ArrayList<>();
//很明顯,這里是mvc最開始說明的處理器映射器,而調(diào)用的處理器適配器(則是準備調(diào)用對應方法的操作),我們由于是模擬,所以可處理或者不處理,在后面處理時,我會進行對比前面的流程的
還有一個前提,我們需要知道這個:
package com.mvc.framework.servlet;import java.util.regex.Matcher;
import java.util.regex.Pattern;public class a {public static void main(String[] args) {String a = "[0-9]{3}";String b = "122";/*public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {*/CharSequence aa = b;System.out.println(b.matches(a));/*public boolean matches(String regex) {return Pattern.matches(regex, this);}//return Pattern.matches(regex, this);public static boolean matches(String regex, CharSequence input) {Pattern p = Pattern.compile(regex);Matcher m = p.matcher(input);return m.matches();}//在這個包下:package java.util.regex;public final class Patternimplements java.io.Serializable
{
}*///通過上面的處理,我們可以分開成這樣Pattern pattern = Pattern.compile("[0-9]{3}");//import java.util.regex.Matcher;Matcher m = pattern.matcher("122");boolean matches = m.matches();System.out.println(matches);//通過源碼說明,Pattern.compile("[0-9]{3}");只是一個賦值作用,并沒有進行識別//而識別的處理在pattern.matcher("122");中,看看該參數(shù)是否符合賦值后的正則表達式System.out.println(pattern); //[0-9]{3}}
}//正則表達式其實本質(zhì)是的對照了,如果其正則獨有的表達并沒有,是一個沒有進行特殊的操作,那么:
/*
Pattern pattern = Pattern.compile("/demo/query");System.out.println(pattern);Matcher m = pattern.matcher("/demo/query");boolean matches = m.matches();System.out.println(matches);返回true,也就是匹配/demo/query,也的確匹配/demo/query也就是說,除了他的規(guī)則處理外,其他的基本就是直接的對比
*/
現(xiàn)在開始修改:
if (method.isAnnotationPresent(RequestMapping.class)) {RequestMapping annotation = method.getAnnotation(RequestMapping.class);String value = annotation.value(); //按照之前寫好的,這里相當于拿到了"/query"//同樣的也會操作默認加上/if ("/".equals(value.substring(0, 1)) == false) {//默認加上/value = "/" + value;}//定義臨時url,因為前面的baseUrl可能存在值,而他需要為所有的方法進行拼接的,所以不能給他進行賦值String url = baseUrl;url += value;//開始封裝路徑和方法信息,參數(shù)Pattern.compile(url),直接保存對應的正則表達式賦值后的對象Handler handler = new Handler(entry.getValue(),method, Pattern.compile(url));//處理參數(shù)位置信息//比如:query(HttpServletRequest request, HttpServletResponse response, String name)//獲取該方法的參數(shù)列表信息,自然包括名稱或者類型或者位置(從左到右,起始下標為0)Parameter[] parameters = method.getParameters();for (int i = 0; i < parameters.length; i++) {Parameter parameter = parameters[i];//parameter.getType()得到參數(shù)類型if(parameter.getType()==HttpServletRequest.class||parameter.getType()==HttpServletResponse.class){//如果是這兩個,建議名稱就是他們,這樣就能保證賦值是對應的,而不會被其他參數(shù)名稱所影響,具體保存名稱干什么在后面就會知道的handler.getParamIndexMapping().put(parameter.getType().getSimpleName(),i);}else{//其他的類型,保存其名稱handler.getParamIndexMapping().put(parameter.getName(),i);}}//保存映射關(guān)系handlerMapping.add(handler);//你會發(fā)現(xiàn)并能沒有難度,難在哪里,實際上難在后面最終到的doPost請求方法的處理//我們到后面去看看吧}
自己對應一下吧,然后我們修改doPost方法:
到這里我們可以知道框架的作用了,他是定義編寫代碼方式以及方便代碼編寫,而中間件是在編寫好的代碼基礎上的一個補充或者增強
由于單純的代碼都寫在doPost方法里面比較麻煩,所以我們定義一個方法(然而我也只是定義了一個小的方法),看如下就知道了:
在之前,我們需要知道這個:
package com.mvc.framework.servlet;import java.lang.reflect.Method;public class a {public void fa(String a, Integer b, int j) {}public static void main(String[] args) throws Exception {Class<a> aClass = a.class;Method fa = aClass.getMethod("fa", String.class, Integer.class, int.class);Class<?>[] parameterTypes = fa.getParameterTypes();for (int i = 0; i < parameterTypes.length; i++) {Class<?> parameterType = parameterTypes[i];System.out.println(parameterType.getSimpleName());}/*StringIntegerint*/}
}
@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Handler handler = getHandler(req);if (handler == null) {//如果沒有匹配上,考慮返回404resp.getWriter().write("404 not found");return;}//參數(shù)綁定//先獲取所有的參數(shù)類型,因為對應的map只能保存其固定的兩個值//如名稱,和位置,所以我們這里給出類型吧,就不額外創(chuàng)建類了Class<?>[] parameterTypes = handler.getMethod().getParameterTypes(); //之前操作名稱的下標就是為了這里,也順便為了map本身,所以大多數(shù)情況下,如果需要保存位置,建議從0開始//因為大多數(shù)的集合或者數(shù)組操作都是從0開始的(除非是手寫的,當然系統(tǒng)提供的基本都是從0開始,以后不確定),從而進行對應//創(chuàng)建一個數(shù)組,用來保存參數(shù)Object[] objects = new Object[parameterTypes.length];//保存已經(jīng)操作的位置int[] ii = new int[parameterTypes.length];//給數(shù)組參數(shù)值,并操作順序,先拿取參數(shù)集合(這里我們只是模擬,就不考慮文件的處理了,所以這里一般我們認為考慮鍵值對的參數(shù)傳遞)Map<String, String[]> parameterMap = req.getParameterMap(); //一個鍵是可以對應多個值的Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();//遍歷所以從請求中拿取的參數(shù)for (Map.Entry<String, String[]> param : entries) {String value = "";//如果是多個的話,那么合并吧,比如name=1&name=2,那么這里的結(jié)果就是1,2(這里考慮在mvc中如果存在多個相同的,那么會處理合并)for (int i = 0; i < param.getValue().length; i++) {if (i >= param.getValue().length - 1) {value = param.getValue()[i];continue;}value = param.getValue()[i] + ",";}//上面的就考慮了value處理合并,當然,如果沒有,自然不會處理,因為value = param.getValue()[i];//現(xiàn)在我們拿取了傳遞的參數(shù),我們考慮將參數(shù)進行存放//但是考慮到類型是否正確,以及名稱是否對應,所以需要判斷存放在objects中//這兩個考慮我們都有存放信息(也就是位置,名稱,類型)//這里就要利用到我們之前保存的handler.getParamIndexMapping()了//getKey是鍵值對的鍵,自然對應與參數(shù)列表中的名稱,所以這里的判斷即可//當然,有些是不看名稱的,但是我們只用名稱定義了位置,所以每次設置完值,建議用數(shù)組保存對應的已經(jīng)操作的位置,從而來判斷其他位置(因為類型在外面定義)if (!handler.getParamIndexMapping().containsKey(param.getKey())) {//如果不包含,那么結(jié)束當前循環(huán),那么考慮其他的參數(shù)了,那么對應的數(shù)組中的值自然就是null了//在67章博客中,我們說明了類型的變化,其實就在這里,可以思考一下就行continue;}//拿取位置,前提是對應的名稱對應才能拿取Integer integer = handler.getParamIndexMapping().get(param.getKey());//如果對應了,那么看看對應的類型是否可行,并且由于我們保存了位置(且大多數(shù)獲取列表的操作都是從0開始),且下標為0,那么正好對應與類型//進行賦值,在賦值之前需要考慮一個問題,我們需要看看他的類型是什么,來進行其他的處理if ("String".equals(parameterTypes[integer].getSimpleName())) {objects[integer] = value;}//默認情況下,前端傳給后端的鍵值對基本都是字符串if ("Integer".equals(parameterTypes[integer].getSimpleName()) || "int".equals(parameterTypes[integer].getSimpleName())) {//在mvc中Integer在考慮多個時,只會拿取第一個,而int與Integer可以互相轉(zhuǎn)換,所以也可以是同一個代碼value = value.split(",")[0];Integer i = null;try {i = Integer.parseInt(value);} catch (Exception e) {e.printStackTrace();throw new RuntimeException("String轉(zhuǎn)換Integet報錯,參數(shù)名稱是:" + param.getKey());}objects[integer] = i;}//其他的類型我們就不判斷了,因為只是模擬而已//保存位置ii[integer] = 1;}//當然,有些是不看名稱的,但是我們只用名稱定義了位置,所以我們需要所以我們需要考慮如下://用來先考慮默認賦值的,而不是先處理請求參數(shù),默認賦值的,一般不會考慮名稱是否一致(看前面的規(guī)定的名稱就知道了,他一般只是保證不會影響其他名稱,自然的,我們也不可能直接寫出,我們只需要判斷類型即可)//這里我們就需要前面的保存的位置來排除了//當然,由于有些名稱是固定的,所以并不需要對應的數(shù)組,直接給出得到位置即可,否則的話,需要循環(huán)找位置Integer integer = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName());objects[integer] = req;ii[integer] = 1;integer = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName());objects[integer] = resp;ii[integer] = 1;//解決其他的默認賦值的,并且沒有保存名稱的(也就是沒有保存對應的位置的)//當然,也可以不這樣做,只是有些默認的賦值null會報錯,這也是mvc的處理方式,那么刪除這個for循環(huán)和不刪除就算處理與不處理的區(qū)別了,后面可以選擇測試一下for (int i = 0; i < ii.length; i++) {if (ii[i] == 0) {if ("int".equals(parameterTypes[i].getSimpleName())) {//用來解決int在mvc中會報錯的情況objects[i] = 0;}//后面還可以定義double等等其他的默認值,反正可以在這里處理默認值(賦值)為null會報錯的情況}}//調(diào)用對應的方法,invoke后面的參數(shù)是可變長參數(shù),所以可以操作數(shù)組//如果沒有賦值,那么對應的下標的值,自然是null,自然也就操作了默認的賦值,但是如果對應的是int,那么自然會報錯,這在mvc中是如此//但是這里我們處理了,即:/*if("int".equals(parameterTypes[i].getSimpleName())){//用來解決int在mvc中會報錯的情況objects[i] = 0;}*/try {handler.getMethod().invoke(handler.getController(), objects); //一般這里由處理器適配器來完成的,得到的結(jié)果通常用于視圖解析器,這里我們沒有寫這個,所以忽略了,這里只是調(diào)用一下,所以稱為處理器適配器也行吧} catch (Exception e) {e.printStackTrace();}//至此,我們總算執(zhí)行了對應的方法了,當然,在mvc中,是存在非常多的自帶的組件,這里我們只是模擬一部分而已//當然,這里的兩個請求HttpServletRequest req, HttpServletResponse resp是在這里得到的,所以對應的mvc得到的也是這個//我們也在前面說明了這個得到了,如果沒有看這里即可}private Handler getHandler(HttpServletRequest req) {if (handlerMapping.isEmpty()) {return null;}String requestURI = req.getRequestURI();String contextPath = req.getContextPath();String substring = requestURI.substring(contextPath.length(), requestURI.length());for (Handler handler : handlerMapping) {//在這里你可能會有以為,好像我們保存url也行,為什么非要操作Pattern呢//這是因為正則存在更多的操作,而不只是路徑,所以加上可以進行更好的擴展//具體擴展可以考慮到在RequestMapping注解中加上正則表達式//從而匹配請求路徑Matcher matcher = handler.getPattern().matcher(substring);//如果不匹配,那么找下一個是否匹配,否則直接返回匹配的信息if (!matcher.matches()) {continue;}return handler;}//如果沒有,自然返回nullreturn null;}
上面解決了mvc中int的默認賦值null的錯誤,但是我們也可以發(fā)現(xiàn),他需要循環(huán),mvc使用報錯來解決也并不是不行,節(jié)省一點性能,當然,這點性能也并不大,所以可以認為是mvc的一個疏忽,然而這些疏忽并不是很影響具體開發(fā),所以mvc就不操作改變了
至此,我們可以選擇來測試一下:
我們回到這里:
package com.mvc.framework.controller;import com.mvc.framework.annotations.Autowired;
import com.mvc.framework.annotations.Controller;
import com.mvc.framework.annotations.RequestMapping;
import com.mvc.framework.service.DemoService;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Controller
@RequestMapping("/demo")
public class DemoController {@Autowiredprivate DemoService demoService;@RequestMapping("/query")public String query(HttpServletRequest request, HttpServletResponse response, String name) {String s = demoService.get(name);return s;}}
修改一下:
@RequestMapping("/query")public String query(HttpServletRequest request, HttpServletResponse response, String name) {System.out.println(request);System.out.println(response); //就算是反射過來的,參數(shù)的指向地址還是與尋常的參數(shù)傳遞是一樣的,所以是共享的String s = demoService.get(name);return s;}
然后修改這個:
package com.mvc.framework.service.impl;import com.mvc.framework.annotations.Service;
import com.mvc.framework.service.DemoService;@Service //這里加上
public class DemoServiceImpl implements DemoService {@Overridepublic String get(String name) {System.out.println("打印:"+name);return name;}
}
然后啟動服務器,訪問這個:http://localhost:8080/springmvc_xie/demo/query?name=1234,其中springmvc_xie是項目名稱
訪問后你會發(fā)現(xiàn),打印的值是null,為什么,通過我的調(diào)試,對應的這個地方得到的名稱不是參數(shù)名稱:
    if (parameter.getType() == HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) {//如果是這兩個,建議名稱就是他們,這樣就能保證賦值是對應的,而不會被其他參數(shù)名稱所影響,具體保存名稱干什么在后面就會知道的handler.getParamIndexMapping().put(parameter.getType().getSimpleName(), i);} else{//其他的類型,保存其名稱handler.getParamIndexMapping().put(parameter.getName(), i);//這里得到的不是name,而是arg2,我們看如下:}
經(jīng)過上面的問題,我們來創(chuàng)建一個類:
package com.mvc.framework.servlet;import java.lang.reflect.Method;
import java.lang.reflect.Parameter;public class a {public void fa(String nam2, String name1) {}public void fb(String nam2, String name1) {}public static void main(String[] args) {Class<a> aClass = a.class;Method[] fa = aClass.getMethods();for (int i = 0; i < fa.length; i++) {Method method = fa[i];System.out.println(method.getName());Parameter[] parameters = method.getParameters();for (int ii = 0; ii < parameters.length; ii++) {Parameter parameter = parameters[ii];System.out.println(parameter.getName());}}//打印的結(jié)果給出部分:/*main:arg0fb:arg0.arg1fa:arg0.arg1獲取的方法是反過來獲取的,并且參數(shù)也是固定的模式當然,參數(shù)列表是從左到右的*/}
}
為什么會這樣,這一般需要看版本,一般在jdk8開始就這樣處理了,當然,這樣的還有私有的,也就是設置可以訪問private變量的變量值,在jdk8之前可能不用設置,但是之后(包括jdk8)不能直接的訪問私有屬性了(可能隨著時間的推移,也會改變),因為需要進行設置這個,所以不能直接訪問私有屬性了
為什么要這樣處理:這是為了提高字節(jié)碼的緊湊性和安全性,參數(shù)名稱在編譯后被抹去,主要有以下原因:
隱私和安全性:在字節(jié)碼中包含參數(shù)名稱可能泄漏程序的細節(jié)信息,可能會被濫用,這對于某些應用來說是不可接受的,因為它可以暴露敏感信息
字節(jié)碼緊湊性:字節(jié)碼文件通常被用于分發(fā)和執(zhí)行,如果包含了大量參數(shù)名稱,將會增加文件大小,在資源受限的環(huán)境中,較小的字節(jié)碼文件可以提高性能和減小存儲需求
字節(jié)碼的緊湊型還比較合理,但是為什么字節(jié)碼會影響安全,舉例:
反編譯和逆向工程:如果參數(shù)名稱包含在字節(jié)碼中,惡意用戶或攻擊者可以更容易地反編譯和逆向工程你的應用程序,以獲取關(guān)于應用程序的內(nèi)部結(jié)構(gòu)和邏輯的信息,這可以幫助他們發(fā)現(xiàn)潛在的漏洞或弱點,甚至濫用應用程序
敏感信息泄露:在某些情況下,方法的參數(shù)名稱可能包含敏感信息,如果這些名稱泄露到字節(jié)碼中,可能會暴露敏感數(shù)據(jù)、業(yè)務邏輯或應用程序內(nèi)部細節(jié),從而引發(fā)安全風險
安全性問題披露:如果方法參數(shù)名稱包含有關(guān)應用程序內(nèi)部的信息,攻擊者可能會更容易發(fā)現(xiàn)應用程序的潛在漏洞,從而加大應用程序受攻擊的風險
當然,spring是解決的或者說mvc是解決的(這里我們稱為spring,因為最終是使用spring的主要內(nèi)容的),但是他是通過其他方式,所以并不會暴露,然而,對方也可以通過spring來獲取該參數(shù),所以也只是避免了方便的獲取,那么在不使用spring時,他既然會出現(xiàn)arg0這樣的名稱,那么有沒有操作讓他不這樣做,其實是有的,只需要加上這個操作:
-parameters,編譯選項
一般情況他是操作編譯的,那么應該是如此的:javac -parameters YourClass.java,然而我們是操作項目,不可能這樣處理,應該需要配置來使得項目默認這樣處理,這里我們就以maven為例:
在maven中的pom.xml中操作如下:
<!--與dependencies同級別
-->
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version> <!-- 版本號根據(jù)實際情況調(diào)整 --><configuration><source>11</source> <!-- Java 版本,根據(jù)你的項目調(diào)整 --><target>11</target> <!-- Java 版本,根據(jù)你的項目調(diào)整 --><compilerArguments><parameters /> <!-- 這里是主要的操作 --></compilerArguments></configuration></plugin></plugins></build>
執(zhí)行后,可以發(fā)現(xiàn),他獲得了對應的參數(shù)名稱,我們試一下修改版本,將上面的11修改成1.8,執(zhí)行后,可以發(fā)現(xiàn),也獲得了參數(shù)名稱了,當然,也并不是說任何版本都可以這樣,可能有些版本這樣的操作是不行的(現(xiàn)在還不確定,看以后,并且隨著jdk的變化(并不是說對方不會對1.8或者11繼續(xù)修改),可能1.8或者11也會不行),但是我們也可以發(fā)現(xiàn),在之前操作mvc中,我們并沒有處理這個,也就是說明了spring并不是使用這個方式,那么他是使用什么方式呢:
spring是使用字節(jié)碼分析庫(如 ASM)來動態(tài)分析類文件,而不依賴于編譯器生成的字節(jié)碼,這種技術(shù)使 Spring 能夠獲取參數(shù)名稱,因為他是直接分析文件的,也就可以說,他內(nèi)部的代碼是相當于跳過了操作了-parameters選項,而訪問并處理字節(jié)碼文件(實際上再怎么處理字節(jié)碼文件中都會保留原始參數(shù)名稱,否則的話,也不可能使用該名稱的變量了,他只是不讓你直接獲取即可,那么如果說選項中他-parameters是解決某一個開關(guān)導致的不讓獲取,那么spring是直接從文件找,也自然就跳過了這個開關(guān))
那么就明白了,也就是直接的分析字節(jié)碼文件,好吧,這對現(xiàn)在的我們來說幾乎不可能手動寫好,因為需要學習太多知識,并且細節(jié)非常多,基本不是我們個人能夠完成的,所以我們拿取spring提供給我們的這個操作代碼,但是引入spring一般我們并不能找到對應的類,所以我們選擇使用第三方的代碼,思考:為什么main的參數(shù)需要是對應的args數(shù)組:一般他代表命令行參數(shù),這取決于對應的main是程序的入口,一般我們可以在命令行中執(zhí)行java時傳遞,比如我們有一個java文件:
public class ceshi {public static void main(String[] args){for (String arg : args) {System.out.println(arg);}}
}
在命令行中(在目錄下),執(zhí)行javac ceshi.java,然后執(zhí)行java ceshi,發(fā)現(xiàn)什么都沒有,但是如果是這樣的執(zhí)行:java ceshi 1 2 3,那么會打印1和2和3,具體自行測試,也就是說他存在這樣的作用:
命令行參數(shù)的傳遞:命令行參數(shù)是用戶在終端或命令提示符中輸入的,它們以字符串的形式傳遞給程序,因此,main函數(shù)需要一個字符串數(shù)組來接收這些參數(shù),以便程序能夠解析和使用它們
靈活性:通過使用命令行參數(shù),程序可以根據(jù)用戶提供的輸入進行不同的操作,參數(shù)的個數(shù)和內(nèi)容可以根據(jù)需要進行調(diào)整,從而增加程序的靈活性
標準化:采用參數(shù)數(shù)組的方式,使得命令行參數(shù)的處理在不同的編程語言和操作系統(tǒng)中更加一致和標準化,這有助于開發(fā)人員編寫跨平臺的代碼
現(xiàn)在我們修改1.8回到11(看你自己,可以選擇修改),開始使用spring的方式,這里我們選擇拿取第三方的代碼(這里我們使用ASM,spring也是使用類似的或者相同的),先在framework包下創(chuàng)建包config,然后在該包下我們創(chuàng)建幾個類(在spring中有類似的這樣的處理,這里我們手動寫一個),這幾個類在后面會給出
在手寫之前,我們使用上面的配置,啟動運行一下,然后操作mvc看看結(jié)果,如果出現(xiàn)了打印的值不是null,而是1234,那么我們可以說初步完成(這個時候,你可以選擇給@Service操作value,然后@Autowired對應(在spring中,我們并沒有這樣的處理,這里我們是這樣處理的),經(jīng)過測試,也打印了1234,也可以測試將String name變成int name,如果打印為0,說明也操作成功(我們的方式,而不是Spring的mvc的報錯),然后我們將路徑中,比如:/demo的開頭的/去掉,訪問,可以發(fā)現(xiàn),也可以,那么說明我們的操作代碼編寫完畢,當然,可能有些地方有問題,但是暫時沒有,且功能都正確,如果出現(xiàn)了,請自行修改一下吧),然后我們使用spring對應的方式來解決:
首先我們需要引入依賴:
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.2</version> <!-- 根據(jù)需要的版本號調(diào)整 --></dependency>
然后去掉這個依賴配置:
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version> <!-- 版本號根據(jù)實際情況調(diào)整 --><configuration><source>1.8</source> <!-- Java 版本,根據(jù)你的項目調(diào)整 --><target>1.8</target> <!-- Java 版本,根據(jù)你的項目調(diào)整 --><compilerArguments><parameters /></compilerArguments></configuration></plugin></plugins></build>
執(zhí)行服務器,先看看是否得到結(jié)果,很明顯,沒有得到,并且加上這個類來測試時,也是沒有得到參數(shù)名稱,這個類是之前的:
package com.mvc.framework.servlet;import java.lang.reflect.Method;
import java.lang.reflect.Parameter;public class a {public void fa(String nam2, String name1) {}public void fb(String nam2, String name1) {}public static void main(String[] args) {Class<a> aClass = a.class;Method[] fa = aClass.getMethods();for (int i = 0; i < fa.length; i++) {Method method = fa[i];System.out.println(method.getName());Parameter[] parameters = method.getParameters();for (int ii = 0; ii < parameters.length; ii++) {Parameter parameter = parameters[ii];System.out.println(parameter.getName());}}}
}
這個時候,我們使用上面的依賴來解決不加這個配置的問題:
首先我們在framework包下,找到前面我們創(chuàng)建的config包(沒有,現(xiàn)在創(chuàng)建也行),然后創(chuàng)建下面兩個(前面的幾個)類:
package com.mvc.framework.config;import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;import java.util.HashMap;
import java.util.Map;public class ParameterNameVisitor extends ClassVisitor {//方法名稱private String methodName;//參數(shù)名集合private Map map;//給出方法名稱public ParameterNameVisitor(String methodName) {super(Opcodes.ASM7);this.methodName = methodName;}//在對應的reader.accept(visitor, 0);中,最終會調(diào)用這個方法//它檢查方法的名稱是否與methodName相匹配,如果匹配,則返回一個新的//有些的參數(shù)://access:1,name:<init>(構(gòu)造)或者fa,descriptor:()V,signature:null,exceptions:null//一般access代表第幾個,從上到下,從1開始,name方法名稱,descriptor參數(shù)列表信息(()V是構(gòu)造)@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {if (name.equals(methodName)) {return new MethodParameterVisitor();}return null;}public Map getParameterNames() {return map;}class MethodParameterVisitor extends MethodVisitor {public MethodParameterVisitor() {super(Opcodes.ASM7);}//上面的visitMethod調(diào)用后,會最終調(diào)用到這里//提取參數(shù)名,如果上面的對應了,那么有多少參數(shù),那么這里執(zhí)行多次,當然,當前的引用也會執(zhí)行一次//有些的參數(shù)://name:this(當前對象引用,不用怕,基本上這是必須需要上面的return new MethodParameterVisitor();執(zhí)行后才可)或者a,descriptor:Lcom/mvc/framework/servlet/a;,signature:null,start:對象,end:對象,index:0//name:方法名稱,descriptor:參數(shù)列表信息(構(gòu)造的話,那么就是類的全限定名),index是位置,從1開始@Overridepublic void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {if (index >= 0) {if (map == null) {map = new HashMap<>();}map.put(index - 1, name); //由于引用是退出的,所以這里的index需要減一,來保證第一個有數(shù)據(jù),而不是null}}}
}
package com.mvc.framework.config;import org.objectweb.asm.*;import java.util.Map;public class ParameterNameExtractor {//獲取指定方法的參數(shù)名地址public static Map getParameterNames(String className, String methodName) throws Exception {//ClassReader對象用于讀取類的字節(jié)碼信息,這里傳遞字節(jié)文件的全限定名ClassReader reader = new ClassReader(className);//ParameterNameVisitor 對象是一個自定義的訪問者類,用于在字節(jié)碼中查找指定方法的參數(shù)名,所以傳遞方法名稱ParameterNameVisitor visitor = new ParameterNameVisitor(methodName);//通過 visitor 訪問指定類的字節(jié)碼,第二個參數(shù) 0 是一個標志//通常用于啟用或禁用不同類型的字節(jié)碼訪問,這里使用0表示默認設置,這個操作一般是根據(jù)對應的類版本來處理的//所以了解即可(也就是訪問字節(jié)碼的方式,一般對應的依賴可能存在多種的,這里使用默認的一種)reader.accept(visitor, 0);//獲取到的方法的參數(shù)名集合return visitor.getParameterNames();}
}
然后我們改造這個a類:
package com.mvc.framework.servlet;import com.mvc.framework.config.ParameterNameExtractor;
import java.util.Map;public class a {String name;public a(String name) {this.name = name;}public a(String name, String j) {this.name = name;}public void fa(String nam2, String name1) {}public void fb(String nam2, String name1) {}public static void main(String[] args) throws Exception {String className = "com.mvc.framework.servlet.a";String methodName = "fa";Map map = ParameterNameExtractor.getParameterNames(className, methodName);for (int i = 0; i < map.size(); i++) {System.out.println("參數(shù)列表的位置:" + i + ",名稱是: " + map.get(i));}methodName = "fb";map = ParameterNameExtractor.getParameterNames(className, methodName);for (int i = 0; i < map.size(); i++) {System.out.println("參數(shù)列表的位置:" + i + ",名稱是: " + map.get(i));}}
}
執(zhí)行后,可以發(fā)現(xiàn),得到了參數(shù)列表,那么我們修改mvc的對應的方法,也就是這里:
 //處理參數(shù)位置信息//比如:query(HttpServletRequest request, HttpServletResponse response, String name)//獲取該方法的參數(shù)列表信息,自然包括名稱或者類型或者位置(從左到右,起始下標為0)Map<String,String> prmap = null;try {//對應得到的基本都是String,所以上面的可以這樣處理(Map<String,String>)prmap = ParameterNameExtractor.getParameterNames(aClass.getName(), method.getName());}catch (Exception e){e.printStackTrace();}Parameter[] parameters = method.getParameters();for (int i = 0; i < parameters.length; i++) {Parameter parameter = parameters[i];//parameter.getType()得到參數(shù)類型if (parameter.getType() == HttpServletRequest.class || parameter.getType() == HttpServletResponse.class) {//如果是這兩個,建議名稱就是他們,這樣就能保證賦值是對應的,而不會被其他參數(shù)名稱所影響,具體保存名稱干什么在后面就會知道的handler.getParamIndexMapping().put(parameter.getType().getSimpleName(), i);} else {//其他的類型,保存其名稱,但是這里是使用prmap,這個循環(huán)是確認位置的以及之所以使用Parameter,也是為了得到一下類型或者其他的,比如parameter.getType().getSimpleName()//他們的參數(shù)都是從0開始,所以可以這樣做,且對應handler.getParamIndexMapping().put(prmap.get(i), i);}}//保存映射關(guān)系handlerMapping.add(handler);
執(zhí)行服務器,訪問后,可以發(fā)現(xiàn),打印值是null,即沒有出來,為什么:通過調(diào)試,我們可以發(fā)現(xiàn),對應的值是完全可以的,但是報錯了,為了找到問題,這里可以給出測試,首先在a類里面操作或者修改如下:
package com.mvc.framework.servlet;import com.mvc.framework.config.ParameterNameExtractor;import java.util.Map;public class a {public static void main(String[] args) throws Exception {String className = "com.mvc.framework.controller.DemoController";String methodName = "query";Map map = ParameterNameExtractor.getParameterNames(className, methodName);for (int i = 0; i < map.size(); i++) {System.out.println("參數(shù)列表的位置:" + i + ",名稱是: " + map.get(i));}}
}
執(zhí)行后,可以得到對應的參數(shù)列表,然后再mvc(我們的模擬)中的init中加上如下(注意:先把他里面的調(diào)用都注釋):
  @Overridepublic void init(ServletConfig config) {String className = "com.mvc.framework.controller.DemoController";String methodName = "query";Map map = ParameterNameExtractor.getParameterNames(className, methodName);for (int i = 0; i < map.size(); i++) {System.out.println("參數(shù)列表的位置:" + i + ",名稱是: " + map.get(i));}}
然后執(zhí)行,發(fā)現(xiàn)就是對應我們啟動服務器的處理,那么肯定的是對應的服務器的關(guān)系,并且一般這個錯誤是Class not found,且錯誤發(fā)生再ClassReader reader = new ClassReader(className);:
我們看看他的源碼:
 public ClassReader(String className) throws IOException {this(readStream(ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true));}private static byte[] readStream(InputStream inputStream, boolean close) throws IOException {if (inputStream == null) {throw new IOException("Class not found");} else {...
可以發(fā)現(xiàn)這樣的:
/*
ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class")
后面的className.replace('.', '/') + ".class"得到的是com/mvc/framework/servlet/a.class(這里替換了.),同理,在服務器中也是如此,那么唯一的區(qū)別就是ClassLoader.getSystemResourceAsStream了,他代表什么:
他代表從系統(tǒng)類路徑找對應的文件,或者說當前項目中找,在前面我們說明了"這里就需要考慮maven的操作與傳統(tǒng)的web的區(qū)別了",對應的區(qū)別是操作web的區(qū)別,那個時候本質(zhì)上只是部署的地方的區(qū)別,雖然我們是不同的構(gòu)建(maven,或者傳統(tǒng)的web,但是對應的最終都是操作服務器來操作對應的路徑說明的,同樣的,前面的這個地方也的確是啟動服務器來得到不同的路徑結(jié)果),但是最終都是操作服務器,而這里我們操作a類時,并不是操作服務器的,所以a類特殊,那么由于他并不是操作服務器,所以他一般情況下,默認從當前的項目或者說當前的工作路徑來找對應的文件,并且對應的class可以認為與java路徑一直,所以com/mvc/framework/servlet/a.class在a類中執(zhí)行時可以找到,因為當前的項目下,也加上這個路徑,正好是對應的文件,但是如果在服務器中處理,那么一般需要從部署層面來進行處理,所以不會默認從當前項目找,一般需要完整的路徑,所以如果這個時候,我們還單純的操作這個路徑,自然是找不到的,那么我們應該這樣的處理,是獲取當前項目部署情況的路徑或者直接給出完整的路徑,一般來說單純的根據(jù)部署情況一般是得不到的,所以我們需要完整的路徑,比如部署方式在前面我們也都知道怎么獲取,也就是:String realPath = servletRequest.getServletContext().getRealPath("/");,但是一般這樣并不能進行操作,因為在web中,對應的ClassLoader.getSystemResourceAsStream只能拿取當前項目的資源文件或者java了,因為class不在當前項目中的(因為部署的原因),那么有沒有其他的方法可以拿取部署的class(或者說io)呢,答:一般沒有
那么我們就需要ClassReader的另外一個構(gòu)造方法了,也就是:
public ClassReader(InputStream inputStream) throws IOException {this(readStream(inputStream, false));}我們可以直接的得到io流賦值給他即可所以我們應該需要這樣的操作來得到io:
即String realPath = servletRequest.getServletContext().getRealPath("/")+"WEB-INF\classes\"+全限定名;(因為替換了"."",所以可以這樣),即通過原始io操作得到了,當然服務器部署時,默認的WEB-INF\classes是固定的,所以寫死,當然,他可能隨著某些配置會改變,所以還是不建議這樣(除非他提供了可以得到這個配置的方法,那么可以處理),雖然前面說,一般沒有方法拿取部署的class,但是是否需要考慮通過其他方法或者是否存在方法直接拿取部署時的class,或者說io呢,而不是通過完整路徑呢:在web中,是可以通過方法得到當前項目對應文件的io流的,然而一般是當前項目直接處理的,所以a類可以,因為他沒有部署路徑,就是當前的項目,而不像web一般只會考慮資源文件,且web中只能通過這樣的io流處理拿取對應的資源文件,也就是說,他的處理其實是在編譯之前處理的,之后的部署的文件還是得不到,甚至是任何相關(guān)操作的流基本只能操作資源文件(java一般也包括),所以就算你提供了完整路徑也是沒有用的,因為他只操作當前項目,也就是說,之前的ClassReader構(gòu)造方法是幾乎不能獲取對應的class文件的,因為對應使用的是ClassLoader.getSystemResourceAsStream,也就是io操作,也就是只能在當前項目處理,其他相關(guān)io操作的幾乎都是當前項目的資源處理,所以考慮通過方法也幾乎不行了,所以我們這里也不能通過自帶的操作的路徑或者對應的方法找當前項目路徑來找編譯后的class文件,當然,他操作java文件一般也可以的,所以我們還是只能操作使用原始io操作String realPath = servletRequest.getServletContext().getRealPath("/")+"WEB-INF\classes\"+全限定名;,那么現(xiàn)在存在獲取WEB-INF\classes方法嗎,答:一般沒有了(當然了,操作mvc必然通常處理服務器,所以spring寫死基本也不會出現(xiàn)問題的,而spring也基本是寫死的),所以我們只能操作如下了:也就是單純的處理io因為沒有方法操作web項目路徑找部署的,就算有,他們也只能操作當前項目處理,那么只能通過原始io處理io流了,當然,現(xiàn)在沒有方法,并不代表以后沒有,所以可以選擇百度看看實際上上面說了這么多其實就總結(jié)如下:方法是得到當前項目的io,那么就存在如下:沒有部署時,默認class和java是同一路徑(因為在當前項目),那么自然可以通過自帶的方法來得到對應的class的io流以及資源文件或者java文件部署時,class不在同一路徑了(不在當前項目了),那么不能通過方法得到對應的class的io流了,只能通過完整路徑了,但是資源文件和java文件還是可以獲得,只是class不能獲得了,所以需要考慮完整路徑了或者存在方法拿取部署io(這個存放方法可以百度,一般沒有)
*/
經(jīng)過上面的說明,我們還需要修改對應的方法,修改如下:

/*
//構(gòu)造一個HandlerMapping處理器映射器,來完成映射關(guān)系initHandlerMapping();找到上面的修改成這個:initHandlerMapping(config); 
*///對應的這里private void initHandlerMapping(ServletConfig config) {//對應的這個地方String ba = aClass.getName().replace('.', '/') + ".class";//對應得到的基本都是String,所以上面的可以這樣處理(Map<String,String>)prmap = ParameterNameExtractor.getParameterNames(config.getServletContext().getRealPath("/")+"WEB-INF\\classes\\"+ba, method.getName());//都是得到getServletContext來處理的,所以config也可以//然后修改ParameterNameExtractor類的getParameterNames方法里面的這個:InputStream inputStream = new FileInputStream(className);//ClassReader對象用于讀取類的字節(jié)碼信息,這里傳遞io流ClassReader reader = new ClassReader(inputStream);
修改后,我們執(zhí)行,訪問一下看看結(jié)果,發(fā)現(xiàn)打印對應的值出來了,說明我們操作成功
至此,我們可以說mvc的模擬框架編寫完成,到這里你會發(fā)現(xiàn),但凡與結(jié)構(gòu)相關(guān)的,基本上反射都可以做到,也就是說注解他是一個結(jié)構(gòu),那么可以絕對的說,注解就是由反射來完成操作關(guān)聯(lián)的(其實被注解操作的方法大多數(shù)只是定義(如參數(shù)列表),具體可能由反射來調(diào)用或者說操作),并且但凡需要與結(jié)構(gòu)信息相關(guān)聯(lián)的,反射也都可以做到,所以以后,如果出現(xiàn)與結(jié)構(gòu)進行關(guān)聯(lián)操作的,那么使用反射吧
到這里,應該很明顯知道,比spring要困難多了吧,spring只需要考慮循環(huán)依賴,而這里除了考慮spring,還需要考慮很多的細節(jié),特別的,是字節(jié)碼文件的解析問題,以及反射的細節(jié)使用,還有保存對應的關(guān)聯(lián)
當然,上面的編寫只是mvc的一部分,特別的,關(guān)于視圖方面的以及攔截方面的(如我們好像并沒有判斷處理/*的操作)我們也沒有給出,所以說才只是一部分
由于博客字數(shù)限制,其他內(nèi)容,請到下一篇博客(112章博客)去看
http://www.risenshineclean.com/news/21658.html

相關(guān)文章:

  • 做網(wǎng)站切圖是什么意思百度關(guān)鍵詞推廣費用
  • 企業(yè)網(wǎng)站建設的一般原則國外網(wǎng)站如何搭建網(wǎng)頁
  • 源代碼建網(wǎng)站百度seo2022新算法更新
  • 泰州網(wǎng)站建設服務熱線國際實時新聞
  • wordpress后臺504seo查詢工具
  • 怎樣優(yōu)化網(wǎng)站渠道推廣有哪些方式
  • 福建省建設執(zhí)業(yè)注冊中心網(wǎng)站沈陽黃頁88企業(yè)名錄
  • 愛愛做網(wǎng)站開源crm系統(tǒng)
  • 網(wǎng)頁首站免費刷贊網(wǎng)站推廣qq免費
  • 網(wǎng)站排名查詢工具有哪些北京推廣優(yōu)化經(jīng)理
  • 華為網(wǎng)站建設官網(wǎng)杭州網(wǎng)站優(yōu)化推薦
  • 網(wǎng)站主頁的要素衡水seo培訓
  • 佛山做網(wǎng)站那家好windows優(yōu)化大師是電腦自帶的嗎
  • 南通做網(wǎng)站企業(yè)qq群排名優(yōu)化軟件
  • 網(wǎng)站怎么做透明導航優(yōu)化大師官網(wǎng)入口
  • 重慶沙坪壩有哪些大學班級優(yōu)化大師是干什么用的
  • 用vs2013做網(wǎng)站案例百度高級搜索入口
  • 淘寶網(wǎng)網(wǎng)站設計分析黃岡seo
  • 彩票網(wǎng)站里的統(tǒng)計怎么做如何在手機上開自己的網(wǎng)站
  • 招商網(wǎng)站建設多少錢合肥seo報價
  • wordpress驗證google站長營銷策劃公司取名大全
  • 鄂爾多斯市東勝區(qū)城市建設局網(wǎng)站網(wǎng)站新域名查詢
  • 畢業(yè)設計指導網(wǎng)站開發(fā)企業(yè)如何做網(wǎng)絡推廣
  • 做動畫相冊在哪個網(wǎng)站好百度游戲中心
  • 怎么做網(wǎng)站用于推廣seo排名優(yōu)化推廣報價
  • 培訓機構(gòu)退費糾紛一般怎么解決關(guān)于進一步優(yōu)化落實疫情防控措施
  • 手工制作大全女生的最愛百度關(guān)鍵詞優(yōu)化服務
  • 唐山如何做百度的網(wǎng)站建設網(wǎng)絡營銷培訓班
  • 動漫制作專業(yè)認知報告廣東seo網(wǎng)站推廣代運營
  • 做簡歷比較好的網(wǎng)站叫什么谷歌優(yōu)化培訓