1. 程式人生 > >【H】[框架類] H-Servlet 簡單的Web框架,基於Tomcat伺服器

【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

結束一期