【JAVA实战】编写迷你spring mvc

9 篇文章 0 订阅
1 篇文章 0 订阅

我用spring系列框架进行开发也有两三年的时间了,但是仅止于使用,偶尔会聊些一些细节的原理什么的,但是对于spring其实还停留在一知半解的地步上_(:зゝ∠)_。这次找到了一个介绍编写spring mvc的小视频,正好从头开始重新系统的了解spring的运行原理。

1. 运行原理

spring的代码运行分为三个阶段:配置阶段,初始化阶段和运行阶段(如下图)。

1. 配置阶段:先在web.xml里配置dispatchServlet类的路径,并顺便配置了配置文件的路径(application.xml或application.properties),和url-pattern。

2. 初始化阶段:在这个阶段里代码开始对spring容器进行初始化。

  1. 加载配置文件(配置文件里配置了需要扫描的包名)
  2. 根据配置文件的配置扫描包里的类,并初始化ioc容器。
  3. 对ioc容器里的类进行实例化
  4. 对ioc容器里的类进行依赖注入。
  5. 初始化url和method的映射handlerMapping。

3. 运行阶段

  1. doGet()/doPost(), 被请求
  2. 根据url在handlerMapping中找到对应的method
  3. method.invoke(),方法调用
  4. 返回。

具体的三个步骤就是执行了上面说的过程,下面我会跟着代码一步一步的来解说一下。

2. 具体代码

2.1 配置阶段

首先我们知道我们需要在web.xml中要配置DispatchServlet的路径,并且配置文件也要在web.xml里标明(当然配置文件里要有需要扫描的包名),还有url的跟路径,代码如下:

web.xml

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" 
         id="WebApp_ID" version="3.0">
	<display-name>Mrh Web Application</display-name>
	
 	<servlet>
	  	<servlet-name>mrhmvc</servlet-name>
	  	<servlet-class>com.mrh.spring.handwrite.mvcframework.servlet.MRHDispatchServlet</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>mrhmvc</servlet-name>
  		<url-pattern>/*</url-pattern>
  	</servlet-mapping>
</web-app>

application.properties:

scanPackage=com.mrh.spring.handwrite.demo

 scanPackage就是配置了需要被扫描的包名。

2.2 初始化阶段

DispatchServlet继承自HttpServlet,我们需要重写doGet(),doPost()和init()方法。

在初始化阶段首先需要完成的是init(), 其实也不是首先了,毕竟整个初始化的流程我们都准备在init方法中完成_(:зゝ∠)_。

@Override
    public void init(ServletConfig config) throws ServletException {
        
        // 1.加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
        
        // 2.扫描并加载相关类
        doScanner(contextConfig.getProperty("scanPackage"));
        
        // 3.初始化IOC容器
        doInstance();
        
        // 4.反射依赖注入
        doAutowired();
        
        // 5.构建handlerMapping, 将url和method建立一对一关系
        doInitHandlerMapping();

        System.out.println("MRH spring MVC is init.");
    }

新建了5个方法,用于完成我们上面列举的五个步骤。

a. 加载配置文件

ServletConfig实际上就是读取web.xml里的配置的一个类, 之前配置的时候我们已经看到我们实际上在web.xml里配置了application.properties的地址,而我们现在要做的就是先找到application.properties文件,并把里面配置的属性加载出来。

    private void doLoadConfig(String contextConfigLocation) {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try {
            contextConfig.load(is);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

b. 扫描并加载相关类

实际上我们在application.properties文件里也只是配了需要扫描的包名,在这一步中我们就要根据配置的包名扫描出所有需要加载的文件,但是需要注意的是,我们只加载.class文件。因为文件结构可能是多层的,所以我们用递归的方式将所有子目录下的文件都加载进来:首先第一步是把包名转换成相对路径,然后取得路径下的文件列表,判断当前文件是否为文件夹,如果是则将这个文件夹的名字加到路径里,递归调用自己的方法,如果不是则加载这个文件。

说的加载这个文件,只是把这个类的类名存放到我们的类名列表里。

private void doScanner(String scanPackage) {
        URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
        File classDir = new File(url.getFile());
        for(File file : classDir.listFiles()) {
            if(file.isDirectory()) {
                doScanner(scanPackage + "." + file.getName());
            } else {
                if(!file.getName().endsWith(".class")) {
                    continue;
                }
                String className = scanPackage + "." + file.getName().replace(".class", "");
                classNames.add(className);
            }
        }
    }

c. 初始化ioc容器

根据类名列表中的类名,反射加载类,并生成实例放入ioc容器中(在mini代码中,ioc容器就是一个以类名为key,实例为value的hashmap)。

如果类名列表为空,则自动返回。否则遍历类名,并根据类名获取类描述对象,需要注意的是我们只需要加载有注解的类,没有注解的类我们概不加载(注解也是自己写的_(:зゝ∠)_,下一块会放出代码)。而且对于不同注解,我们要用不同的方法处理,controller因为基本上都是不是接口和实现的形式,所以我们对于注解了@Controller的类,只需要对类名的首字母改成小写作为key,生成实体对象为value,存入ioc的map就可以。但是因为@Service注解的类经常有可能是接口的实现类,而且经常会自定义service的名字,所以我们还需要看是否有另外定义名字,如果有另外定义,就用自定义的,如果没有就将类名的首字母改成小写。而且对于service对象,还要用实例对所有实现的接口进行初始化,key就是接口的全类名。

private void doInstance() {
        if(classNames.isEmpty()) {
            return;
        }
        try {
            for(String className : classNames) {
                Class<?> clazz = Class.forName(className);
                
                if(clazz.isAnnotationPresent(MRHController.class)) {
                    Object instance = clazz.newInstance();
                    String beanName = lowerFirstCase(clazz.getSimpleName());
                    ioc.put(beanName, instance);
                } else if(clazz.isAnnotationPresent(MRHService.class)) {
                    // Service初始化的并不是类的本身,如果是接口的话,而是类对应的实现类
                    // 1. 如果service不是接口,默认就是类名的首字母小写作为key
                    MRHService service = clazz.getAnnotation(MRHService.class);
                    String beanName = service.value();
                    
                    // 2. 如果用户自定义了beanName,那么要优先使用beanName进行key-value的组合
                    if("".equals(beanName)) {
                        beanName = lowerFirstCase(clazz.getSimpleName());
                    }
                    Object instance = clazz.newInstance();
                    ioc.put(beanName, instance);
                    
                    // 3. 赋值的对象是接口的话,那么要采用一种投机取巧的方式,用接口的全类名做为key,实现的实例作为值,方便依赖注入时使用
                    Class<?>[] interfaces = clazz.getInterfaces();
                    for(Class<?> i : interfaces) {
                        if(ioc.containsKey(i.getName())) {
                            throw new Exception("The " + clazz.getName() + " for " + beanName + " exists!!");
                        }
                        ioc.put(i.getName(), instance);
                    }
                    
                } else {
                    continue;
                }
                
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

对了需要补充一下,将类名首字母小写的私有方法:

 private String lowerFirstCase(String simpleName) {
        char[] chars = simpleName.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

d. 反射依赖注入

我们已经把被注解的类的初始化到容器中,但是类中还注解了@Autowired的一些对象,我们在初始化的时候就进行依赖注入。

如果ioc容器此时为空容器,我们就不需要做任何操作。

ioc容器不为空的情况下,我们需要遍历容器中存储的类,取出每一个类的field列表,遍历field是否注解了@Autowired,如果有就需要注入实例(从ioc中取出对应的实例,注入)。需要说明的是:如果字段是private 或者protected 或者是default,这个字段是不可见的,就要强制将它置为可见。

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(MRHAutowired.class)) {
                    continue;
                }
                MRHAutowired autowired = field.getAnnotation(MRHAutowired.class);
                String beanName = autowired.value();
                if("".equals(beanName)) {
                    beanName = field.getType().getName();
                }
                
                // 如果字段是private 或者protected 或者是default,这个字段是不可见的
                field.setAccessible(true); // 不管你是否是外部可见,都强制设为可见,暴力访问
                
                try {
                    field.set(entry.getValue(), ioc.get(beanName));
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        
    }

f.构建handlerMapping

构建handlerMapping主要是建立url和method的一一对应关系,这样请求就能找到对应的方法。

首先我们先要找出所有的controller,先拿出注解在类上的@HandlerMapping注解,取得这个controller的基础url,然后再获取每个方法上注解的@HandlerMapping的值与基础url拼接成这个method对应的url pattern(转成Pattern是因为url的匹配是支持正则表达式的),用controller的类信息和method的信息与url构建一个Handler对象,并把需要的参数和参数的顺序也存入其中。最后HandlerMapping就是一个handler对象的列表。

private void doInitHandlerMapping() {
        if(ioc.isEmpty()) {
            return;
        }
        
        for(Map.Entry<String, Object> entry : ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();
            if(!clazz.isAnnotationPresent(MRHController.class)) {
                continue;
            }
            
            String baseUrl = "";
            if(clazz.isAnnotationPresent(MRHRequestMapping.class)) {
                MRHRequestMapping mrhRequestMapping = clazz.getAnnotation(MRHRequestMapping.class);
                baseUrl = mrhRequestMapping.value();
            }
            
            Method [] methods = clazz.getMethods();
            for(Method method : methods) {
                if(!method.isAnnotationPresent(MRHRequestMapping.class)) {
                    continue;
                }
                MRHRequestMapping mrhRequestMapping = method.getAnnotation(MRHRequestMapping.class);
                String url = ("/" + baseUrl + "/" + mrhRequestMapping.value()).replaceAll("/+", "/");
                handlerMapping.add(new Handler(Pattern.compile(url), clazz, method));
                System.out.println("Mapped : " + url + "," + method);
            }
        }
        
        
    }

    private class Handler {
        protected Object controller;
        protected Method method;
        protected Pattern pattern;
        protected Map<String, Integer> paramIndexMapping;
        
        protected Handler(Pattern pattern, Object controller, Method method) {
            this.controller = controller;
            this.method = method;
            this.pattern = pattern;
            
            paramIndexMapping = new HashMap<String, Integer>();
            paramIndexMapping(method);
        }
        
        private void paramIndexMapping(Method method) {
            Annotation [][] pa = method.getParameterAnnotations();
            for(int i = 0; i<pa.length; i++) {
                for (Annotation a : pa[i]) {
                    if(a instanceof MRHRequestParam) {
                        String paramName = ((MRHRequestParam)a).value();
                        if(!"".equals(paramName.trim())) {
                            paramIndexMapping.put(paramName, i);
                        }
                    }
                }
            }
            
            // 提取方法中的request和response参数
            Class<?>[] paramTypes = method.getParameterTypes();
            for(int i=0; i<paramTypes.length; i++) {
                Class<?> type = paramTypes[i];
                if(type == HttpServletRequest.class || type == HttpServletResponse.class) {
                    paramIndexMapping.put(type.getName(), i);
                }
            }
        }
    }

到这一步, mini spring的初始化就算完成了。

3. 注解

根据上面的初始化过程,我们可以看出,spring的运行很大程度上是靠了各种注解。而我们在平常的开发过程中,也常用各种注解,对这些注解可以说已经很熟悉了,如@Controller,@Service, @Autowired,@RequestMapping,@RequestParam。因为是手写迷你spring框架,所以注解我们也是简单的自己写了一下。

@MRHController

package com.mrh.spring.handwrite.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

@MRHService

package com.mrh.spring.handwrite.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

@MRHAutowired

package com.mrh.spring.handwrite.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

@MRHRequestMapping

package com.mrh.spring.handwrite.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

@MRHRequestParam

package com.mrh.spring.handwrite.mvcframework.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

中间需要注意的是,@Target是java的元注解(也就是用于注解的一种注解),它是用来配置这个新注解可以注解的对象的,比如@MRHController就只能注解类,@MRHRequestMapping就既能注解类又能注解方法,@MRHRequestParam注解参数,@MRHAutowired就只能注解变量。详细列一下可以配置的值,和对应的意思:

1.CONSTRUCTOR:用于描述构造器
2.FIELD:用于描述域
3.LOCAL_VARIABLE:用于描述局部变量
4.METHOD:用于描述方法
5.PACKAGE:用于描述包
6.PARAMETER:用于描述参数
7.TYPE:用于描述类、接口(包括注解类型) 或enum声明

而@Retention也是java的元注解,用来配置这个注解被保留的时间长短,也就是生命期,RetentionPolicy.RUNTIME表明是运行时生效:

1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在class文件中有效(即class保留)
3.RUNTIME:在运行时有效(即运行时保留)

4. 运行阶段

插播了注解的内容,我们的整个容器初始化阶段已经完成了,接下来进进入到运行阶段需要的代码了。在运行阶段,我们会通过url发起请求,spring会找到对应的方法执行后返回返回值后返回给前台。

在初始化DispatchServlet的时候,我们除了初始化了init()方法,还初始化了doGet和doPost两个方法,因为我们只是简单写功能,所以我们就让doGet和doPost走同一套逻辑。

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

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            doDispatch(req, resp);
        } catch(Exception e) {
            resp.getWriter().write("500 Exception, Detail:" + Arrays.toString(e.getStackTrace()));
        }
    }

其实最后的执行逻辑还是写在doDispatch()方法里,在doPost()方法中统一处理了一下异常。

然后doDispatch()方法中的逻辑是这样的:先根据url遍历handler找到匹配的handler, 没找到就报404异常。然后从handler中取出method,和对应的参数类型,根据参数类型数组,配置一个对应的值数组(当然目前这个是空的,准备从request里取出来放进去的)。然后从request里取出参数map,进行转换后放到对应的数组index里去。最后再把request和response也放入参数数组里,invoke方法。

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        try {
            Handler handler = getHandler(req);
            
            if(handler == null) {
                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(Entry<String, String[]> param : params.entrySet()) {
                String value = Arrays.deepToString(param.getValue()).replaceAll("\\[|\\]", "");
                
                // 如果找到匹配的对象,则开始填充参数值
                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;
            
            String beanName = lowerFirstCase(handler.method.getDeclaringClass().getSimpleName());
            
            handler.method.invoke(ioc.get(beanName), paramValues);
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }
    
    public Object convert(Class<?> type, String value) {
        if(type.equals(Integer.class)) {
            return Integer.valueOf(value);
        }
        
        if(type.equals(Double.class)) {
            return Double.valueOf(value);
        }
        
        if(type.equals(Float.class)) {
            return Float.class;
        }
        
        if(type.equals(Date.class)) {
            return Date.valueOf(value);
        }
        
        if(type.equals(Long.class)) {
            return Long.valueOf(value);
        }
        
        if(type.equals(String.class)) {
            return value;
        }
        Object valueObj = (Object)value;
        JSONObject jsonObject = JSONObject.fromObject(valueObj);
        
        return JSONObject.toBean(jsonObject, type);
    }

    private Handler getHandler(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) {
            if(handler.pattern.matcher(url).matches()) {
                return handler;
            }
        }
        return null;
    }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值