【H】[框架类] H-Servlet 简单的Web框架


这是一个系列,【 】里面是作为代号,[ ]里面是指这个jar的类型

目录:

项目地址:
https://github.com/Heshiqian/H-Servlet (暂时最好不要Clone,可能导入不能使用)
篇幅较长,请耐心看完,整个框架的原理基本就在这里了。


实现原理:

【H】[框架类] H-Servlet 简单的Web框架,基于Tomcat服务器

大致就是:
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地址,如果不存在这样的地址就会返回错误,没有此接口;如果有这个接口,此方法内就会继续判断这个类是否已被初始化加入到池中,已经加入了就会直接取用,没有初始化就会加载此类,并添加进类池,同时类回收机制也在运行中。回收机制后续会讲。

此类的完整源码还请在GitHub上查看,建议有反射基础后再看,比较容易理解。
https://github.com/Heshiqian/H-Servlet/blob/master/src/cn/heshiqian/framework/h/servlet/handler/CenterHandle.java


四、视图处理

说是视图处理,其实就是返回数据;当中对执行结果进行了些许简单判断(因为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分钟存在,超过后会被回收掉实例化的对象。

还是老样子,源码还请移步GitHub查看,这里基本上原理是说了,不清楚还可以留言问我哦~
ClassManage
https://github.com/Heshiqian/H-Servlet/blob/master/src/cn/heshiqian/framework/h/servlet/classs/ClassManage.java
ClassPool
https://github.com/Heshiqian/H-Servlet/blob/master/src/cn/heshiqian/framework/h/servlet/classs/ClassPool.java


使用方法:

H-Servlet框架教程【第一章】基础环境建立

H-Servlet框架教程【第二章】第一个接口

H-Servlet框架教程【第三章】注解以及使用方法


下载地址:

http://www.heshiqian.cn/getHServlet.html 官网下载((#.#))

链接: https://pan.baidu.com/s/1M7Njo0mj8_F-NT5gcfMamA 提取码: car1 百度云


开发日志:

2018年8月10日

修改了对于静态文件的发送机制,根据浏览器请求Accept值来回复,但是这里面还有问题,对于浏览器发送的 */* (任意)的请求没有办法。应该还是要判断请求文件的后缀名来决定回复的类型,要不然就是重新设计一个对于静态文件的处理,让它不会被Servlet接受,就不会出现静态文件进入Servlet了。

2018年9月7日

弃用之前的CFLog,改为最新版,之前版本不可用!!!

2018年10月30日10:40:02

结束一期

相关文章:

  • 2022-01-06
  • 2021-11-11
  • 2021-09-21
  • 2022-12-23
  • 2021-12-12
  • 2022-01-20
  • 2022-12-23
  • 2021-12-04
猜你喜欢
  • 2021-09-28
  • 2022-12-23
  • 2021-06-25
  • 2021-10-07
  • 2022-12-23
  • 2021-10-08
相关资源
相似解决方案