SpringBoot之仿写Tomcat实现方式的程序

SpringBoot之仿写Tomcat实现方式的程序

前提

在使用SpringBoot 的时候,也就一直再想,怎么去思考下他的部分底层,于是,开始对Tomcat实现方式的程序进行编写.

该程序通过使用,Java提供的socket,线程池,io流,String,map等API,模拟服务端对客户端的请求解析与结果响应的过程,然后我自身在创建了annotation注解,并使用dispatchServelet,handlerMapping对controller类和方法进行关系的映射与调用,从而实现了spring mvc对于controller中不同类型的传参方式.最后通过对项目中的容器和默认配置进行打包,最终程序可以按照SpringBoot开箱即用的方式来进行运行.

接下来是代码部分

一 WebServer 启动类

其中使用时socket,还有线程池的知识

package com.webserver.core;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * WebServer web容器
 * 用于实现Tomcat基本功能。
 */
public class WebServerApplication {
    
    private ServerSocket serverSocket;
    private ExecutorService threadPool;

    public WebServerApplication(){
        try {
            System.out.println("正在启动服务端...");
            serverSocket = new ServerSocket(8088);
            threadPool = Executors.newFixedThreadPool(50)//设置线程池大小;
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start(){
        try {
            while(true) {
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了!");
                //启动一个线程处理该客户端的交互
                ClientHandler handler = new ClientHandler(socket);
                threadPool.execute(handler);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    public static void main(String[] args) {
        WebServerApplication server = new WebServerApplication();
        server.start();
    }
}

二 客户端

用于连接操作

package com.webserver.core;

import com.webserver.http.EmptyRequestException;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.IOException;
import java.net.Socket;

/**
 * 与客户端完成一次HTTP的交互
 * 按照HTTP协议要求,与客户端完成一次交互流程为一问一答
 * 因此,这里分为三步完成该工作:
 * 1:解析请求  目的:将浏览器发送的请求内容读取并整理
 * 2:处理请求  目的:根据浏览器的请求进行对应的处理工作
 * 3:发送响应  目的:将服务端的处理结果回馈给浏览器
 *
 *
 */
public class ClientHandler implements Runnable{
    private Socket socket;
    public ClientHandler(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            //1解析请求,实例化请求对象的过程就是解析的过程
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);

            //2处理请求
            DispatcherServlet.getInstance().service(request,response);

            //3发送响应
            response.response();

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

        } finally {
            //按照HTTP协议要求,一问一答后断开连接
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
三 DispatcherServlet

用于接收客户端发过来的请求并处理

package com.webserver.core;

import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;
import com.webserver.controller.UserController;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;

/**
 * 用于完成一个http交互流程中处理请求的环节工作.
 * 实际上这个类是Spring MVC框架提供的一个核心的类,用于和Web容器(Tomcat)整合,
 * 使得处理请求的环节可以由Spring MVC框架完成.
 */
public class DispatcherServlet {
    private static DispatcherServlet instance = new DispatcherServlet();
    private static File dir;
    private static File staticDir;

    static {
        //定位环境变量ClassPath(类加载路径)中"."的位置
        //在IDEA中执行项目时,类加载路径是从target/classes开始的
        try {
            dir = new File(
                    DispatcherServlet.class.getClassLoader()
                            .getResource(".").toURI()
            );
            //定位target/classes/static目录
            staticDir = new File(dir, "static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private DispatcherServlet() {
    }

    public static DispatcherServlet getInstance() {
        return instance;
    }

    /**
     * 处理请求的方法
     *
     * @param request  请求对象,通过这个对象可以获取来自浏览器提交的内容
     * @param response 响应对象,通过设置响应对象将处理结果最终发送给浏览器
     */
    public void service(HttpServletRequest request, HttpServletResponse response) {
        String path = request.getRequestURI();
        System.out.println("请求的抽象路径:" + path);
        //path====>/regUser
        //首先判断该请求是否为请求一个业务
        try {
           Method method = HandlerMapping.getMethod(path);
           if(method!=null){
               method.invoke(method.getDeclaringClass().newInstance(),
                             request,response);
               return;
           }
        } catch (Exception e) {
            e.printStackTrace();
        }


        File file = new File(staticDir, path);
        if (file.isFile()) {//浏览器请求的资源是否存在且是一个文件
            //正确响应其请求的文件
            response.setContentFile(file);
        } else {
            //响应404
            response.setStatusCode(404);
            response.setStatusReason("NotFound");
            file = new File(staticDir, "/root/404.html");
            response.setContentFile(file);
        }
    }
}
四 HandlerMapping

使用反射,用来调用DispatcherServlet的各类请求

package com.webserver.core;

import com.webserver.annotations.Controller;
import com.webserver.annotations.RequestMapping;

import java.io.File;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 维护请求路径对对应的业务处理方法(某个Controller的某个方法)
 */
public class HandlerMapping {
    /*
        key:请求路径 例如:/regUser
        value:方法对象(Method实例)  例如:表示Controller的reg方法的Method对象
     */
    private static Map<String, Method> mapping = new HashMap<>();
    static {
        initMapping();
    }
    private static void initMapping(){
        try {
            File dir = new File(
                    HandlerMapping.class.getClassLoader()
                            .getResource(".").toURI()
            );
            File controllerDir = new File(dir,"/com/webserver/controller");
            File[] subs = controllerDir.listFiles(f->f.getName().endsWith(".class"));
            for(File sub : subs){//遍历目录中所有的.class文件
                String fileName = sub.getName();//获取文件名
                String className = fileName.substring(0,fileName.indexOf("."));//根据文件名截取出类名
                Class cls = Class.forName("com.webserver.controller."+className);//加载类对象
                if(cls.isAnnotationPresent(Controller.class)){//判断这个类是否被@Controller注解标注
                    Method[] methods = cls.getDeclaredMethods();//获取这个类定义的所有方法
                    for(Method method : methods){//遍历每一个方法
                        if(method.isAnnotationPresent(RequestMapping.class)){//判断该方法是否被@RequestMapping注解标注
                            RequestMapping rm = method.getAnnotation(RequestMapping.class);//获取该方法上的注解@RequestMapping
                            String value = rm.value();//获取该注解上定义的参数

                            mapping.put(value,method);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据请求路径返回对应的处理方法
     * @param path
     * @return
     */
    public static Method getMethod(String path){
        return mapping.get(path);
    }

    public static void main(String[] args) {
        Method method = mapping.get("/regUser");
        //通过方法对象可以获取其所属的类的类对象
        Class cls = method.getDeclaringClass();
        System.out.println(cls);
        System.out.println(method);
    }
}
五 HttpServletRequest

解析消息头,解析请求行,解析请求正文

package com.webserver.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class HttpServletRequest {
    private Socket socket;
    //请求行相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本

    private String requestURI;//抽象路径中请求部分,即:uri中"?"左侧的内容
    private String queryString;//抽象路径中参数部分,即:uri中"?"右侧的内容
    private Map<String,String> parameters = new HashMap<>();//保存每一组参数

    //消息头相关信息
    private Map<String,String> headers = new HashMap<>();

    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;
        //1解析请求行
        parseRequestLine();
        //2解析消息头
        parseHeaders();
        //3解析消息正文
        parseContent();
    }
    //解析请求行
    private void parseRequestLine() throws IOException, EmptyRequestException {
        String line = readLine();

        if(line.isEmpty()){//若请求行是空字符串,则说明本次为空请求,并抛出异常
            throw new EmptyRequestException("request is empty");
        }

        System.out.println("请求行:"+line);
        String[] data = line.split("\\s");
        method = data[0];
        uri = data[1];
        protocol = data[2];
        //进一步解析uri
        parseURI();

        System.out.println("method:"+method);
        System.out.println("uri:"+uri);
        System.out.println("protocol:"+protocol);
    }
    //进一步解析uri
    private void parseURI(){
         /*
            uri有两种情况:
            1:不含有参数的
              例如: /index.html
              直接将uri的值赋值给requestURI即可.

            2:含有参数的
              例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22
              将uri中"?"左侧的请求部分赋值给requestURI
              将uri中"?"右侧的参数部分赋值给queryString
              将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
              并将参数名作为key,参数值作为value存入到parameters中。
         */
        String[] data = uri.split("\\?");
        requestURI = data[0];
        if(data.length>1){//有参数
            //queryString:username=fancq&password=&nickname=chuanqi&age=22
            queryString = data[1];
            parseParameter(queryString);
        }

        System.out.println("requestURI:"+requestURI);
        System.out.println("queryString:"+queryString);
        System.out.println("parameters:"+parameters);
    }

    /**
     * 解析参数
     * 参数的格式应当为:name1=value1&name2=value2&...
     * @param line
     */
    private void parseParameter(String line){
        try {
            //中文格式的参数
            line = URLDecoder.decode(line,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        //paras:[username=fancq, password=, nickname=chuanqi, age=22]
        String[] paras = line.split("&");
        //para:username=
        for(String para : paras){
            //array:[username,fancq]   若没参数值array:[password]
            String[] array = para.split("=",2);
            parameters.put(array[0],array[1]);
        }
    }

    //解析消息头
    private void parseHeaders() throws IOException {
        //读取消息头
        while(true) {
            String line = readLine();
            if(line.isEmpty()){
                break;
            }
            System.out.println("消息头:" + line);
            String[] data = line.split(":\\s");
            //key:Connection value:keep-alive
            headers.put(data[0],data[1]);//key:消息头的名字  value:消息头的值

        }
        System.out.println("headers:"+headers);
    }
    //解析消息正文
    private void parseContent() throws IOException {
        //请求方式是否为POST请求
        if("post".equalsIgnoreCase(method)){
            if(headers.containsKey("Content-Length")) {
                //根据消息头Content-Length确定正文长度
                int contentLength = Integer.parseInt(
                    headers.get("Content-Length")
                );
                System.out.println("正文长度:"+contentLength);
                //读取正文数据
                InputStream in = socket.getInputStream();
                byte[] data = new byte[contentLength];
                in.read(data);
                /*
                    根据Content-Type来分析正文是什么以便进行对应的处理
                 */
                String contentType = headers.get("Content-Type");
                if("application/x-www-form-urlencoded".equals(contentType)){//是否为form表单提交数据
                    String line = new String(data, StandardCharsets.ISO_8859_1);
                    parseParameter(line);
                }
                //TODO
//                else if(){//比如判断表单提交时附带附件的.
//
//                }
            }

        }
    }

    private String readLine() throws IOException {
        //当对同一个socket调用多次getInputStream方法时,获取回来的输入流始终是同一条流
        InputStream in = socket.getInputStream();
        int d;
        StringBuilder builder = new StringBuilder();
        char pre='a',cur='a';
        while((d = in.read())!=-1){
            cur = (char)d;
            if(pre==13&&cur==10){
                break;
            }
            builder.append(cur);
            pre = cur;
        }
        return builder.toString().trim();
    }


    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

    public String getHeader(String name) {
        return headers.get(name);
    }

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    public String getParameter(String name) {
        return parameters.get(name);
    }
}
六 HttpServletResponse,响应,发送状态行,发送响应头,发送响应正文
package com.webserver.http;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 响应对象
 * 该类的每一个实例用于表示一个HTTP协议要求的响应内容
 * 每个响应由三部分构成:
 * 状态行,响应头,响应正文
 */
public class HttpServletResponse {
    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;
    private String statusReason = "OK";

    //响应头相关信息
    private Map<String,String> headers = new HashMap<>();

    //响应正文相关信息
    private File contentFile;//正文对应的实体文件
    //动态数据可以先通过该流写出到其内部维护的字节数组中,发送响应时将该数组内容作为正文
    private ByteArrayOutputStream out;


    public HttpServletResponse(Socket socket){
        this.socket = socket;
    }

    /**
     * 将当前响应对象内容按照标准的响应格式发送给客户端
     */
    public void response() throws IOException {
        //发送前的准备工作
        sendBefore();
        //发送状态行
        sendStatusLine();
        //发送响应头
        sendHeaders();
        //发送响应正文
        sendContent();
    }
    //发送响应前的准备工作
    private void sendBefore(){
        if(out!=null){//说明有动态数据
            //根据动态数据长度添加响应头Content-Length
            addHeader("Content-Length",out.size()+"");
        }
    }

    //发送状态行
    private void sendStatusLine() throws IOException {
//        HTTP/1.1 200 OK
        println("HTTP/1.1" + " " + statusCode + " " + statusReason);
    }
    //发送响应头
    private void sendHeaders() throws IOException {
        /*
            headers:
            key             value
            Content-Type    text/html
            Content-Length  245
            ...             ...
         */
        Set<Map.Entry<String,String>> entrySet = headers.entrySet();
        for(Map.Entry<String,String> e: entrySet){
            String name = e.getKey();
            String value = e.getValue();
            println(name + ": " + value);
        }
        //单独发送个回车+换行表示响应头发送完毕
        println("");
    }
    //发送响应正文
    private void sendContent() throws IOException {
        OutputStream out = socket.getOutputStream();
        if(this.out!=null){
            byte[] data = this.out.toByteArray();
            out.write(data);//将动态数据作为正文发送给浏览器
        }else if(contentFile!=null) {
            FileInputStream fis = new FileInputStream(contentFile);
            byte[] buf = new byte[1024 * 10];//10kb
            int len = 0;//记录每次实际读取的字节数
            while ((len = fis.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
        }
    }


    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        out.write(13);
        out.write(10);
    }

    public int getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatusReason() {
        return statusReason;
    }

    public void setStatusReason(String statusReason) {
        this.statusReason = statusReason;
    }

    public File getContentFile() {
        return contentFile;
    }

    public void setContentFile(File contentFile) {
        this.contentFile = contentFile;

        //添加用于说明正文的响应头Content-Type和Content-Length
        try {
            String contentType = Files.probeContentType(contentFile.toPath());
                /*
                    如果根据正文文件分析出了Content-Type的值则设置该响应头.
                    HTTP协议规定如果服务端发送的响应中没有包含这个头,就表明让浏览器自行判断
                    响应正文的内容类型.
                 */
            if(contentType!=null){
                addHeader("Content-Type",contentType);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        addHeader("Content-Length",contentFile.length()+"");



    }

    /**
     * 添加一个响应头
     * @param name  响应头的名字
     * @param value 响应头的值
     */
    public void addHeader(String name,String value){
        headers.put(name,value);
    }

    /**
     * 发送重定向响应,要求浏览器重新请求path指定的位置.
     * @param path
     */
    public void sendRedirect(String path){
        //重定向的状态代码为302
        statusCode = 302;
        statusReason = "MovedTemporarily";
        //响应头Location
        addHeader("Location",path);

    }

    /**
     * 获取字节输出流,通过这个流写出的所有字节最终都会作为响应正文发送给客户端
     * @return
     */
    public OutputStream getOutputStream(){
        if(out==null){
            out = new ByteArrayOutputStream();
        }
        return out;
    }

    public PrintWriter getWriter(){
        return new PrintWriter(
                new BufferedWriter(
                        new OutputStreamWriter(
                                getOutputStream(),
                                StandardCharsets.UTF_8
                        )
                ),true
        );
    }

    /**
     * 添加响应头Content-Type
     * @param mime
     */
    public void setContentType(String mime){
        addHeader("Content-Type",mime);
    }
}
七 空请求异常类
package com.webserver.http;

/**
 * 空请求异常
 * 当HttpServletRequest在解析请求时发现本次为空请求就会抛出这个异常
 */
public class EmptyRequestException extends Exception{
    public EmptyRequestException() {
    }

    public EmptyRequestException(String message) {
        super(message);
    }

    public EmptyRequestException(String message, Throwable cause) {
        super(message, cause);
    }

    public EmptyRequestException(Throwable cause) {
        super(cause);
    }

    public EmptyRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
八 Controller 和 RequestMapping 的注解类
package com.webserver.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}


package com.webserver.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value();
}

以上就是其中所用要用到的类,基本可以实现SpringBoot开箱即用的功能.

下一篇文章会介绍如何将这个项目打包,让自己可以使用…

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是呈祥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值