【H】[框架类] H-Servlet 简单的Web框架
这是一个系列,【 】里面是作为代号,[ ]里面是指这个jar的类型
目录:
项目地址:
https://github.com/Heshiqian/H-Servlet (暂时最好不要Clone,可能导入不能使用)
篇幅较长,请耐心看完,整个框架的原理基本就在这里了。
实现原理:
大致就是:
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
使用方法:
…
下载地址:
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
结束一期