【H】[框架類] H-Servlet 簡單的Web框架,基於Tomcat伺服器
【H】[框架類] H-Servlet 簡單的Web框架
這是一個系列,【 】裡面是作為代號,[ ]裡面是指這個jar的型別
目錄:
實現原理:
大致就是: 1、使用者傳送的請求,都會通過MainProcessServlet來接受。 2、過濾掉靜態檔案的訪問。 3、請求URL對應去執行方法。 4、將執行結果返回使用者
當然這裡面還是有點複雜的
詳細說明:
一、主處理Servlet
主處理Servlet(MainProcessServelt)繼承HttpServlet,主要用於在web.xml中配置為Servlet,並且所有的請求都將由此類攔截,所以建議web.xml中為以下配置:
< servlet>
<servlet-name>main</servlet-name>
<servlet-class>cn.heshiqian.framework.h.servlet.servlet.MainProcessServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
...
<servlet-mapping>
<servlet-name >main</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
其中:
<load-on-startup>
建議為 1
,這樣可以使框架第一時間載入
<url-pattern>
建議為 /
這樣可以捕獲所有的請求,當然也可以使用其他支援的匹配方式(Tips:換成其他的還沒有測試是否可用)
在此類(父類)的 init()
方法內將進行整個框架的初始化,其中包括讀取包路徑、靜態檔案路徑、基本資料、掃描類、初始化類、初始化請求前處理器等。
其中,若沒有正確的配置類路徑或靜態檔案路徑,將丟擲:NotExistInitParameterException
二、請求前處理器
請求前處理器(ServletReqHandler)將負責處理由MainProcessServlet發來的請求進行解析,其中對於GET和POST請求方式有不同的處理方法。
1、對於GET請求:
首先去除 favicon.ico
的請求,然後對於首頁URL /
單斜槓的請求也去除;
然後解析出GET請求中的Key和Value值;
最後交由中心處理器處理;
實現的部分程式碼:
//這裡開始都是分割請求地址
String serverPort = String.valueOf(request.getServerPort());
String requestURL = request.getRequestURL().toString();
requestURL=requestURL.substring(requestURL.indexOf(serverPort)+serverPort.length(),requestURL.length());
//去除瀏覽器的favicon.ico請求
if(checkIconFile(requestURL)) return;
//單'/'處理首頁
if(requestURL.equals("/")) return;
cfLog.info("path:"+requestURL);
//解析出GET請求的引數key和值
HashMap<String,String> keyMap=new HashMap<>();
Enumeration<String> keys = request.getParameterNames();
while (keys.hasMoreElements()){
String key = keys.nextElement();
keyMap.put(key,request.getParameter(key));
}
//交由中心Handle處理
centerHandle.distributor(RequestMethod.GET,requestURL,request,response,request.getCookies(),keyMap);
2、對於POST請求 不同於GET,POST涉及檔案提交、Form表單、JSON字串等內容體,所以在POST請求上採用和GET請求一樣的方式去獲得表單的Key和Value,但是在檔案和Json上就需要自己去解析了。 實現程式碼:
//這裡區分是檔案還是正常的post提交
boolean multipartContent = ServletFileUpload.isMultipartContent(request);
if(multipartContent){
//按照檔案的方式處理
}else {
//解析出POST請求的引數key和值
HashMap<String,String> keyMap=new HashMap<>();
Enumeration<String> keys = request.getParameterNames();
while (keys.hasMoreElements()){
String key = keys.nextElement();
keyMap.put(key,request.getParameter(key));
}
//表單驗證
if(keyMap.size()==0){
//證明不是表單提交的,應該是json字串,解析JSON字串
try {
String json = Tool.readInputStream(request.getInputStream());
if(json.equals("")){
HttpHelper.sendErr(response,"傳入資料流為空!如果沒有傳入任何'{}'、'[]'的空JSON結構,可能導致框架解析JSON異常!如果需要空執行,請不要將方法使用返回值並且不要帶上@ResponesBody註解!");
return;
}
if(json.indexOf("{")==0&&json.lastIndexOf("}")==json.length()-1){
//證明這個應該是JSONObject物件
HashMap<String, String> map = Tool.jsonToHashMap(json);
keyMap.putAll(map);
keyMap.put(HServlet.SYS_CONSTANT_KEY,json);
centerHandle.distributor(RequestMethod.POST,requestURL,request,response,request.getCookies(),keyMap);
return;
}
if(json.indexOf("[")==0&&json.lastIndexOf("]")==json.length()-1){
//證明這是個陣列JSONArray物件
//todo 不完整,需完善
// HashMap<String, ?> listMap = Tool.jsonToList(json);
// keyMap.putAll(listMap);
// centerHandle.distributor(RequestMethod.POST,requestURL,request,response,request.getCookies(),keyMap);
HttpHelper.sendErr(response,"未開發部分,請等待更新!");
return;
}
throw new JSONException("檢查到JSON開始與結束沒有使用'{','}','[',']'這些括弧包括,請檢查你傳入的JSON串!");
} catch (IOException e) {
HttpHelper.sendErr(response,"讀入資料流時發生錯誤!");
e.printStackTrace();
}
}
}
三、中心處理
(Waring: 需要反射基礎,不會反射的不慌去理解,先去補補知識,博主也是現炒現賣)
中心處理(CenterHandle),主要負責將請求分發給對應的類去處理,當中會根據方法上註解的不同去預處理資料(當然這裡面肯定不是特別完善和足夠智慧,出錯什麼的。。。),並且預留由開發者自己處理原資料的註解,這樣就會在框架無法處理的時候,手動處理部分資料。
處理流程和部分程式碼:
1、由 public void distributor(...)
方法接受引數,包括request和response物件;然後進行URL匹配,獲得URL對應的類,下面程式碼:
Class cclass = ClassManage.checkClassWasInit(url);
if (cclass == null) {
HttpHelper.sendErr(response, "沒有此介面!");
return;
}
在ClassManage中(之後會說到)checkClassWasInit()
方法會在ClassPool中尋找開發者所使用的@RequestUrl註解裡面對應的URL地址,如果不存在這樣的地址就會返回錯誤,沒有此介面;如果有這個介面,此方法內就會繼續判斷這個類是否已被初始化加入到池中,已經加入了就會直接取用,沒有初始化就會載入此類,並新增進類池,同時類回收機制也在執行中。回收機制後續會講。
四、檢視處理
說是檢視處理,其實就是返回資料;當中對執行結果進行了些許簡單判斷(因為invoke返回Object嘛~),如果是String就直接返回字串,如果是框架的VO物件,就會解析VO物件,這個VO物件其實也很簡單,沒有辣麼複雜,就是一個存KV和檔名,然後可以做模板注入的這麼個普通物件,沒有複雜的功能在裡面。 原始碼: 對於所有正常的返回:
if (rs == null) {
HttpHelper.sendErr(response, "警告!方法沒有返回值。此警告不影響流程執行,只是系統返回!<br>如果希望不再出現此提示,請在返回方法上註解@NullReturn");
return;
}
if (isMapToFile) {
if (rs instanceof String) {
//todo 按照一個正常的文字返回
HttpHelper.sendNormal(response, (String) rs);
} else if (rs instanceof VO) {
//todo 按照ViewObject返回,需要解析VO物件
VO vo = (VO) rs;
if (vo.isTemplate()) {
String templateFile = vo.getTemplateFile();
if ("".equals(templateFile)) {
HttpHelper.sendErr(response, "此介面返回的VO設定了模板模式,但是你沒有設定任何對應的檔名,請使用setTemplateFile方法設定!");
} else {
File fDir = new File(ContextScanner.getContext().getRealPath(FrameworkMemoryStorage.staticFilePath));
String path = Tool.FileFinder(fDir, vo.getTemplateFile());
String s = Tool.FileReadByUTF8(path);
HashMap<String, Object> map = vo.getMap();
if (map.size() == 0) {
HttpHelper.sendNormal(response, s);
return;
}
String fullTemple = Tool.InjectKVMapToString(s, map);
HttpHelper.sendNormal(response, fullTemple);
}
} else {
HashMap<String, Object> map = vo.getMap();
if (map.size() == 0) {
HttpHelper.sendNormal(response, "");
return;
}
}
} else {
//預留
}
}else {
//沒有加註解的情況
if (rs instanceof VO){
HttpHelper.sendErr(response,"此方法返回為VO物件,需在方法上加註解MapToFile以支援VO物件返回!");
return;
}
HttpHelper.sendNormal(response,rs.toString());
}
對於註解了@ResponseBody的:
if(rs==null){
HttpHelper.sendNormal(response,"null");
return;
}
if(rs.getClass().isArray()){
try {
JSONArray jsonArray = JSONArray.fromObject(rs);
HttpHelper.sendJson(response,jsonArray.toString());
}catch (JSONException e){
HttpHelper.sendErr(response,"JSON生成異常!<br>"+e.getMessage());
}
}else if (rs instanceof Collection<?>){
try {
JSONArray jsonArray = JSONArray.fromObject(rs);
HttpHelper.sendJson(response,jsonArray.toString());
}catch (JSONException e){
HttpHelper.sendErr(response,"JSON生成異常!<br>"+e.getMessage());
}
}else {
try {
JSONObject jsonObject = JSONObject.fromObject(rs);
HttpHelper.sendJson(response,jsonObject.toString());
}catch (JSONException e){
HttpHelper.sendErr(response,"JSON生成異常!<br>"+e.getMessage());
}
}
對於靜態檔案的:
String fileLastName=fileName.substring(fileName.lastIndexOf(".")+1,fileName.length());
String fileURL=FrameworkMemoryStorage.staticFileDir+fileURI.substring(1,fileURI.length());
if(head.equals("*/*")){
//不設定頭部發送,全接受型
if(fileLastName.equals("woff")||fileLastName.equals("otf")||fileLastName.equals("eot")||fileLastName.equals("svg")||fileLastName.equals("woff2")||fileLastName.equals("ttf")){
//字型檔案,二進位制傳送(流)
HttpHelper.sendStream(response,Tool.FileReadByStream(fileURL));
}else {
HttpHelper.sendCustomTitle(response,"*/*",Tool.FileReadByUTF8(fileURL));
}
return;
}
if(head.contains("text")||head.contains("html")||head.contains("css")||head.contains("xml")){
//傳送文字類內容
HttpHelper.sendCustomTitle(response,head,Tool.FileReadByUTF8(fileURL));
} else if(head.contains("application")||head.contains("json")){
//Json格式
HttpHelper.sendJson(response,Tool.FileReadByUTF8(fileURL));
} else if(head.contains("image")||head.contains("webp")||head.contains("video")){
//傳送流
HttpHelper.sendStream(response,Tool.FileReadByStream(fileURL));
}
基本上就返回一個正常的請求了。
五、類管理
類管理ClassManage,只完成了最基本的操作,沒有那種神之操作,比較基礎。 在最開始的類掃描中,獲得了所有已標註@Mapping的類的Class物件,所以,在未使用時,這些類都是以Class物件存在的並沒有例項化。 哦,對了,忘了說,基本上所有的功能類都使用單例生成。但是哈,沒有測試有沒有影響,就是這樣寫:
private static ClassManage classManage;
private static ClassScanner scanner;
//這裡執行new,會不會產生什麼問題,畢竟下面進行對這個類的new
private static LifeRecycle lifeRecycle = new LifeRecycle();
private static CFLog cfLog=new CFLog(ClassManage.class);
public static ClassManage newInstance() {
if (classManage == null) {
classManage = new ClassManage();
lifeRecycle.autoRun();
return classManage;
} else {
return classManage;
}
}
當這個類裡面的成員變量出現賦初值的情況,且為static
時,再進行 new
操作時,會不會出現重複,上一個被例項化的變數會被怎麼處理,這裡博主還不是很明白其中的原理,其實是準備全部寫在newInstance()
裡面的,想到這幾個變數不需要單例,就放在外面初始賦值了,不知道影響大不大。
然後繼續講哈:
(1) 用了一個ClassManage$Bridge
內部靜態類來劃分操作,一些操作可以直接使用ClassManage的靜態方法呼叫,另外一些涉及ClassPool的操作就放在這裡面了。具體有這幾個方法:
checkClassIsInit()
檢查類是否例項化
newClass()
例項化類
stopLifeRecycle()
停止類回收機制(主要就是控制Timer停止)
(2) 私有的靜態內部類 ClassManage$LifeRecycle
用來控制類回收。因為自我感覺寫這個框架,static用的挺多的,所以記憶體使用量應該挺大的,在長時間不使用(不訪問)這個Service類的時候,就把它移出掉,能省則省。這個類裡面有個Timer在重複執行方法。
可以根據一個HashMap儲存的訪問計量來判斷是否移出。 最多一個類會被計數5次,每2分鐘檢查一次,所以當這個類訪問次數大於5次後,最長能存在10分鐘,因為2分鐘的迴圈,所以最最最大也就12分鐘存在,超過後會被回收掉例項化的物件。
使用方法:
…
下載地址:
開發日誌:
2018年8月10日
修改了對於靜態檔案的傳送機制,根據瀏覽器請求Accept值來回復,但是這裡面還有問題,對於瀏覽器傳送的 */* (任意)的請求沒有辦法。應該還是要判斷請求檔案的字尾名來決定回覆的型別,要不然就是重新設計一個對於靜態檔案的處理,讓它不會被Servlet接受,就不會出現靜態檔案進入Servlet了。
2018年9月7日
棄用之前的CFLog,改為最新版,之前版本不可用!!!
2018年10月30日10:40:02
結束一期