手写简化版SpringMVC

目录

基本思路与流程

环境搭建与配置

核心实现

初始化

加载配置文件

扫描相关的类

IOC容器初始化

依赖注入

初始化HandlerMapping

请求分发

验证


基本思路与流程

环境搭建与配置

 1. 创建maven工程,然后在src/main/webapp/WEB-INF目录下创建web.xml文件,文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<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>My Web Application</display-name>
    <servlet>
        <servlet-name>mvc</servlet-name>
        <servlet-class>com.hujy.servlet.MyDispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>application.properties</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>mvc</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

2. 在src/main/resources资源路径下创建application.properties配置文件,文件内容为:

scanPackage = com.hujy.mvc

3. 创建com.hujy.servlet.MyDispatcherServlet文件,作为项目启动的servlet,需要继承HttpServlet,后续核心的核心功能都要在这个类里完成。

4. 创建自定义注解,模拟SpringMVC

@MyAutowired

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

@MyController

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

@MyRequestMapping

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

@MyRequestParam 

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

@MyService 

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

5. 业务测试类

service接口与实现

public interface DemoService {
    String hello(String name);
}
@MyService
public class DemoServiceImpl implements DemoService {
    @Override
    public String hello(String name) {
        return "hello " + name;
    }
}

controller

@MyController
@MyRequestMapping("/demo")
public class DemoController {

    @MyAutowired
    private DemoService demoService;

    @MyRequestMapping("/hello")
    public void hello(HttpServletRequest req, HttpServletResponse resp, @MyRequestParam("name") String name) {
        String result = demoService.hello(name);
        try {
            resp.getWriter().write(result);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 最终目录结构为:

核心实现

MyDispatcherServlet继承了HttpServlet,重写了其中的doGet、doPost、init方法。

初始化

init方法定义了整个初始化流程,具体分成五大步骤:

  1. 加载配置文件
  2. 扫描相关的类
  3. 初始化扫描到的类,并且将它们放入到IOC 容器之中
  4. 完成依赖注入
  5. 初始化HandlerMapping
public class MyDispatcherServlet extends HttpServlet {

    //保存application.properties 配置文件中的内容
    private Properties contextConfig = new Properties();

    //保存扫描的所有的类名
    private List<String> classNames = new ArrayList<>();

    // ioc容器
    private Map<String, Object> ioc = new HashMap<>();

    //保存所有的Url和方法的映射关系
    private List<Handler> handlerMapping = new ArrayList<>();

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

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            doDispatch(req, resp);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void init(ServletConfig config) {
        //1、加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
        //2、扫描相关的类
        doScanner(contextConfig.getProperty("scanPackage"));
        //3、初始化扫描到的类,并且将它们放入到IOC 容器之中
        doInstance();
        //4、完成依赖注入
        doAutowired();
        //5、初始化HandlerMapping
        initHandlerMapping();
        System.out.println("**************** My Spring MVC has been initialized successfully *********************");
    }

    ......
}

加载配置文件

加载application.properties文件的内容

    /**
     * 加载配置文件
     *
     * @param contextConfigLocation
     * @return void
     * @author hujy
     * @date 2019-09-16 11:31
     */
    private void doLoadConfig(String contextConfigLocation) {
        InputStream fis = null;
        try {
            fis = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
            //1、读取配置文件
            contextConfig.load(fis);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != fis) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

扫描相关的类

扫描配置的scanPackage路径下的全部文件,将文件名以[包名].[文件名]的形式保存到集合中

    /**
     * 扫描配置文件中指定的路径,将文件名以[包名].[文件名]的形式保存到集合中
     *
     * @param packageName
     * @return void
     * @author hujy
     * @date 2019-09-16 11:33
     */
    private void doScanner(String packageName) {
        //将所有的包路径转换为文件路径
        URL url = this.getClass().getClassLoader().getResource("/" + packageName.replaceAll("\\.", "/"));
        File dir = new File(url.getFile());
        for (File file : dir.listFiles()) {
            //如果是文件夹,继续递归
            if (file.isDirectory()) {
                doScanner(packageName + "." + file.getName());
            } else {
                classNames.add(packageName + "." + file.getName().replace(".class", "").trim());
            }
        }
    }

IOC容器初始化

初始化扫描到的类,并且将它们放入到IOC容器之中,其中对@MyController与@MyService修饰的类采用了不同的保存策略。

    /**
     * 对象初始化,保存到IOC容器
     *
     * @param
     * @return void
     * @author hujy
     * @date 2019-09-16 11:46
     */
    private void doInstance() {
        if (classNames.size() == 0) {
            return;
        }

        try {
            for (String className : classNames) {
                Class<?> clazz = Class.forName(className);
                if (clazz.isAnnotationPresent(MyController.class)) {
                    //默认将首字母小写作为beanName
                    String beanName = toLowerFirstCase(clazz.getSimpleName());
                    ioc.put(beanName, clazz.newInstance());
                } else if (clazz.isAnnotationPresent(MyService.class)) {

                    MyService service = clazz.getAnnotation(MyService.class);
                    String beanName = service.value();
                    //优先使用用户自定义的名字
                    if (!"".equals(beanName.trim())) {
                        ioc.put(beanName, clazz.newInstance());
                        continue;
                    }

                    //如果没有自定义名字,就将接口类型作为bean的名字
                    Class<?>[] interfaces = clazz.getInterfaces();
                    for (Class<?> i : interfaces) {
                        ioc.put(i.getName(), clazz.newInstance());
                    }
                } else {
                    continue;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 首字母转成小写
     *
     * @param str
     * @return java.lang.String
     * @author hujy
     * @date 2019-09-16 14:08
     */
    private String toLowerFirstCase(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

依赖注入

    /**
     * 依赖注入
     *
     * @param
     * @return void
     * @author hujy
     * @date 2019-09-16 14:13
     */
    private void doAutowired() {
        if (ioc.isEmpty()) {
            return;
        }

        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            //拿到实例对象中的所有属性
            Field[] fields = entry.getValue().getClass().getDeclaredFields();
            for (Field field : fields) {

                if (!field.isAnnotationPresent(MyAutowired.class)) {
                    continue;
                }

                // 拿到autowired的属性名
                MyAutowired autowired = field.getAnnotation(MyAutowired.class);
                String beanName = autowired.value().trim();
                if ("".equals(beanName)) {
                    beanName = field.getType().getName();
                }
                // 设置私有属性的访问权限
                field.setAccessible(true);
                try {
                    // 为属性注入实例,这里没有考虑循环依赖的复杂场景
                    field.set(entry.getValue(), ioc.get(beanName));
                } catch (Exception e) {
                    e.printStackTrace();
                    continue;
                }
            }
        }
    }

初始化HandlerMapping

这里扫描了被@MyController与@MyRequestMapping修饰的类与方法,将配置的url与方法的映射关系进行保存

    /**
     * 初始化url映射关系
     *
     * @param
     * @return void
     * @author hujy
     * @date 2019-09-16 14:41
     */
    private void initHandlerMapping() {
        if (ioc.isEmpty()) {
            return;
        }

        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();
            // 这里是对持有MyController的bean做处理
            if (!clazz.isAnnotationPresent(MyController.class)) {
                continue;
            }

            String url = "";
            // 获取Controller的url配置
            if (clazz.isAnnotationPresent(MyRequestMapping.class)) {
                MyRequestMapping requestMapping = clazz.getAnnotation(MyRequestMapping.class);
                url = requestMapping.value();
            }

            // 获取Method的url配置
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {

                // 没有加RequestMapping注解的直接忽略
                if (!method.isAnnotationPresent(MyRequestMapping.class)) {
                    continue;
                }

                // 映射URL
                MyRequestMapping requestMapping = method.getAnnotation(MyRequestMapping.class);
                String regex = ("/" + url + requestMapping.value()).replaceAll("/+", "/");
                Pattern pattern = Pattern.compile(regex);
                handlerMapping.add(new Handler(pattern, entry.getValue(), method));
                System.out.println("mapping " + regex + "," + method);
            }
        }
    }

以上是MVC容器的初始化逻辑,下面是具体的请求分发处理。

请求分发

该方法根据request对象中的url信息,完成了url与方法的映射匹配,已经参数的赋值匹配,最后通过反射调用了具体的业务实现。

   /**
     * 请求处理
     * @author hujy
     * @date 2019-09-16 15:11
     * @param req
     * @param resp
     * @return void
     */
    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        try {
            Handler handler = checkUrl(req);

            if (handler == null) {
                // 如果没有匹配上,返回404错误
                resp.getWriter().write("404 Not Found");
                return;
            }

            // 获取方法的参数列表
            Class<?>[] paramTypes = handler.method.getParameterTypes();

            // 保存所有需要自动赋值的参数值
            Object[] paramValues = new Object[paramTypes.length];

            Map<String, String[]> params = req.getParameterMap();
            for (Map.Entry<String, String[]> param : params.entrySet()) {
                String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",\\s", ",");

                //如果找到匹配的对象,则开始填充参数值
                if (!handler.paramIndexMapping.containsKey(param.getKey())) {
                    continue;
                }
                int index = handler.paramIndexMapping.get(param.getKey());
                paramValues[index] = convert(paramTypes[index], value);
            }

            // 设置方法中的request和response对象
            int reqIndex = handler.paramIndexMapping.get(HttpServletRequest.class.getName());
            paramValues[reqIndex] = req;
            int respIndex = handler.paramIndexMapping.get(HttpServletResponse.class.getName());
            paramValues[respIndex] = resp;

            handler.method.invoke(handler.controller, paramValues);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * URL校验
     * @author hujy
     * @date 2019-09-16 16:00
     * @param req
     * @return com.hujy.servlet.MyDispatcherServlet.Handler
     */
    private Handler checkUrl(HttpServletRequest req) throws Exception {
        if (handlerMapping.isEmpty()) {
            return null;
        }

        String url = req.getRequestURI();
        String contextPath = req.getContextPath();
        url = url.replace(contextPath, "").replaceAll("/+", "/");

        for (Handler handler : handlerMapping) {
            try {
                Matcher matcher = handler.pattern.matcher(url);
                // 如果没有匹配上继续下一个匹配
                if (!matcher.matches()) {
                    continue;
                }
                return handler;
            } catch (Exception e) {
                throw e;
            }
        }
        return null;
    }

    /**
     * 类型转换
     * url传过来的参数都是String类型的,HTTP是基于字符串协议,只需要把String转换为任意类型就好
     * @author hujy
     * @date 2019-09-16 16:24
     * @param type
     * @param value
     * @return java.lang.Object
     */
    private Object convert(Class<?> type, String value) {
        if (Integer.class == type) {
            return Integer.valueOf(value);
        }
        // 如果还有double等其他类型,可以利用策略模式继续扩展
        return value;
    }

Handler为自定义的内部类,记录了Controller中的RequestMapping和Method的对应关系

/**
  * 请求处理内部类
  * 记录Controller中的RequestMapping和Method的对应关系
  * @author hujy
  * @date 2019-09-16 14:50
  */
private class Handler {

        // 保存方法对应的实例
        private Object controller;
        // 保存映射的方法
        private Method method;
        // url匹配正则
        private Pattern pattern;
        // 保存参数顺序
        private Map<String, Integer> paramIndexMapping;

        private Handler(Pattern pattern, Object controller, Method method) {
            this.controller = controller;
            this.method = method;
            this.pattern = pattern;
            paramIndexMapping = new HashMap<>();
            // url传过来的参数都是String类型的,HTTP是基于字符串协议,只需要把String转换为任意类型就好
            putParamIndexMapping(method);
        }

        /**
         * 保存参数在方法中的位置映射关系
         * @author hujy
         * @date 2019-09-16 16:02
         * @param method
         * @return void
         */
        private void putParamIndexMapping(Method method) {
            //提取方法中加了注解的参数
            Annotation[][] pa = method.getParameterAnnotations();
            for (int i = 0; i < pa.length; i++) {
                for (Annotation a : pa[i]) {
                    if (a instanceof MyRequestParam) {
                        String paramName = ((MyRequestParam) a).value();
                        if (!"".equals(paramName.trim())) {
                            paramIndexMapping.put(paramName, i);
                        }
                    }
                }
            }

            //提取方法中的request和response参数
            Class<?>[] paramsTypes = method.getParameterTypes();
            for (int i = 0; i < paramsTypes.length; i++) {
                Class<?> type = paramsTypes[i];
                if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
                    paramIndexMapping.put(type.getName(), i);
                }
            }
        }
    }

自此便完成了简化版SpringMVC的实现逻辑。

验证

通过tomcat启动服务,输入:http://localhost:8080/demo/hello?name=hujy666

完整代码在:https://github.com/hjy0319/my-simple-mvc

 

以上内容为本人对《Spring5核心原理与30个类手写实战》部分内容的学习总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@从入门到入土

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

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

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

打赏作者

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

抵扣说明:

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

余额充值