小练习简单的JAVAEE框架

发布时间 2023-11-23 12:11:46作者: 可乐还是要剩一点~

简单的JAVAEE框架

注意:本次框架练习是为了了解tomcat的框架底层代码

一、解析web.xml文件

package cn.servlet;

abstract class LoadConfig {
//缺省 不允许外包访问,抽象 不允许实例化 不能被继承
    private static Map<String,String > config;

    private LoadConfig(){

    }

    static {
        config = new HashMap<String,String>();
        load();
    }

    /**
     * 读取配置文件的方法
     */
    public static void load(){
        SAXBuilder buid = new SAXBuilder();
        try {
            Document doc = buid.build(new FileReader(new File("WEB-INF/web.xml")));
            Element root  = doc.getRootElement();
            XPath servletPath = XPath.newInstance("//servlet");
            XPath servletMappingPath = XPath.newInstance("//servlet-mapping");

            List<Element> servlets = servletPath.selectNodes(root);
            List<Element> servletMappings = servletMappingPath.selectNodes(root);

            for(Element map : servletMappings){
                //获得各个键所对应的文本
                String servletName = map.getChildText("servlet-name");
                String servletMappingName = map.getChildText("url-pattern");
                for(Element s : servlets){
                    if(servletName.equals(s.getChildText("servlet-name"))){
                        String servletClass = s.getChildText("servlet-class");
                        config.put(servletMappingName,servletClass);
                    }
                }
            }

        } catch (JDOMException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**取Map方法
     *
     * @return 返回读取完的Map
     */
    public static Map<String,String> getConfig(){
        return config;
    }

}

本过程是在cn.servlet包下进行的,该类为缺省的,抽象的不允许别人访问,与不允许被实例化,由于于配置文件只需读一次,所以生成Map和执行load()方法都写在静态块中,load()方法为读取web.xml文件方法。getConfig()是返回读取好的class和url键值对方法。

二、封装请求与响应

1.写出请求响应接口

先是写出请求与响应的接口,即HttpServletRequestHttpServletResponse两个接口

package cn.servlet.http;

/**
 * 对请求做封装
 */
public interface HttpServletRequest {
    String getMethod();                              //获取请求方式
    String getRequestURI();                        //获取URI
    String getProtocol();                             //获得协议
    String[] getHeads();                              //获取所有的请求头
    String getHead(String key);                 //获取请求头的键
    String getParameter(String name);     //返回?aaa=bbb& 中的输入aaa返回bbb 获得前端数据
}

去看看请求报文就知道为什么要有这些方法

package cn.servlet.http;

import java.io.PrintWriter;

/**
 * 对响应进行封装
 */
public interface HttpServletResponse {
    PrintWriter getWriter();
}

2.写所有控制器的父类

当写到这里就发现了一个问题,当我们在浏览器搜索栏搜索时要访问login界面类时,需要通过输入下图的/login,但这也牵扯到了一些JAVA类的运行,所以我们的框架会自动解析web.xml,找到URL所对应的class,进行反射实例化对象进而执行逻辑。

image-20231120232509805

image-20231120232931412

但是我们又会遇到问题,在别人使用我们代码时,不同的人写的类大有不同,我们怎么能解决这个问题 ?

当然是进行写一个所有控制器的父类,当别人想用我框架时就要进行继承该类。

本次将Httpservlet作为最终父类。

package cn.servlet.http;

import cn.servlet.ServletException;

import java.io.IOException;

//这里进行了简化,真正框架本应该是这种结构
//Servlet
//      –GenericServlet
//             –HttpServlet
//                        –自己的servlet
//太过复杂,本次就当HttpServlet是Servlet  即所有控制器的父类
public class HttpServlet {
    /**
     * 当我的对象被实例化时第一个执行的方法(初始化方法)
     */
    public void init(){

    }

    /**
     * 你只需自己所写的业务逻辑,不需要客户进行处理异常
     * 一般不要覆盖该方法
     */
    public void service(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
        if(request.getMethod().equalsIgnoreCase("get")){
            doGet(request, response);
        }else{
            doPost(request, response);
        }
    }
    public void doGet(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {

    }
    public void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {

    }
    /**
    *框架死亡前去调用
    */
    public void destroy(){

    }
}

对于这里发现service类处理异常时多了一个异常为ServletException,这是为什么呢?

因为我们框架在这里我们遇到的异常五花八门,我们不方便进行处理,所以要抛出到我们写的专门处理异常的类。

package cn.servlet;

/**
 * 直接继承throwable可以处理所有java异常
 */
public class ServletException extends Throwable{
    public ServletException(){

    }
    public ServletException(String msg){
        super(msg);
    }
}

3.实现请求响应接口

请求实现类

package cn.servlet.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

/**
 * 请求的实现子类
 */
public class HttpServletRequestImpl implements HttpServletRequest {
    private BufferedReader br;
    private String line; //请求报文第一行
    private String[] heads; //第一行按照“ ”做拆分
    private String params; //参数
    private Map<String, String> paramsMap; //请求头中的键值对
    private String requestHead;
    private Map<String, String> headMap; //请求体中的键值对

    public HttpServletRequestImpl(BufferedReader br) throws IOException {
        this.br = br;
        this.line = br.readLine();
        this.heads = line.split(" ");
        this.requestHead = line;
        headMap = new HashMap<String, String>();
        paramsMap = new HashMap<String, String>();
        while (true) {
            line = br.readLine();
            if (line.matches("^\\s*$")) {
                break;
            }
            String[] temp = line.split(": ");
            headMap.put(temp[0], temp[1]);
        }
        params = "";
        if (heads[1].contains("?")) {
            params = heads[1].substring(heads[1].indexOf("?") + 1);
            params = URLDecoder.decode(params, "UTF-8");
            params = params + "&";
            setParams(params);

        }
        if (heads[0].equalsIgnoreCase("post")) {
            //这里用字节流
            char[] c = new char[1024 * 1024];
            int len = br.read(c);
            String s = new String(c, 0, len);
            params = URLDecoder.decode(params, "UTF-8");
            params = params + "&";
            setParams(params);
        }
    }

    public void setParams(String params) {
        String[] pams = params.split("&");
        for (String s : pams) {
            String[] parts = s.split("=");
            paramsMap.put(parts[0], parts[1]);
        }
    }

    @Override
    public String getMethod() {
        return this.heads[0];
    }

    @Override
    public String getRequestURI() {
        return this.heads[1].contains("?") ? this.heads[1].substring(0, this.heads[1].indexOf("?")) : this.heads[1];
    }

    @Override
    public String getProtocol() {
        return this.heads[2];
    }

    @Override
    public String[] getHeads() {
        return heads;
    }

    @Override
    public String getHead(String key) {
        return this.headMap.get(key);
    }

    @Override
    public String getParameter(String name) {
        return this.paramsMap.get(name);
    }
}

看了上面的代码,我们发现了好多的字符串拆分,我们思考一下是为什么?

我们是服务器,客户端想请求我们的文件是需要发送请求报文的,并且现在的报文一般都是要符合http协议的,所以我们知道请求报文长啥样,我们就需要根据报文模版对报文进行解析,来获得有用的信息。所以我们才需要进行如此多的字符串拆分。

响应实现类

package cn.servlet.http;

import java.io.PrintWriter;

public class HttpServletResponseImpl implements HttpServletResponse{

    private PrintWriter out;

    /**响应封装的实现类
     *
     * @param out 输出响应的打印流
     * @param request 请求(我要知道请求是什么协议,我响应才知道是什么协议)
     * @param stat 服务器的状态
     */
    public HttpServletResponseImpl(PrintWriter out,HttpServletRequest request,String stat){
        this.out = out;
        out.println(request.getProtocol()+" "+stat+" OK\r\nContent-type: text/html; charset=UTF-8\r\n\r\n");
    }
    @Override
    public PrintWriter getWriter() {
        return out;
    }
}

对于响应实现类来说,实现就会相对简单,我们只需获取当前的请求报文的协议是什么,用户所请求的资源是什么,我们响应后的状态是什么,再以相同的协议将这些数据返回给客户就行。

三、写核心调度类

之前写的类都是为这个类所服务的,他是框架的核心,本次训练核心调度类为WebService

1.核心调度类WebService

我们已经知道,其他类都是为我这个类做辅助的,即为我提供各种数据。在这个类中我们只需要作为服务器,使用套接字监听固定端口就行。在有人连接我时,只需要进行初始化,开一个线程进行接收数据。

package cn.servlet;

import cn.servlet.http.HttpServlet;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

class WebService {
    private ServerSocket ss;
    private Socket s;
    private static Map<String, HttpServlet> servlets;
    private boolean bool;

    static {
        servlets = new HashMap<String, HttpServlet>();
    }

    static Map<String, HttpServlet> getServlets() {
        return servlets;
    }

    public WebService() {
        try {
            bool = true;
            ss = new ServerSocket(9001);
            System.out.println("服务器已启动,正在监视9001端口");
        } catch (IOException e) {
            e.printStackTrace();
        }
        init();
    }

    public void seBoolean(boolean bool) {
        this.bool = bool;
    }

    private void init() {
        while (bool) {
            try {
                s = ss.accept();
                new SessionThread(s).start();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        Set<String> sets = servlets.keySet();
        for (String key : sets) {
            servlets.get(key).destroy();
            sets.remove(key);
        }
    }
}

注意:在启动线程时While(bool)是当我服务器关闭时将bool赋值为false。之后就会执行:将servets 中所有的值放在sets 集合中,进行遍历销毁,再移出集合。

2.会话线程Sessionthread

这就是为核心调度类所调控的与客户进行交互的线程。

这个类也不难理解,思路如下:

  1. 需要获得套接字与用户之间进行交互
  2. 需要获得已解析所约定的web.xml文件
  3. 需要输入输出流,知道用户输入的是什么,我输出给用户是什么
  4. 要判断请求报文是否合法
  5. 通过请求报文获得uri,通过Config,按键取值获得获得类的路径
  6. 反射获得servlet对象
  7. 生成响应报文
  8. 执行servlet.init() servlet.service() servlet.distory() 方法
  9. 关闭流
package cn.servlet;

import cn.servlet.http.*;

import java.io.*;
import java.net.Socket;
import java.util.Map;

public class SessionThread extends Thread {
    private Socket s;
    private BufferedReader br;
    private PrintWriter out;
    private static Map<String, String> config;
    private static Map<String, HttpServlet> servlets;

    static {
        config = LoadConfig.getConfig();
        servlets = WebService.getServlets();
    }

    public SessionThread(Socket s) {
        this.s = s;
    }

    public void run() {
        HttpServletRequest request = null;
        HttpServletResponse response = null;
        HttpServlet servlet = null;
        BufferedReader brFile = null;
        try {
            br = new BufferedReader(new InputStreamReader(s.getInputStream()));
            out = new PrintWriter(new OutputStreamWriter(s.getOutputStream()), true);
            request = new HttpServletRequestImpl(br);

            if (request.getHeads().length != 3) {
                return;
            }
            if (!(request.getMethod()).equalsIgnoreCase("get") || (request.getMethod()).equalsIgnoreCase("post")) {
                return;
            }
            if (!(request.getRequestURI().startsWith("/"))) {
                return;
            }

            String uri = request.getRequestURI();
            String classPath = config.get(uri);
            //如果之前获得过该客户的对象,只需通过用户进行请求的uri进行按键取值,不需要再实例化了
            //如果没实例化就进行反射实例化对象
            if (classPath != null) {
                if (!servlets.containsKey(uri)) {
                    classPath = config.get(uri);
                    if (classPath != null) {
                        Class c = null;
                        c = Class.forName(classPath);
                        servlet = (HttpServlet) c.newInstance();
                        servlet.init();
                        //这里至关重要,通过反射生成Servlet对象,并且用户已经继承Servlet,重写了doGet或doPost方法,但我们只需进行执行service方法就可以运行用户的业务逻辑
                        servlets.put(uri, servlet);
                    }
                } else {
                    servlet = servlets.get(uri);
                }
                response = new HttpServletResponseImpl(out, request, "200");
                servlet.service(request, response);
                servlet.destroy();

            } else if (uri.toLowerCase().endsWith("html")) {
                File file = new File(uri.substring(1));
                if (file.isFile()) {
                    response = new HttpServletResponseImpl(out, request, "200");
                    brFile = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
                    String line = null;
                    while ((line = brFile.readLine()) != null) {
                        out.println(line);
                    }
                } else {
                    response = new HttpServletResponseImpl(out, request, "404");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ServletException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (brFile != null) {
                try {
                    brFile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                out.close();
            }
            if (request.getProtocol().equalsIgnoreCase("http/1.0")) {
                if (s != null) {
                    try {
                        s.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

  1. 这里会发现多了一个Map<String , HttpServlst> servlets
    • 这个容器是为了减轻我框架的压力,因为当一个用户反复请求我的页面时,每访问一次就会进行反射,产生新的对象,访问完成后调用distory方法进行销毁,这就会不停的产生销毁对象,效率低下。所以我就将每个用户的uri和产生的HttpServlet存入Map中,每次访问都会压入servlets集合中,在下一次访问中就会搜索servlets集合中是否有url这个键,如果有就找map将上次对象赋给他,如果没有就newInstence创建对象。
  2. 在最后关闭连接时候,可以看出http/1.0和http/1.1的差异。

到这里主要的就完成了

四、进行测试

1.写个方法启动服务器

package cn.servlet;

import cn.servlet.http.HttpServlet;

public class Start {
    public static void main(String[] args) {
        new WebService();
    }
}

2.写一个类继承HttpServlet

package cn.com.servlets;

import cn.servlet.ServletException;
import cn.servlet.http.HttpServlet;
import cn.servlet.http.HttpServletRequest;
import cn.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

public class ShowServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        PrintWriter pw = response.getWriter();
        pw.println("<html>");
        pw.println("<head>");
        pw.println("</head>");
        pw.println("<body>");
        pw.println("<h1>helloWord</h1>");
        pw.println("</bode>");
        pw.println("</html>");

    }
}

3.用浏览器访问事先规定的url参数

image-20231122230939358

完成了简单的框架,最后的结构如下:

image-20231123115755785