从0到整写一个Mini-Spring/Web框架实现基础的功能


文章持续更新中…
目前字数: 15590


1. 针对于Spring/Web的执行流程

  1. 配置阶段
    1.1 配置Web.xml —> 我们自己写的DispatcherServlet
    1.2 设定init-param —> contextConfigLocation = classpath:application.properties
    1.3 设定url-pattern —> /*
    1.4 配置Annotation —> @XXCOntroller,@XXRequestMapping,@XXService…
  2. 初始化阶段
    2.1 调用init()方法 —> 加载配置文件
    2.2 初始化Ioc容器 —> Map ioc
    2.3 扫描相关的类 —> scan-package = “xxx.xxx.xx”
    2.4 创建实例化并保存到容器中 —> 通过反射机制,创建实例化且放入到初始化完成的IOC容器中
    2.5 DI注入操作 —> 扫描IOC容器中的实例,给没有赋值的属性自动赋值
    2.6 初始化HandlerMapping —> 将一个URL和一个Method进行一对一的关联映射Map
  3. 运行阶段
    3.1 调用doPost()/doGet() —> Web容器调用doPost/Get,获得Reqeust/Response对象
    3.2 匹配HandlerMapping —> 从request对象中获得用户输入的url,找到其对应的Method
    3.3 反射调用method.invoker() —> 利用反射调用方法并返回结果
    3.4 response.getWrite().write() —> 将结果返回输出到浏览器

2. 创建对应的项目工程

首先这是一个基础的Maven项目,且项目只需要依赖于一个Servlet-api即可。以下是最基础的pom文件,也是整个项目从头到尾所需要的依赖。


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>org.examplegroupId>
    <artifactId>implementSpringartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    properties>


    <dependencies>

        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>javax.servlet-apiartifactId>
            <version>4.0.1version>
        dependency>

    dependencies>

project>

在resource文件夹下新建一个application.properties 文件,里面暂时写一个k-v ,对应的value值是我们要扫描的包的路径。

scannerPackage = org.example.wzl

新建包为 mvc_farmeworke,且在下面暂时有一个包为 dispatcher,里面存放我们自己写的DispatcherServlet,
在这里我的DispatcherServlet暂且命名为ZLDispatcherServlet。

在完成上述工作后,我们配置对应的web.xml配置文件,配置DispatcherServlet为我们自己的DispatcherServlet。


<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                  http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <display-name>Wzl Web Applicationdisplay-name>

    <servlet>
        <servlet-name>wzlMvcservlet-name>
        <servlet-class>org.example.wzl.mvc_framework.dispatcher.ZLDispatcherServletservlet-class>
        <init-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>classpath:application.propertiesparam-value>
        init-param>
    servlet>

    <servlet-mapping>
        <servlet-name>wzlMvcservlet-name>
        <url-pattern>/*url-pattern>
    servlet-mapping>

web-app>

在完成以上基础项目搭建后,我们开始进行代码书写。


3. DispatcherServlet的编写

其实本质上,DispatcherServlet也是一个Servlet,他帮我们做了很多处理,做了封装。那么我们也是需要继承自HttpServlet,于是我们这个类的最基本结构就有了

public class ZLDispatcherServlet extends HttpServlet {

}

我们要做的事情如果是使用Servlet,那么我们直接重写doPost/Get即可,或者重写service即可。但是我们的目的是写一个我们自己的DispatcherServlet,我们就不能这样干。上面的执行流程也有说。存在初始化,存在方法调用doGet/Post,这些方法在HttpServlet和他的继承类GenericServlet做了实现,其中doGet/Post在HttpServlet中,init方法在GenericServlet中,于是就有了下面的代码

public class ZLDispatcherServlet extends HttpServlet {
    
	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

	@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    
   @Override
    public void init(ServletConfig config) throws ServletException {
    
    }
    
}

接下来我们就按照我们的步骤进行分析,我们主要的主打方法,在init()中。我们不论怎么样,一定是要先进行初始化的,首先我们会调用init方法,去加载配置文件,这一点是必不可少的。先读取web.xml中的配置文件,于是我们就可以写第一个方法。我们将方法定义为如下格式: doLoadConfig(config.getInitParameter(APPLICATION_LOCATION));
因为我们需要通过servlet提供的ServletConfig 在web.xml中取值,我们通过他就可以取值到我们init-param中的value。我们需要先获取到配置文件的位置。其中,APPLICATION_LOCATION为常量定义。

public class ZLDispatcherServlet extends HttpServlet {

    // 加载Pro文件
    private final Properties properties = new Properties();

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

	@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    
    @Override
    public void init(ServletConfig config) throws ServletException {
    	// 1. 加载配置文件
    	String APPLICATION_LOCATION = "contextConfigLocation";
    	doLoadConfig(config.getInitParameter(APPLICATION_LOCATION));
    }

	/**
     * 加载配置文件
     */
    private void doLoadConfig(String initParameter) {
        InputStream is = null;
        try {
            is = this.getClass().getClassLoader().getResourceAsStream(initParameter);
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != is) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }    

}

private void doLoadConfig(String initParameter) 方法介绍。

我们需要通过this.getClass().getClassLoader().getResourceAsStream()获取到一个配置文件流对象,然后我们会通过Java提供的Properties对象,来把这个流对象转换为Properties对象即可。Properties对象的实例是一个成员变量。

在完成配置文件的加载后,第二步就是容器初始化,对于容器初始化,其实就较为简单。我们需要创建一个IOC容器,也就是Map ioc = new HashMap<>();

public class ZLDispatcherServlet extends HttpServlet {
	
    // 加载Pro文件
    private final Properties properties = new Properties();
    
    // 2. 容器初始化
    private final Map<String, Object> ioc = new HashMap<>();

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

	@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    
    @Override
    public void init(ServletConfig config) throws ServletException {
    	// 1. 加载配置文件
    	String APPLICATION_LOCATION = "contextConfigLocation";
    	doLoadConfig(config.getInitParameter(APPLICATION_LOCATION));
    }

	/**
     * 加载配置文件
     */
    private void doLoadConfig(String initParameter) {
        InputStream is = null;
        try {
            is = this.getClass().getClassLoader().getResourceAsStream(initParameter);
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != is) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }    

}

第三步,扫描包下的类。相关的类。这个就是把我们配置文件application.properties中配置的scanPackage中的value值进行包扫描,然后扫描到的包下的类及其子类进行暂存,暂存到一个集合中。

public class ZLDispatcherServlet extends HttpServlet {
	
    // 加载Pro文件
    private final Properties properties = new Properties();
    
    // 2. 容器初始化
    private final Map<String, Object> ioc = new HashMap<>();

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

	@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    
    @Override
    public void init(ServletConfig config) throws ServletException {
    	// 1. 加载配置文件
    	String APPLICATION_LOCATION = "contextConfigLocation";
    	doLoadConfig(config.getInitParameter(APPLICATION_LOCATION));
    	
		// 3. 扫描相关的类
        doScanner(properties.getProperty("scannerPackage"));
    }

	/**
     * 加载配置文件
     */
    private void doLoadConfig(String initParameter) {
        InputStream is = null;
        try {
            is = this.getClass().getClassLoader().getResourceAsStream(initParameter);
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != is) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }    

}

doScanner(properties.getProperty(“scannerPackage”));

方法介绍,用来进行包扫描,传入一个对象为我们配置文件中k对应的value,也就是 xxx.xx.xx包路径
实际的方法为

private void doScanner(String scannerPackage) {}

需要一个String对象,具体的需要内容为需要的scanPacakage对应的value值。获取到对应的要扫描的包路径。

 /**
     * 扫描相关的类
     */
    private void doScanner(String scannerPackage) {
        // 做替换,找到包下的所有的内容。
        URL url = this.getClass().getClassLoader().getResource(scannerPackage.replaceAll("\\.", "/"));

        // 拿到文件夹的路径
        assert url != null;
        File classPath = new File(url.getFile());

        // 对这个文件夹进行迭代,看看里面都有那些文件
        for (File file : Objects.requireNonNull(classPath.listFiles())) {
            // 包里可能存在另外的包,所以需要迭代
            if (file.isDirectory()) {
                // 进行递归。用现有的包名称 + 子包
                doScanner(scannerPackage + "/" + file.getName());
            } else {
                // 包下可能存在很多文件类型如,.xml , .pro .txt等,所以我们只需要扫描class文件,如果不是,则回溯, 反之继续向下
                if (!file.getName().endsWith(".class"))  {continue;}
                // 通过拼接获取到一个全包类名 如 org.example.wzl.Main
                String className = (scannerPackage + "." + file.getName().replaceAll(".class", ""));
                // 暂存到容器中
                classNames.add(className);
                // fullClassName 进行反射
            }
        }
    }

逐行解析一下这个方法里面的代码

URL url = this.getClass().getClassLoader().getResource(scannerPackage.replaceAll("\\.", "/"));

第一行代码,获取到一个url对象,通过this.getClass().getClassLoader().getResource()获取到classpath下的路径,然后通过我们获取到的配置文件内的 xxx.xxx.xx 然后做拼接,并且把 符号 . 替换为 /,从而拼接成一个完整的url路径。

assert url != null;
File classPath = new File(url.getFile());

// 对这个文件夹进行迭代,看看里面都有那些文件
for (File file : Objects.requireNonNull(classPath.listFiles())) {
    // 包里可能存在另外的包,所以需要迭代
    if (file.isDirectory()) {
        // 进行递归。用现有的包名称 + 子包
        doScanner(scannerPackage + "/" + file.getName());
    } else {
        // 包下可能存在很多文件类型如,.xml , .pro .txt等,所以我们只需要扫描class文件,如果不是,则回溯, 反之继续向下
        if (!file.getName().endsWith(".class"))  {continue;}
        // 通过拼接获取到一个全包类名 如 org.example.wzl.Main
        String className = (scannerPackage + "." + file.getName().replaceAll(".class", ""));
        // 暂存到容器中
        classNames.add(className);
        // fullClassName 进行反射
    }
}

然后首先使用断言判断一下url不为空,然后执行下面的File实例的创建,使用url.getFile()这个方法,获取到完整的File路径,然后创建File实例。

在拥有File实例后,通过迭代,来看看里面都有那些文件,首先需要判断你这个迭代到的File对象是不是一个文件夹,因为包里面还有包,包实际上就是一个文件夹。如果是文件夹的话,我们也需要遍历文件夹内的内容,所以只需要通过递归即可,传入现有的包名 + 文件夹名称

 doScanner(scannerPackage + "/" + file.getName());

那么另一种情况就是,如果这个不是一个文件夹,我们就需要判断一下他是不是其他文件,因为我们只要.class结尾的Java文件。所以我们做了以下判断。

if (!file.getName().endsWith(".class"))  {continue;}

如果这个名称的结尾不等于.class,我们直接回退即可下面代码无需执行。如果他是一个Java文件的话,我们就需要把他拼接起来,拼成一个全包类名,比如 org.wzl.pojo.Main 这种格式的。于是就有了这一行代码

 String className = (scannerPackage + "." + file.getName().replaceAll(".class", ""));

这个要点也就是 .replaceAll(“.class”, “”); 其实也就是把结尾的.class 替换为 空字符串。于是我们就得到了一个全包类名,我们会创建一个暂时的容器,为一个List集合。将这个全包类名存储到该集合中(暂存)。

最后得到已下完整代码

public class ZLDispatcherServlet extends HttpServlet {
	
    // 加载Pro文件
    private final Properties properties = new Properties();
    
    // 2. 容器初始化
    private final Map<String, Object> ioc = new HashMap<>();
    
	// 存入全包类名的容器。
    private final List<String > classNames = new ArrayList<>();

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

	@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    
    @Override
    public void init(ServletConfig config) throws ServletException {
    	// 1. 加载配置文件
    	String APPLICATION_LOCATION = "contextConfigLocation";
    	doLoadConfig(config.getInitParameter(APPLICATION_LOCATION));
    	
		// 3. 扫描相关的类
        doScanner(properties.getProperty("scannerPackage"));
    }


    /**
     * 扫描相关的类
     */
    private void doScanner(String scannerPackage) {
        // 做替换,找到包下的所有的内容。
        URL url = this.getClass().getClassLoader().getResource(scannerPackage.replaceAll("\\.", "/"));

        // 拿到文件夹的路径
        assert url != null;
        File classPath = new File(url.getFile());

        // 对这个文件夹进行迭代,看看里面都有那些文件
        for (File file : Objects.requireNonNull(classPath.listFiles())) {
            // 包里可能存在另外的包,所以需要迭代
            if (file.isDirectory()) {
                // 进行递归。用现有的包名称 + 子包
                doScanner(scannerPackage + "/" + file.getName());
            } else {
                // 包下可能存在很多文件类型如,.xml , .pro .txt等,所以我们只需要扫描class文件,如果不是,则回溯, 反之继续向下
                if (!file.getName().endsWith(".class"))  {continue;}
                // 通过拼接获取到一个全包类名 如 org.example.wzl.Main
                String className = (scannerPackage + "." + file.getName().replaceAll(".class", ""));
                // 暂存到容器中
                classNames.add(className);
                // fullClassName 进行反射
            }
        }
    }


	/**
     * 加载配置文件
     */
    private void doLoadConfig(String initParameter) {
        InputStream is = null;
        try {
            is = this.getClass().getClassLoader().getResourceAsStream(initParameter);
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != is) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }    

}

在扫描全包类名完成后,下一步要进行的就是初始化所有实例,并且将创建好的实例放入到IOC容器中。我们使用这个方法 doInstance(); 其实代码很简单,如下

 /**
  * 初始化所有类相关的实例,并且放入到IOC容器中
  */
 private void doInstance() {
     // 如果classNames容器内的内容为空,则直接返回即可
     if (classNames.isEmpty()) {return;}
     try {
         for (String className : classNames) {
             Class<?> aClass = Class.forName(className);
             // 通过反射创建新实例
             Object instance = aClass.newInstance();
             // 创建bean key  规则为 首字母小写,其他驼峰 通过getSimpleName获取到纯粹的类名。默认大写
             String iocBeanKey = toLowerFirstCase(aClass.getSimpleName());
             // IOC容器添加
             ioc.put(iocBeanKey, instance);
         }
     } catch (Exception e) {
         e.printStackTrace();
     }
 }

依然是逐行分析一下,首先做了一个判断,如果我们上面刚刚暂存全包类名的路径的一个集合为空,则说明一个对象都没有扫描到,则IOC的实例创建没有必要,直接return即可。

// 如果classNames容器内的内容为空,则直接返回即可
if (classNames.isEmpty()) {return;}

如果该集合内存在数据,则说明实实在在的扫描到了已存在的对象,且以全包类名的形式进行存在,那么既然有全包类名,即可通过反射来进行对象的实例化。代码如下

try {
	for (String className : classNames) {
	    Class<?> aClass = Class.forName(className);
	    // 通过反射创建新实例
	    Object instance = aClass.newInstance();
	    // 创建bean key  规则为 首字母小写,其他驼峰 通过getSimpleName获取到纯粹的类名。默认大写
	    String iocBeanKey = toLowerFirstCase(aClass.getSimpleName());
	    // IOC容器添加
	    ioc.put(iocBeanKey, instance);
	} catch (Exception e) {
   		e.printStackTrace();
}

首先遍历我们刚刚的暂存容器,里面存放我们扫描到的全包类名,然后我们首先通过Class.forName要求Jvm去找到并且返回对应的类,然后我们可以通过Jvm提供到的Class aClass对象,来对该对象进行一个实例化,也就是以下一行代码

// 通过反射创建新实例
Object instance = aClass.newInstance();

听过反射创建实例完成后,那我们就需要对其bean名称命名规则进行一定的定义,首先大家都知道的是,在Spring框架中,Ioc的bean命名规则默认为类的首字母小写且后续驼峰。如 PojoService 在Ioc容器中默认的名字为pojoService,所以我们需要对我们的一个类名称进行一个解析,我们单独的拉出去一个专门进行该功能实现的方法,芳芳名称为 toLowerFirstCase(String className);需要传入一个String类型的值,返回一个首字母小写且其他为驼峰命名法的单词。以此来实现Ioc容器中Bean的命名规则。

/**
 * 返回一个首字母小写且驼峰命名法的单词
 */
private String toLowerFirstCase(String simpleName) {
    char[] ascii = simpleName.toCharArray();
    // 防止有些傻逼类首字母小写,做ascii判断
    if (ascii[0] > 96 && ascii[0] < 123) {
        // 在这个区间内说明就是小写了
        return String.valueOf(ascii);
    }
    // 使其大写变小写 ascii码
    ascii[0] += 32;
    return String.valueOf(ascii);
}

上述代码做了实现,采用ASCII码的形式进行了一个较为舒适的转换。且同时判断了如果一开始类就是小写的突发情况。在做了上述工作后,我们就可以得到一个完美符合bean命名规则的一个方法。

在这一行代码有进行书写。

// 创建bean key  规则为 首字母小写,其他驼峰 通过getSimpleName获取到纯粹的类名。默认大写
String iocBeanKey = toLowerFirstCase(aClass.getSimpleName());

我们获取到的bean命名规则名称为 iocBeanKey,实例有了,名称有了,接下来就是向Ioc容器中添加

// IOC容器添加
ioc.put(iocBeanKey, instance);

在写完这一步骤后,我们得到了以下的代码

public class ZLDispatcherServlet extends HttpServlet {
	
    // 加载Pro文件
    private final Properties properties = new Properties();
    
    // 2. 容器初始化
    private final Map<String, Object> ioc = new HashMap<>();
    
	// 存入全包类名的容器。
    private final List<String > classNames = new ArrayList<>();

	@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

	@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    
    @Override
    public void init(ServletConfig config) throws ServletException {
    	// 1. 加载配置文件
    	String APPLICATION_LOCATION = "contextConfigLocation";
    	doLoadConfig(config.getInitParameter(APPLICATION_LOCATION));
    	
		// 3. 扫描相关的类
        doScanner(properties.getProperty("scannerPackage"));
    }

	 /**
	  * 初始化所有类相关的实例,并且放入到IOC容器中
	  */
	 private void doInstance() {
	     // 如果classNames容器内的内容为空,则直接返回即可
	     if (classNames.isEmpty()) {return;}
	     try {
	         for (String className : classNames) {
	             Class<?> aClass = Class.forName(className);
	             // 通过反射创建新实例
	             Object instance = aClass.newInstance();
	             // 创建bean key  规则为 首字母小写,其他驼峰 通过getSimpleName获取到纯粹的类名。默认大写
	             String iocBeanKey = toLowerFirstCase(aClass.getSimpleName());
	             // IOC容器添加
	             ioc.put(iocBeanKey, instance);
	         }
	     } catch (Exception e) {
	         e.printStackTrace();
	     }
	 }

	/**
	 * 返回一个首字母小写且驼峰命名法的单词
	 */
	private String toLowerFirstCase(String simpleName) {
	    char[] ascii = simpleName.toCharArray();
	    // 防止有些傻逼类首字母小写,做ascii判断
	    if (ascii[0] > 96 && ascii[0] < 123) {
	        // 在这个区间内说明就是小写了
	        return String.valueOf(ascii);
	    }
	    // 使其大写变小写 ascii码
	    ascii[0] += 32;
	    return String.valueOf(ascii);
	}

    /**
     * 扫描相关的类
     */
    private void doScanner(String scannerPackage) {
        // 做替换,找到包下的所有的内容。
        URL url = this.getClass().getClassLoader().getResource(scannerPackage.replaceAll("\\.", "/"));

        // 拿到文件夹的路径
        assert url != null;
        File classPath = new File(url.getFile());

        // 对这个文件夹进行迭代,看看里面都有那些文件
        for (File file : Objects.requireNonNull(classPath.listFiles())) {
            // 包里可能存在另外的包,所以需要迭代
            if (file.isDirectory()) {
                // 进行递归。用现有的包名称 + 子包
                doScanner(scannerPackage + "/" + file.getName());
            } else {
                // 包下可能存在很多文件类型如,.xml , .pro .txt等,所以我们只需要扫描class文件,如果不是,则回溯, 反之继续向下
                if (!file.getName().endsWith(".class"))  {continue;}
                // 通过拼接获取到一个全包类名 如 org.example.wzl.Main
                String className = (scannerPackage + "." + file.getName().replaceAll(".class", ""));
                // 暂存到容器中
                classNames.add(className);
                // fullClassName 进行反射
            }
        }
    }


	/**
     * 加载配置文件
     */
    private void doLoadConfig(String initParameter) {
        InputStream is = null;
        try {
            is = this.getClass().getClassLoader().getResourceAsStream(initParameter);
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != is) {
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }    

}

你可能感兴趣的:(Java,经验分享,Spring,spring,servlet,mvc,java)