实现一个简易的Spring MVC

申明:仅作为个人学习笔记,原文请访问: 手写Spring MVC

 

如果需要源码,请点击上面的链接。

 

总体思路:

1、Spring MVC是通过注册DispacherServlet,并配置url-pattern为/* 来接管所有的请求。同样的,如果我们需要实现Mvc框架,也需要在web.xml中注册一个自定义的servlet:

<servlet>
    <servlet-name>xmvc</servlet-name>
    <servlet-class>com.ph.xspring.servlet.XDispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <!--you can't use classpath*: -->
      <param-value>application.properties</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>xmvc</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

2、实现MVC的功能,还需要配置一些常用的注解,例如@RequestMapping, @Controller等等。下面列举一个自定义的注解

@Target({ElementType.TYPE}) //限定注解的使用位置
@Retention(RetentionPolicy.RUNTIME) //注解的保留政策,如果想要通过反射来获取注解信息,需要在内存中保留注解信息,只能用runtime
@Documented  //用于制作文档
public @interface XController {
    String value()default "";
}

3、接着要实现XDispacherServlet中的内容,在这个Servlet内读取配置文件、初始化参数、IOC容器初始化、依赖注入等等。这些列初始化的功能,都需要重写HttpServlet中的init方法,在init方法中实现。

4、初始化完成后,就需要接收并处理浏览器传过来的请求了,将service(GET,POST为例)方法中的请求交给doDispacher来处理,并返回结果。

 

下面来解析XDispacherServlet中的内容:

1、静态变量

public class XDispatcherServlet extends HttpServlet {
    /*
    属性配置文件
     */
    private Properties contextConfig = new Properties();
    private List<String> classNamelist = new ArrayList<>();

    /**
     * IOC容器
     */
    Map<String ,Object>iocMap = new HashMap<String,Object>();
    Map<String, Method>handlerMapping = new HashMap<String, Method>();

首先是上面几个变量,在下面的init方法中,需要对上述变量进行初始化。

contextConfig用于表示配置文件,例如applicationContext

classNameList用来存放用户指定包下的所有java类的字节码的名称,用于后面通过反射来获取类信息。

iocMap用来表示IOC容器,通过注解或者xml文件注入进来的对象(本文仅通过注解注入),会存放在此。

handlerMapping用来存放url与对应方法之间的映射关系,例如 在controller中 ”/login“  路径与 Login()方法之间是对应的,通过访问/login,即可调用Login()方法。

2、初始化

        //1.加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));

        //2.扫描相关的类
        doScanner(contextConfig.getProperty("scan-package"));

        //3.初始化IOC容器,将所有的相关类实例保存到IOC容器中
        doInstance();

        //4.依赖注入
        doAutowired();

        //5.初始化HandlerMapping
        initHandlerMapping();

初始化有上述五个步骤,下面会一 一说明。

2.1、加载配置文件

    private void doLoadConfig(String contextConfigLocation) {
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);

        try {
            //保存在内存
            contextConfig.load(inputStream);
            System.out.println("[INFO-1] property file has been saved in contextConfig.");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(null != inputStream){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

需要传入文件名,该文件放在resources目录下,我在该文件配置了所需要扫描的包(下面一个步骤用到),如下

scan-package=com.ph

2.2、扫描相关类

       private void doScanner(String scanPackage) {

        URL resourcePath = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));
        System.out.println("INFO-2   "+resourcePath);
        if(resourcePath == null){
            return;
        }
        File classPath = new File(resourcePath.getFile());
        for(File file: classPath.listFiles()){
            if(file.isDirectory()){
                System.out.println("[INFO-2] {" + file.getName() + "} is a directory.");
                //子目录递归
                doScanner(scanPackage + "." + file.getName());
            }else{
                if(!file.getName().endsWith(".class")){
                    System.out.println("[INFO-2] {\" + file.getName() + \"} is not a class file.");
                    continue;
                }
                String className = (scanPackage+"." +file.getName()).replace(".class","");
               
                classNamelist.add(className);
                System.out.println("[INFO-2] {" + className + "} has been saved in classNameList.");
            }
        }

    }

将指定包下所有字节码文件,添加到classNameList中。

2.3、初始化IOC容器

    private void doInstance() {
        if(classNamelist.isEmpty()){
            return;
        }
        try {
            for(String className: classNamelist){
                Class<?>clazz = Class.forName(className);

                if(clazz.isAnnotationPresent(XController.class)){
                    String beanName = toLowerFirstCase(clazz.getSimpleName());
                    Object instance = clazz.newInstance();

                    //保存在ioc容器
                    iocMap.put(beanName, instance);
                    System.out.println("[INFO-3] {" + beanName + "} has been saved in iocMap.");

                }else if(clazz.isAnnotationPresent(XService.class)){
                    String beanName = toLowerFirstCase(clazz.getSimpleName());

                    //如果注解包含自定义名称
                    XService xService = clazz.getAnnotation(XService.class);
                    if("".equals(xService.value())){
                        beanName = xService.value();
                    }

                    Object instance = clazz.newInstance();
                    iocMap.put(beanName, instance);
                    System.out.println("[INFO-3] {" + beanName + "} has been saved in iocMap.");

                    //找类的接口(把该类所依赖的接口全都注入)
                    for(Class<?> i :clazz.getInterfaces()){
                        if(iocMap.containsKey(i.getName())){
                            throw new Exception("The Bean Name is Exist.");
                        }
                        iocMap.put(i.getName(), instance);
                        System.out.println("[INFO-3] {" + i.getName() + "} has been saved in iocMap.");
                    }
                }
            }
        }catch(Exception e){

        }
    }

这里的注入都是通过添加注解来实现的,那么需要判断哪些类被添加了注解(如@Service,@Controller等),如果有这些注解,则需要将这个类注入到容器中。对应的就是在iocMap中添加这个类就行了。如果在注解中指定了value,那么可以将这个value值作为bean的名称,否则就取这个类的类名(将类名的首字母替换为小写)。

2.4、依赖注入

 private void doAutowired() {
        if(iocMap.isEmpty())
            return;
        for(Map.Entry<String, Object>entry : iocMap.entrySet()){
            //获取每一个class里的字段
            Field[]fields = entry.getValue().getClass().getDeclaredFields();

            for(Field field: fields){
                if(!field.isAnnotationPresent(XAutowired.class)){
                    continue;
                }
                //如果该字段上有注解 @Autowired
                System.out.println("[INFO-4] Existence XAutowired.");

                //获取注解对应的类
                XAutowired xAutowired = field.getAnnotation(XAutowired.class);
                String beanName = xAutowired.value().trim();

                //获取XAutowired注解的值
                if("".equals(beanName)){
                    System.out.println("[INFO] xAutowired.value() is null");
                    //如果注解没有值,则取该字段的字段名
                    beanName = field.getType().getName();
                }

                //只要加了注解,都需要加载,不管是private 还是protect
                field.setAccessible(true);

                try{
                    //entry.getValue()获取的是类对象, icoMap.get(beanName)获取的是需要被注入的类的对象
                    //理解起来就是,给controller中被注入的那个字段赋值。
                    field.set(entry.getValue(), iocMap.get(beanName));
                    System.out.println("[INFO-4] field set {" + entry.getValue() + "} - {" + iocMap.get(beanName) + "}.");
                }catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

    }

所谓依赖注入,在这里就是指的是解析@Autowired注解。@Autowired标注在变量上,我们需要从IOC容器中,取出对应的实例,赋给这个变量。

例如:

    @XAutowired
    XTestService testService;

需要再在iocMap容器中,找到XTestService的实例,将其赋给testService这个变量,那么在controller中,就可以调用testService这个服务。

2.5、初始化url与方法的映射

    //5、初始化handlerMapping
    private void initHandlerMapping() {
        if(iocMap.isEmpty()){
            return;
        }
        for(Map.Entry<String, Object> entry : iocMap.entrySet()){
            Class<?>clazz = entry.getValue().getClass();
            //必须要有controller标签
            if(!clazz.isAnnotationPresent(XController.class)){
                continue;
            }
            String baseUrl = "";
            if(clazz.isAnnotationPresent(XRequestMapping.class)){
                XRequestMapping xRequestMapping = clazz.getAnnotation(XRequestMapping.class);
                baseUrl = xRequestMapping.value();
            }

            for(Method method: clazz.getMethods()){
                if(!method.isAnnotationPresent(XRequestMapping.class)){
                    continue;
                }
                XRequestMapping xRequestMapping = method.getAnnotation(XRequestMapping.class);
                String url = ("/" + baseUrl + "/" + xRequestMapping.value()).replaceAll("/+","/");
                handlerMapping.put(url, method);
                System.out.println("[INFO-5] handlerMapping put {" + url + "} - {" + method + "}.");
            }
        }
    }

将@XRequestMapping注解中的value值与所在的方法对应起来,当有请求来的时候,可以通过请求的url来调用相应的方法。

3、处理请求

初始化之后,就可以处理浏览器发来的请求了。

@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //7、运行阶段
        try {
            doDispach(req, resp);
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("500 Exception Detail:\n" + Arrays.toString(e.getStackTrace()));
        }
    }
    private void doDispach(HttpServletRequest request, HttpServletResponse response) throws InvocationTargetException, IllegalAccessException {
        String url = request.getRequestURI();
        String contextPath = request.getContextPath();

        url = url.replaceAll(contextPath, "").replaceAll("/+","/");
        System.out.println("[INFO]request url ----> "+url);
        if(!(this.handlerMapping.containsKey(url))){
            try {
                response.getWriter().write("404 NOT FOUND");
                return;
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }

        //获取url对应的方法
        Method method = this.handlerMapping.get(url);
        System.out.println("[INFO]method ----> "+method);

        String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
        System.out.println("[INFO]iocMap.get(beanName)->" + iocMap.get(beanName));

        //第一个参数是获取方法,后面的参数,多个参数直接加,按顺序对应
        method.invoke(iocMap.get(beanName),request, response);
        System.out.println("[INFO] method.invoke put {" + iocMap.get(beanName) + "}.");
    }

通过url,来调用相应的方法,传入 request和response对象。

4、编写测试类

@XController
@XRequestMapping("/test")
public class TestController {

    @XAutowired
    XTestService testService;

    @XRequestMapping("/getUserInfo")
    public void  getUserInfo(HttpServletRequest request, HttpServletResponse response){
        String userInfo = testService.getUserInfo();
        try {
            response.getWriter().write(userInfo);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

 

@XService
public class XTestServiceImpl implements XTestService {
    @Override
    public String getUserInfo() {
        return "admin:123";
    }
}

5、配置tomcat并启动,浏览器中访问:

http://localhost:8080/test/getUserInfo
posted @ 2020-07-03 20:06  小楼夜听雨QAQ  阅读(232)  评论(0)    收藏  举报