龙空技术网

从MVC原理开始手敲一个MVC框架,带你体会当大神的乐趣

代码熬夜敲 7180

前言:

当前姐妹们对“springmvc读取properties”大体比较关切,姐妹们都想要了解一些“springmvc读取properties”的相关知识。那么小编在网络上搜集了一些关于“springmvc读取properties””的相关知识,希望你们能喜欢,兄弟们快快来学习一下吧!

每一个程序员,在刚入行时学的第一个框架应该就是ssm了,其他的各种学习框架都是后期在工作中,随着业务的不断复杂,在工作和bug中不断成长起来的,但是最经典的应该还是最一开始的时候ssm框架吧

当时刚学习这个时候,就觉得,好牛啊,这样就可以实现一个网站,这群大神是怎么做到的啊,嘿嘿嘿,不知道大家当时有没有这样的问题产生,所以今天就带大家来搭建一个简单的mvc框架,从原理讲起,也能帮助大家更好的理解底层源码

好了,话不多说,我们来看一下

Springmvc基本原理流程

SpringMvc本质上就是对Servlet的封装。

因为创建一个Maven项目,然后在pom文件中增加一个依赖:

<dependency>  <groupId>javax.servlet</groupId>  <artifactId>servlet-api</artifactId>  <version>2.5</version>  <!-- 部署在服务器时,不使用这个servlet-api 而使用tomcat的-->  <scope>provided</scope></dependency>

2,创建DispatcherServlet,并注册到web.xml中

package com.dxh.edu.mvcframework.servlet;import javax.servlet.ServletConfig;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;public class DxhDispatcherServlet extends HttpServlet {    /**     *  接收处理请求     */    @Override    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        doPost(req, resp);    }    @Override    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {            }}

web.xml:

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "; ><web-app>  <display-name>Archetype Created Web Application</display-name>  <servlet>    <servlet-name>dxhmvc</servlet-name>    <servlet-class>com.dxh.edu.mvcframework.servlet.DxhDispatcherServlet</servlet-class>    <init-param>      <param-name>contextConfigLocation</param-name>      <param-value>springmvc.properties</param-value>    </init-param>  </servlet>  <servlet-mapping>    <servlet-name>dxhmvc</servlet-name>    <url-pattern>/*</url-pattern>  </servlet-mapping></web-app>
2,注解开发

因为要使用到注解,所以首先要自定义几个注解:

这里就不赘述如何自定义注解了,详情请看:

Controller注解:

@Documented@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface DxhController {    String value() default "";}

Service注解:

@Documented@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface DxhService {    String value() default "";}

RequestMapping注解:

@Documented@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface DxhRequestMapping {    String value() default "";}

Autowired注解:

@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface DxhAutowired {    String value() default "";}
编写测试代码:

测试代码我们放在同项目中的com.dxh.demo包中:

package com.dxh.demo.controller;import com.dxh.demo.service.IDemoService;import com.dxh.edu.mvcframework.annotations.DxhAutowired;import com.dxh.edu.mvcframework.annotations.DxhController;import com.dxh.edu.mvcframework.annotations.DxhRequestMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@DxhController@DxhRequestMapping("/demo")public class DemoController {    @DxhAutowired    private IDemoService demoService;    /**     * URL:/demo/query     */    @DxhRequestMapping("/query")    public String query(HttpServletRequest request, HttpServletResponse response, String name){        return demoService.get(name);    }}
package com.dxh.demo.service;public interface IDemoService {    String get(String name);}
package com.dxh.demo.service.impl;import com.dxh.demo.service.IDemoService;import com.dxh.edu.mvcframework.annotations.DxhService;@DxhService("demoService")public class IDemoServiceImpl implements IDemoService {    @Override    public String get(String name) {        System.out.println("Service实现类中的Name:"+ name);        return name;    }}
目录结构:3,编写自定义DispatcherServlet中的初始化流程:

在创建好的DxhDispatcherServlet中重写init()方法,并在init方法中做初始化配置:

加载配置文件 springmvc.properties扫描相关的类——扫描注解初始化Bean对象(实现IOC容器,基于注解)实现依赖注入构造一个handleMapping处理器映射器,将配置好的url和method建立映射关系

@Overridepublic void init(ServletConfig config) throws ServletException {    //1. 加载配置文件  springmvc.properties    String contextConfigLocation = config.getInitParameter("contextConfigLocation");    doLoadConfig(contextConfigLocation);    //2. 扫描相关的类——扫描注解    doScan("");    //3. 初始化Bean对象(实现IOC容器,基于注解)    doInstance();    //4. 实现依赖注入    doAutoWired();    //5. 构造一个handleMapping处理器映射器,将配置好的url和method建立映射关系    initHandleMapping();    System.out.println("MVC 初始化完成");    //6. 等待请求进入处理请求}

以及5个空方法,这篇文章自定义MVC框架其实就是需要对这5个步骤的编写。

//TODO 5,构造一个映射器private void initHandleMapping() {}//TODO 4,实现依赖注入private void doAutoWired() {}//TODO 3,IOC容器private void doInstance() {}//TODO 2,扫描类private void doScan(String scanPackage) {}//TODO 1,加载配置文件private void doLoadConfig(String contextConfigLocation) {}
3.1 加载配置文件首先在resource目录中创建一个配置文件——springmvc.properties

表示要扫描com.dxh.demo下的所有注解。然后在web.xml中进行配置:

这样,就可以通过config.getInitParameter("contextConfigLocation")获得这个路径。

在DxhDispatcherServlet中定义一个属性,我们把加载后的配置文件中的信息,存储在Properties 中

private Properties properties = new Properties();;
//1,加载配置文件private void doLoadConfig(String contextConfigLocation) {    //根据指定路径加载成流:    InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);    try {        properties.load(resourceAsStream);    } catch (IOException e) {        e.printStackTrace();    }}
3.2 扫描相关的类,扫描注解上一步骤,我们已经把需要扫描的包存在Properties中,所以这里需要取出:
//2. 扫描相关的类——扫描注解doScan(properties.getProperty("scanPackage"));
把扫描到的类型的全类名存在一个List中缓存起来,等待使用,在DxhDispatcherServlet中定义一个list:
//缓存扫描到的类的全类名private List<String> classNames = new ArrayList<>();
从配置文件中我们得到了一个需要扫描的包名(com.dxh.demo),我们需要根据classPath+包名,来得到它实际上在磁盘上存的路径,然后递归,直到把所有的该包下(包括子包...)所有的类文件(.class结尾)。然后存在在List classNames中。
//2,扫描类//scanPackage :com.dxh.demo    package--->磁盘的文件夹(File)private void doScan(String scanPackage) {    //1.获得classPath路径    String clasPath = Thread.currentThread().getContextClassLoader().getResource("").getPath();    //2.拼接,得到scanPackage在磁盘上的路径    String  scanPackagePath= clasPath + scanPackage.replaceAll("\\.","/");    File pack = new File(scanPackagePath);    File[] files = pack.listFiles();    for (File file : files) {        if (file.isDirectory()){ //子 package            //递归            doScan(scanPackage+"."+file.getName());  //com.dxh.demo.controller        }else if(file.getName().endsWith(".class")){            String className = scanPackage + "." + file.getName().replace(".class", "");            classNames.add(className);        }    }}
3.3 初始化Bean对象(实现IOC容器,基于注解)

上一步骤我们把扫描到的类的全类名放到了,list中,那么本次步骤需要遍历整个list:

遍历List,依次得到所有的全类名通过反射得到类对象根据类对象判断有没有注解,并区分controller和servicecontroller,它的id此处不做过多处理,不取value了,用类的首字母小写作为id,保存到IOC容器中。service,service层往往是有接口的,再以接口名为id再存入一份bean到ioc,便于后期根据接口类型注入完成

代码实现:

//IOC容器private Map<String,Object> ioc = new HashMap<>();
//3,IOC容器//基于classNames缓存的类的全限定类名,以及反射技术,完成对象创建和管理private void doInstance()  {    if (classNames.size()==0) return;    try{        for (int i = 0; i < classNames.size(); i++) {            String className = classNames.get(i);  //com.dxh.demo.controller.DemoController            //反射            Class<?> aClass = Class.forName(className);            //区分controller ,区分service            if (aClass.isAnnotationPresent(DxhController.class)){                //controller的id此处不做过多处理,不取value了,用类的首字母小写作为id,保存到IOC容器中                String simpleName = aClass.getSimpleName();//DemoController                String lowerFirstSimpleName = lowerFirst(simpleName); //demoController                Object bean = aClass.newInstance();                ioc.put(lowerFirstSimpleName,bean);            }else if (aClass.isAnnotationPresent(DxhService.class)){                DxhService annotation = aClass.getAnnotation(DxhService.class);                //获取注解的值                String beanName = annotation.value();                //指定了id就以指定的id为准                if (!"".equals(beanName.trim())){                    ioc.put(beanName,aClass.newInstance());                }else{                    //没有指定id ,首字母小写                    String lowerFirstSimpleName = lowerFirst(aClass.getSimpleName());                    ioc.put(lowerFirstSimpleName,aClass.newInstance());                }                //service层往往是有接口的,再以接口名为id再存入一分bean到ioc,便于后期根据接口类型注入                Class<?>[] interfaces = aClass.getInterfaces();                for (Class<?> anInterface : interfaces) {                    //以接口的类名作为id放入。                    ioc.put(anInterface.getName(),aClass.newInstance());                }            }else {                continue;            }        }    }catch (Exception e){        e.printStackTrace();    }}
3.4 实现依赖注入:

上一步骤把所有需要加载的bean,存入了ioc Map中,此时,我们就需要遍历这个map然后依次得到每个bean对象,然后判断对象中有没有被@****DxhAutowired修饰的属性。

遍历ioc这个map,得到每个对象获取对象的字段(属性)信息判断字段是否被@DxhAutowired修饰判断@DxhAutowired有没有设置value值有,直接从ioc容器中取出,然后设置属性。无,需要根据当前字段的类型注入(接口注入)

代码实现:

//4,实现依赖注入private void doAutoWired() {    if (ioc.isEmpty()){return;}    //1,判断容器中有没有被@DxhAutowried注解的属性,如果有需要维护依赖注入关系    for (Map.Entry<String,Object> entry: ioc.entrySet()){        //获取bean对象中的字段信息        Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();        for (Field declaredField : declaredFields) {            if (!declaredField.isAnnotationPresent(DxhAutowired.class)){                continue;            }            //有该注解:            DxhAutowired annotation = declaredField.getAnnotation(DxhAutowired.class);            String beanName = annotation.value(); //需要注入的bean的Id            if ("".equals(beanName.trim())){                //没有配置具体的beanId,需要根据当前字段的类型注入(接口注入)  IDemoService                beanName = declaredField.getType().getName();            }            //开启赋值            declaredField.setAccessible(true);            try {                //字段调用,两个参数:(哪个对象的字段,传入什么)                declaredField.set(entry.getValue(),ioc.get(beanName));            } catch (IllegalAccessException e) {                e.printStackTrace();            }        }    }}
3.5 构造一个handleMapping处理器映射器

构造一个handleMapping处理器映射器,将配置好的url和method建立映射关系****。

手写MVC框架最关键的环节

假设有一个:

那么如何通过/demo/query定位到 DemoController类中的query这个方法 ?

之前我们所有被@DxhController(自定义Controller注解)的类,都存在了ioc 这个map中。

我们可以遍历这个map,得到每个bean对象

然后判断是否被@DxhController所修饰(排除@DxhService所修饰的bean)

然后判断是否被@DxhRequestMapping所修饰,有的话,就取其value值,作为baseUrl

然后遍历该bean对象中的所有方法,得到被@DxhRequestMapping修饰的方法。得到其value值,作为methodUrl。

baseUrl + methodUrl = url

我们把url和当前method绑定起来,存在map中,也就是建立了url和method建立映射关系。

代码实现:

//handleMapping ,存储url和method直接的映射关系private Map<String,Object> handleMapping = new HashMap<>();
//5,构造一个映射器,将url和method进行关联private void initHandleMapping() {    if (ioc.isEmpty()){return;}    for (Map.Entry<String,Object> entry: ioc.entrySet()){        //获取ioc中当前遍历对象的class类型        Class<?> aClass = entry.getValue().getClass();        //排除非controller层的类        if (!aClass.isAnnotationPresent(DxhController.class)){            continue;        }        String baseUrl = "";        if (aClass.isAnnotationPresent(DxhRequestMapping.class)){            //Controller层 类上 注解@DxhRequestMapping中的value值            baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value();        }        //获取方法        Method[] methods = aClass.getMethods();        for (Method method : methods) {            //排除没有@DxhRequestMapping注解的方法            if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;}            //Controller层 类中方法上 注解@DxhRequestMapping中的value值            String methodUrl = method.getAnnotation(DxhRequestMapping.class).value();            String url = baseUrl+methodUrl;            //建立url和method之间的映射关系,用map缓存起来            handleMapping.put(url,method);        }    }}
4,测试一下:

到目前位置,还没有完全写完,但是不妨碍我们测试一下看看刚才写的那部分内容有没有什么问题:

完整的pom文件:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="; xmlns:xsi=";  xsi:schemaLocation=" ;>  <modelVersion>4.0.0</modelVersion>  <groupId>com.dxh.edu</groupId>  <artifactId>mvc</artifactId>  <version>1.0-SNAPSHOT</version>  <packaging>war</packaging>  <name>mvc Maven Webapp</name>  <url>;/url>  <properties>    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>    <maven.compiler.source>11</maven.compiler.source>    <maven.compiler.target>11</maven.compiler.target>  </properties>  <dependencies>    <dependency>      <groupId>junit</groupId>      <artifactId>junit</artifactId>      <version>4.12</version>      <scope>test</scope>    </dependency>    <dependency>      <groupId>javax.servlet</groupId>      <artifactId>servlet-api</artifactId>      <version>2.5</version>      <!-- 部署在服务器时,不使用这个servlet-api 而使用tomcat的-->      <scope>provided</scope>    </dependency>  </dependencies>  <build>    <plugins>      <plugins>  <!-- 编译插件定义编译细节-->      <plugin>        <groupId>org.apache.maven.plugins</groupId>        <artifactId>maven-compiler-plugin</artifactId>        <version>3.2</version>        <configuration>          <source>11</source>          <target>11</target>          <encoding>utf-8</encoding>          <!-- 告诉编译器,编译的时候记录下形参的真实名称-->          <compilerArgs>            <arg>-parameters</arg>          </compilerArgs>        </configuration>      </plugin>          <plugin>        <groupId>org.apache.tomcat.maven</groupId>        <artifactId>tomcat7-maven-plugin</artifactId>        <version>2.2</version>        <configuration>          <port>8080</port>          <path>/</path>        </configuration>      </plugin>    </plugins>  </build></project>

pom文件中加入了一个tomcat插件,并设置端口为8080,因此我们通过tomcat启动项目:

启动完成后,打开浏览器url中输入:

浏览器中什么都没返回(我们的代码还没真正的完成,尚未编写处理请求步骤),同时控制台中打印了MVC初始化完成,可以认为,目前的代码没有明显的缺陷。 我们继续~~~~~

5,改造initHandleMapping()5.1 为什么改造?

DxhDispatcherServlet这个类继承了HttpServlet,并重写了doGet和doPost方法,在doGet中调用了doPost方法,当我们使用反射调用方法时(method.invoke(......))发现少了一部分参数:

因此我们要改造initHandleMapping(),修改url和method的映射关系(不简简单单的存入map中)。

5.2 新建Handler类

package com.dxh.edu.mvcframework.pojo;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;import java.util.regex.Pattern;public class Handler {    //method.invoke(obj,)需要    private Object controller;    private Method method;    //spring中url支持正则    private Pattern pattern;    //参数的顺序,为了进行参数绑定  ,Key 参数名, Value 代表第几个参数    private Map<String,Integer> paramIndexMapping;    public Handler(Object controller, Method method, Pattern pattern) {        this.controller = controller;        this.method = method;        this.pattern = pattern;        this.paramIndexMapping = new HashMap<>();    }    //getset方法这里省略,实际代码中需要...}

在Handler类中编写了4个属性:

private Object controller:method.invoke(obj,)需要private Method method:与url绑定的方法private Pattern pattern:可以通过正则匹配,也可以直接写String url。private Map<String,Integer> paramIndexMapping:参数的顺序,为了进行参数绑定 ,Key 参数名, Value 代表第几个参数5.3 修改initHandleMapping()

首先,就不能直接通过Map<url,Method>的得方式进行关系映射了,使用一个list,泛型是刚才创建的Handler。

    //handleMapping ,存储url和method直接的映射关系//    private Map<String,Method> handleMapping = new HashMap<>();    private List<Handler> handlerMapping = new ArrayList<>();

改动前,改动后代码对比:

改动后的initHandleMapping():

//5,构造一个映射器,将url和method进行关联private void initHandleMapping() {    if (ioc.isEmpty()){return;}    for (Map.Entry<String,Object> entry: ioc.entrySet()){        //获取ioc中当前遍历对象的class类型        Class<?> aClass = entry.getValue().getClass();        //排除非controller层的类        if (!aClass.isAnnotationPresent(DxhController.class)){            continue;        }        String baseUrl = "";        if (aClass.isAnnotationPresent(DxhRequestMapping.class)){            //Controller层 类上 注解@DxhRequestMapping中的value值            baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value();        }        //获取方法        Method[] methods = aClass.getMethods();        for (Method method : methods) {            //排除没有@DxhRequestMapping注解的方法            if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;}            //Controller层 类中方法上 注解@DxhRequestMapping中的value值            String methodUrl = method.getAnnotation(DxhRequestMapping.class).value();            String url = baseUrl+methodUrl;            //把method所有信息以及url封装为Handler            Handler handler = new Handler(entry.getValue(),method, Pattern.compile(url));            //处理计算方法的参数位置信息            Parameter[] parameters = method.getParameters();            for (int i = 0; i < parameters.length; i++) {                Parameter parameter = parameters[i];                //不做太多的参数类型判断,只做:HttpServletRequest request, HttpServletResponse response和基本类型参数                if (parameter.getType()==HttpServletRequest.class||parameter.getType()==HttpServletResponse.class){                    //如果时request和response对象,那么参数名称存 HttpServletRequest 和 HttpServletResponse                    handler.getParamIndexMapping().put(parameter.getType().getSimpleName(),i);                }else{                    handler.getParamIndexMapping().put(parameter.getName(),i);                }            }            handlerMapping.add(handler);        }    }}
6, 请求处理开发 doPost():

上一步骤,我们配置了 uri和method的映射关系,并封装到了Handler中存入list,那么接下来,就要通过HttpServletRequest,取出uri,然后找到具体的Handler:

通过HttpServletRequest取出uri找到具体的Handler得到将调用方法的参数的数组根据上述数组长度创建一个新的数组(参数数组,传入反射调用的)通过req.getParameterMap()得到前台传来的参数parameterMap遍历parameterMap通过StringUtils.join方法把name=1&name=2格式的参数变为name[1,2] (需要commons-lang依赖)参数匹配并设置

private Handler getHandler(HttpServletRequest req) {    if (handlerMapping.isEmpty()){return null;}    String url = req.getRequestURI();    //遍历 handlerMapping    for (Handler handler : handlerMapping) {        Matcher matcher = handler.getPattern().matcher(url);        if (!matcher.matches()){continue;}        return handler;    }    return null;}
 @Override    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws  IOException {        //根据uri获取到能够处理当前请求的Handler(从handlerMapping中(list))        Handler handler = getHandler(req);        if (handler==null){            resp.getWriter().write("404 not found");            return;        }        //参数绑定        //该方法所有参数得类型数组        Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();        //根据上述数组长度创建一个新的数组(参数数组,传入反射调用的)        Object[] paramValues = new Object[parameterTypes.length];        //以下就是为了向参数数组中设值,而且还得保证参数得顺序和方法中形参顺序一致。        Map<String,String[]> parameterMap = req.getParameterMap();        //遍历request中所有的参数 ,(填充除了request、response之外的参数)        for (Map.Entry<String,String[]> entry: parameterMap.entrySet()){            //name=1&name=2 name[1,2]            String value = StringUtils.join(entry.getValue(), ",");// 如同 1,2            //如果参数和方法中的参数匹配上了,填充数据            if (!handler.getParamIndexMapping().containsKey(entry.getKey())){continue;}            //方法形参确实有该参数,找到它得索引位置,对应得把参数值放入paramValues            Integer index = handler.getParamIndexMapping().get(entry.getKey());            //把前台传递过来的参数值,填充到对应得位置去            paramValues[index] = value;        }        Integer requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName());        paramValues[requestIndex] = req;        Integer responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName());        paramValues[responseIndex] = resp;        //最终调用handler得method属性        try {            Object invoke = handler.getMethod().invoke(handler.getController(), paramValues);//简单操作,把方法返回的数据以字符串的形式写出resp.getWriter().write(invoke.toString());        } catch (IllegalAccessException e) {            e.printStackTrace();        } catch (InvocationTargetException e) {            e.printStackTrace();        }    }
7,测试:

打开浏览器,url中输入:

返回:

控制台中打印出:

OK完成~

标签: #springmvc读取properties