一、前言

只实现了主线流程,因为看源码主要还是需要掌握其核心流程做了什么,所以我也希望通过这种方式来进行源码阅读后的记录和总结。工程我已经搭建好并测试过了,注释也写得比较详细,感兴趣的小伙伴可以关注私聊我给你们发工程地址~

二、实现思路

  • 1、创建Maven工程
  • 2、创建控制层、业务层代码
  • 3、准备自定义注解和SpringMVC核心配置文件springmvc.xml
  • 4、准备前端控制器DispatcherServlet,并在web.xml文件中声明自定义的前端控制器
  • 5、创建Spring容器,通过DOM4J解析springmvc的XML文件
  • 6、扫描springmvc中的控制器以及service类并实例化对象放入容器中【iocMap】
  • 7、实现容器中对象的注入,比如将Service对象注入至Controller
  • 8、建立请求映射地址与控制器以及方法之间的映射关系【MyHandler对象存储】
  • 9、接收用户请求并进行分发操作【DispatcherServlet.doDispatcher()】
  • 10、Controller方法调用以及不同类型的响应数据处理

三、代码结构

Spring MVC(二)手写一个简易的Spring MVC框架_xml

四、上干货~

1、创建Maven工程

在pom文件中引入一些必要的依赖:

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

    <!--有梦想的肥宅-->
    <groupId>com.zhbf.springmvc</groupId>
    <artifactId>springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <!--设置maven编译的属性-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--导入servlet依赖-->
        <!--PS:SpringMvc底层还是依赖于servlet,所以需要导入这个包-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope><!--编译和测试阶段使用-->
        </dependency>
        <!--apache.commons工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <!--lombok包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>
        <!--jackson【json转换工具包】-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.3</version>
        </dependency>
        <!--XML解析包-->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--编译插件,用于把方法中的参数从arg[0],argp[1]转换成对应的参数名-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2、创建控制层、业务层代码

  这里就不一一粘贴代码了,涉及到的代码如下:

Spring MVC(二)手写一个简易的Spring MVC框架_xml_02

3、准备自定义注解和SpringMVC核心配置文件springmvc.xml

springmvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <!-- 配置创建容器时要扫描的包-->
    <component-scan base-package="com.zhbf.business.controller,com.zhbf.business.service"></component-scan>
</beans>

自定义注解

/**
 * 自定义注解【AutoWired】
 * -- @Retention: 注解保留策略
 * -- @Target: 注解作用域
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(value = RetentionPolicy.RUNTIME)//运行时保留,可以通过反射读取
public @interface AutoWired {

}

/**
 * 自定义注解【Controller】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target(ElementType.TYPE)//能在类、接口(包含注解类型)和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
    String value() default "";
}

/**
 * 自定义注解【RequestMapping】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target({ElementType.TYPE, ElementType.METHOD})//能在类、接口、方法和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value() default "";
}

/**
 * 自定义注解【ResponseBody】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target({ElementType.TYPE, ElementType.METHOD})//能在类、接口、方法和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseBody {
}

/**
 * 自定义注解【ResponseBody】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
@Target(ElementType.TYPE)//能在类、接口(包含注解类型)和枚举类型上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
    String value();
}

4、准备前端控制器DispatcherServlet,并在web.xml文件中声明自定义的前端控制器

web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <display-name>有梦想的肥宅手写SpringMVC测试</display-name>
    <!--1、配置前端控制器-->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>com.zhbf.springmvc.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!--2、配置web应用程序启动时加载servlet
            (1)load-on-startup 元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。
            (2)它的值必须是一个整数,表示servlet被加载的先后顺序。
            (3)如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
            (4)如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。
        -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!--3、将请求映射到对应的Servlet,“/”表示映射所有请求-->
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

DispatcherServlet

/**
 * 自定义前端控制器
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
public class DispatcherServlet extends HttpServlet {

    //自定义的SpringMvc容器
    private WebApplicationContext webApplicationContext;

    //创建集合:用于存放自定义映射关系对象,处理请求直接从该集合中进行匹配
    List<MyHandler> handList = new ArrayList<>();


    /**
     * 初始化方法【主线流程】
     *
     * @throws ServletException
     */
    @Override
    public void init() throws ServletException {

        //1、加载初始化参数【读取web.xml中配置的参数contextConfigLocation对应的值】
        String contextConfigLocation = this.getServletConfig().getInitParameter("contextConfigLocation");

        //2、创建Springmvc容器
        webApplicationContext = new WebApplicationContext(contextConfigLocation);

        //3、进行初始化操作
        try {
            webApplicationContext.init();
        } catch (Exception e) {
            System.out.println("初始化自定义IOC容器异常:" + e.getMessage());
            e.printStackTrace();
        }

        //4、初始化请求映射关系
        initHandlerAdapter();
    }

    /**
     * 初始化请求映射关系
     */
    private void initHandlerAdapter() {

        //1、轮询IOC容器集合
        for (Map.Entry<String, Object> entry : webApplicationContext.iocMap.entrySet()) {

            //2、获取bean的class类型
            Class<?> clazz = entry.getValue().getClass();

            //3、判断是否为Controller
            if (clazz.isAnnotationPresent(Controller.class)) {

                //3.1 获取Controller的RequestMapping
                String controllerMapping = "";
                if (clazz.isAnnotationPresent(RequestMapping.class)) {
                    controllerMapping = clazz.getAnnotation(RequestMapping.class).value();
                }

                //4、获取bean中所有的方法,为这些方法建立映射关系
                Method[] methods = clazz.getDeclaredMethods();
                for (Method method : methods) {

                    //5、如果含有注解RequestMapping则建立映射关系
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        String url = controllerMapping + requestMapping.value();//获取注解中的值

                        //6、建立RequestMapping地址与控制器方法的映射关系,保存到MyHandler对象中
                        MyHandler myHandler = new MyHandler(url, entry.getValue(), method);

                        //7、映射关系存入集合中
                        handList.add(myHandler);
                    }
                }
            }
        }
    }

    /**
     * Get请求处理
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        //PS:doGet内部调用doPost主要是为了统一处理方便
        this.doPost(request, response);
    }

    /**
     * Post请求处理
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        //进行请求分发处理
        this.doDispatcher(request, response);
    }

    /**
     * 进行请求分发处理
     */
    public void doDispatcher(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {

        //1、根据用户的请求地址找到对应的自定义映射关系对象【模拟HandlerMapping】
        MyHandler myHandler = getHandler(req);

        //2、获取方法返回对象
        Object result = null;
        if (myHandler == null) {
            resp.getWriter().print("<h1>404 NOT  FOUND!</h1>");
        } else {
            //2.1 获取目标方法【模拟HandlerAdapter】
            try {
                result = myHandler.getMethod().invoke(myHandler.getController());
            } catch (Exception e) {
                e.printStackTrace();
            }

            //2.2 设置响应内容
            if (result instanceof String) {//跳转JSP
                String viewName = (String) result;
                //判断返回的路径 forward:/test.jsp
                if (viewName.contains(":")) {
                    String viewType = viewName.split(":")[0];
                    String viewPage = viewName.split(":")[1];
                    if (viewType.equals("forward")) {//转发
                        req.getRequestDispatcher(viewPage).forward(req, resp);
                    } else {//重定向
                        resp.sendRedirect(viewPage);
                    }
                } else {//默认转发
                    req.getRequestDispatcher(viewName).forward(req, resp);
                }
            } else {//返回JSON格式数据
                Method method = myHandler.getMethod();
                if (method.isAnnotationPresent(ResponseBody.class)) {
                    //将返回值转换成 json格式数据
                    ObjectMapper objectMapper = new ObjectMapper();
                    String json = objectMapper.writeValueAsString(result);
                    resp.setContentType("text/html;charset=utf-8");
                    PrintWriter writer = resp.getWriter();
                    writer.print(json);
                    writer.flush();
                    writer.close();
                }
            }
        }
    }

    /***
     * 获取请求对应的handler
     * @param req
     * @return
     */
    public MyHandler getHandler(HttpServletRequest req) {
        // 1、读取请求的URI
        String requestURI = req.getRequestURI();
        //2、从容器的Handle取出URL和用户的请求地址进行匹配,找到满足条件的Handler
        for (MyHandler myHandler : handList) {
            if (myHandler.getUrl().equals(requestURI)) {
                return myHandler;
            }
        }
        return null;
    }
}

5、创建Spring容器,通过DOM4J解析springmvc的XML文件

/**
 * 应用上下文【Spring的IOC容器】
 *
 * @author 有梦想的肥宅
 * @date 2021/08/24
 */
public class WebApplicationContext {

    //定义容器配置的路径:classpath:springmvc.xml
    String contextConfigLocation;

    //定义List:用于存放bean的类路径【用于反射创建对象】
    List<String> classNameList = new ArrayList<String>();

    //定义IOC容器:key存放bean的名字,value存放bean实例
    public Map<String, Object> iocMap = new ConcurrentHashMap<>();


    /**
     * 无参构造方法
     */
    public WebApplicationContext() {
    }

    /**
     * 有参构造方法
     *
     * @param contextConfigLocation
     */
    public WebApplicationContext(String contextConfigLocation) {
        this.contextConfigLocation = contextConfigLocation;
    }

    /**
     * 初始化Spring容器
     */
    public void init() throws Exception {

        //1、解析springmvc.xml配置文件【com.zhbf.business.*】
        String pack = XmlPaser.getbasePackage(contextConfigLocation.split(":")[1]);
        String[] packs = pack.split(",");

        //2、进行包扫描
        for (String pa : packs) {
            excuteScanPackage(pa);
        }

        //3、实例化容器中bean
        executeInstance();

        //4、bean自动注入
        executeAutoWired();
    }

    /**
     * 扫描包
     *
     * @author 有梦想的肥宅
     */
    private void excuteScanPackage(String pack) {
        //1、把包路径转换为文件目录  com.zhbf.business  ==>  com/zhbf/business
        URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/"));
        String path = url.getFile();
        //2、通过IO流读取文件并解析
        File dir = new File(path);
        for (File f : dir.listFiles()) {
            if (f.isDirectory()) {
                //若当前为文件目录,则递归继续进行扫描
                excuteScanPackage(pack + "." + f.getName());
            } else {
                //若当前为文件,则获取全路径   UserController.class ==>  com.zhbf.business.controller.UserController
                String className = pack + "." + f.getName().replaceAll(".class", "");
                //3、存放bean的类路径【用于反射创建对象】
                classNameList.add(className);
            }
        }
    }

    /**
     * 实例化容器中的bean
     *
     * @author 有梦想的肥宅
     */
    private void executeInstance() throws Exception {

        // 1、循环存放bean的类路径的集合【用于反射创建对象】
        for (String className : classNameList) {

            //2、获取Class对象
            Class<?> clazz = Class.forName(className);

            //3、根据注解判断是Controller还是Service
            if (clazz.isAnnotationPresent(Controller.class)) {
                //Controller
                String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
                iocMap.put(beanName, clazz.newInstance());
            } else if (clazz.isAnnotationPresent(Service.class)) {
                //Service【如果是Service则读取@Service注解中设置的BeanName】
                Service serviceAn = clazz.getAnnotation(Service.class);
                String beanName = !StringUtils.isEmpty(serviceAn.value()) ? serviceAn.value() : clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
                iocMap.put(beanName, clazz.newInstance());
            }
        }
    }

    /**
     * 进行自动注入操作
     *
     * @author 有梦想的肥宅
     */
    private void executeAutoWired() throws IllegalAccessException {
        //1、从容器中取出bean并判断bean中是否有属性上使用了@AutoWired注解,如果使用了,就需要进行自动注入操作
        for (Map.Entry<String, Object> entry : iocMap.entrySet()) {

            //2、获取容器中的bean
            Object bean = entry.getValue();

            //3、获取bean中的属性
            Field[] fields = bean.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(AutoWired.class)) {

                    //4、获取属性值
                    String beanName = field.getName();
                    field.setAccessible(true);//取消检查机制

                    //5、在bean中设置iocMap.get(beanName)获得的对象
                    field.set(bean, iocMap.get(beanName));
                }
            }
        }
    }


}

6、扫描springmvc中的控制器以及service类并实例化对象放入容器中【iocMap】

  详见WebApplicationContext.excuteScanPackage(String pack)方法。

7、实现容器中对象的注入,比如将Service对象注入至Controller

  详见WebApplicationContext.executeAutoWired()方法。

8、建立请求映射地址与控制器以及方法之间的映射关系【MyHandler对象存储】

  详见DispatcherServlet.initHandlerAdapter()和DispatcherServlet.getHandler(HttpServletRequest req)方法。

9、接收用户请求并进行分发操作【DispatcherServlet.doDispatcher()】

  详见DispatcherServlet.doDispatcher()方法。

10、Controller方法调用以及不同类型的响应数据处理

  详见DispatcherServlet.doDispatcher()方法中2.2注释下的代码实现。

五、调用测试

Spring MVC(二)手写一个简易的Spring MVC框架_mvc_03

Spring MVC(二)手写一个简易的Spring MVC框架_maven_04

  PS:上面所放出的代码不是所有的代码,不过最主线核心的代码已经放在上面了,感兴趣的小伙伴可以自己尝试着补充未写在博客上的代码,或者联系我获取所有源码~共同探讨进步,共勉!🤞🤞🤞