SpringMVC的原理分析以及手写SpringMVC框架

在手些SpringMVC之前,先明确SpringMVC的内容有那些:

  • DispatcherServlet的初始化
  • 请求
    在SpringMVC里面扫描是在xml文件中配置了包的地址
    在这里使用properties代替xml
    前端访问后端的过程:
    首先解析web.xml文件,找到前端控制器(DispatcherServlet,前端控制器得到了前端的url地址,并通过HandlerMapping分配到不同的控制器(Controller),控制器处理请求并返回ModelAndView(包括模型和视图)给前端控制器,前端控制器将渲染结果发送到前端;前后端完成一次交互:

在这里插入图片描述

在实现SpringMVC的过程中,需要实例化业务类(如Controller, Service ),此处通过注解进行实例化
所以需要创建一些注解:
首先:
实例化对象的注解有:
@Controller
@Service
自动装配置的注解有
@AutoWired
为方法配置访问名的注解:
@ResultMapping
获取参数名的注解:
@Param:

  1. @Controller是注解在类上面的,被他注解的类是一个总控制类,它在运行时起作用;如何实现它的作用呢?扫描所有的类,如果发现这个类上有这个注解,就为这个类创建对象,并且把这个对象放入IOC容器中
@Target(value={ElementType.TYPE}) // 只能作用在类上
@Retention(RetentionPolicy.RUNTIME) // 运行时起作用
@Documented // 生成文档
public @interface Controller {
}
  1. @Service和@Controller作用都是实例化对象,只是标记不同作用的类而已, 是注解在类上面的,被他注解的类是一个服务类,它在运行时起作用。实现它的作用的方法和@Controller一样,即扫描所有的类,如果发现这个类上有这个注解,就为这个类创建对象,并且把这个对象放入IOC容器中
@Target(value={ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
}
  1. @AutoWired自动装配置,只能注解在属性上,这个属性必须是对象,不能是基本数据类型,它的作用是为这个属性赋值。即在Ioc容器里拿相应的对象为其赋值。IOC容器是通过一个Map集合进行存放对象的;此处为简化,为实现这个注解的作用,可以通过以下方法来实现,即当扫描到一个属性有这个注解时,通过这个属性的名字,在IOC的Map集合中获取值,这样就得到 了这个属性的对象,并对其赋值。
@Target(value= {ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoWired {
}
  1. @ResultMapping这个注解既可以注释在类上也可以注解在方法方法上,这个注解中存放了这个方法或类的地址名,可以通过这个注解的值拼接一个url,并把这个方法作为值,url作为键存放在一个集合中,有了这个集合,所有的请求都可以通过地址来访问对应的方法,这个方法再处理这个请求,并作出一些相应。从而就完成了请求响应的实现;
@Target(value= {ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
     String value(); // 存放方法或类的地址名
}
  1. @Param,这个注解只能注解在参数上。这个注解的值是参数的名称;在进行请求的过程中,前端会传来一些参数,那么后端就需要接收这些参数,我们可以通过参数名来获取参数的值,如何获取参数的值,后面会进行详解;这就要求方法的参数必须要加这个注解,不然无法接收参数
@Target(value={ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {
     String value(); // 存放参数的名称
}

以上准备做好了,下面开始手写SpringMVC;以下分两步进行手写:

  1. IOC容器的实现;
  2. 请求响应数据的实现;

先写第一步:IOC容器的实现

写IOC容器之前,先明白三个问题

  1. 什么是IOC容器?
    IOC容器就是使用IoC/DI容器反过来控制应用程序所需要的外部资源,是程序开发思想。
  2. 为什么要写IOC容器?
    IOC容器的底层是一个Map集合,集合中的键是对象名,值是一个对象,这个对象的属性有对象名对应的对象以及对象的类型。在请求之前就已经把所有配置了的对象创建好了,要用的话直接从里面拿就行了。这个类型是表示这个对象是@Controller注解的对象,还是@Service注解的对象。区分好了@Controller,就能得到处理前端请求的对象,从而可以通过这个对象处理请求。
  3. 怎么写IOC容器?
    接下来,我们来详细地看看实现IOC容器的过程
    前端访问后端,首先要解析Web.xml文件,所以Web.xml文件中必须配置好servlet的信息,以及包名的信息。这个包名是存放业务类的包,后期可以通过包名得到所有的业务类,并对这些类进行筛选;
    所以我们先配置这个web.xml文件
<?xml version="1.0" encoding="UTF-8"?>

<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>MySpringmvc</display-name>

  <welcome-file-list>

    <welcome-file>index.html</welcome-file>

    <welcome-file>index.htm</welcome-file>

    <welcome-file>index.jsp</welcome-file>

    <welcome-file>default.html</welcome-file>

    <welcome-file>default.htm</welcome-file>

    <welcome-file>default.jsp</welcome-file>

  </welcome-file-list>

  <!-- 配置前端控制器开始 -->

  <servlet>
     <servlet-name>springmvc</servlet-name>
     <servlet-class>com.young.spring.core.DispatcherServlet</servlet-class>
     <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>source.properties</param-value>
     </init-param>
     <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
     <servlet-name><u>springmvc</u></servlet-name>
     <url-pattern>*.action</url-pattern>  
  </servlet-mapping>
   <!-- 配置前端控制器结束 -->
</web-app>

标签中的标签中的内容是配置文件名,通过解析文件获取这个文件中的内容,这个配置文件中的内容是业务类所在的包。创建配置文件:source.properties

basePackage=com.young.business

标签中的内容是前端控制器的完全限定名,前端控制器是Spring的核心类;它的作用是解析配置文件,得到配置文件中的包的信息,再扫描这个包,得到所有的类,把这些加了@Controller或@Servlet注解的类全部实力化,并放在一个Map集合中,从而实现了IOC容器;
当然实现了IOC容器还有很多其他的功能,获得了IOC容器,就要创建HandlerMapping对象,即访问控制器的花名册,这个对象是一个Map集合,可通过url地址获得值,这个值是url访问的目标方法。以上功能是这一步需要实现的功能;
首先我们来实现这一步需要实现的功能
作为一个Servlet,DispatcherServlet必须先继承HttpServlet类;
请求发送过来后,首先访问的是它的init方法,所以先重写HttpServlet类的init方法;

@Override
    private static final String BASE_PACKAGE = "basePackage";

     public void init(ServletConfig config) throws ServletException {
          // 1. 扫描并读取配置文件
          doFile(config.getInitParameter(CONTEXT_CONFIGLOCATION));
          // 2. 扫描用户设定的包下面的所有的类
          doScanner(prop.getProperty(BASE_PACKAGE));
          // 3. 根据className去实例化
          try {
             doInstance();
          } catch (Exception e) {
              e.printStackTrace();
          } 
          // 4. 自动装配
          try {
              doAutoWired();
          } catch (Exception e) {
              e.printStackTrace();
          }
          // 5. 初始化HandlerMapping,解析controller里面的@RequestMapping注解
          initHandlerMapping();
     }

init方法需要扫描并读取配置文件,这个配置文件存放的是业务类所在的包,可通过web.xml中获取,通过标签中的值获取,获取方式:下面写init方法的,扫描并读取配置文件的方法 doFile(config.getInitParameter(CONTEXT_CONFIGLOCATION));即解析source.properties,并获取到他的内容,放入Properties集合中,properties集合中放的就是业务类所在的包名,通过包名可以获取到它里面所有的类

// 全局定义<param-name>标签中的值
private static final String CONTEXT_CONFIGLOCATION = "contextConfigLocation";
// 使用properties存放配置文件的内容
private Properties prop = new Properties();
private void doFile(String configLocation) {

          InputStream in = DispatcherServlet.class.getClassLoader().getResourceAsStream(configLocation);
          try {
              prop.load(in);
          } catch (IOException e) {
              e.printStackTrace();
          }
     }

业务类所在的包存在properties中了,接下来我们要找到这个包下所有的类,创建一个List集合

  1. 首先获取文件中的内容
  2. 格式化这个包,把它变成地址
  3. 通过这个地址创建File对象;
  4. 扫描File,遇到文件,就将它的完全限定名放入集合中,
  5. 遇到文件夹就进入这个文件,重复步骤4

// 存放完全限定名的集合
private List<String> paths = new ArrayList<>();
     private void doScanner(String basePackage) {
          String newPath = basePackage.replaceAll("\\.", "/");
          // resource 的值为file:/F:/WH_JAVA_190328/MySQL/apache-tomcat-8.5.41/webapps/MySpringmvc/WEB-INF/classes/com/young/business/
          URL resource =DispatcherServlet.class.getClassLoader().getResource(newPath);
          String filePath = resource.getFile();
          File file = new File(filePath);
          if(file.exists()){
              File[] listFiles = file.listFiles();
              for (File file2 : listFiles) {
                   if(file2.isDirectory()) {
                        doScanner(basePackage + "." + file2.getName());
                   }else {
                        // clazz是类名
                        String clazz = file2.getName().replace(".class", "");
                        // 包名+类名就组成了这个类的完全限定名
                        paths.add(basePackage +"." + clazz);
                  }    
              }
          }
     }

获取到所有的类完全限定名后,我们要扫描里面所有的被@Controller和@Service注释的类,并为他们创建对象,并把这些对象以及对象的注解类型放入一个HandlerObject对象中,再以对象名为键,HandlerObject对象为值放入Map集合中,一个IOC容器就形成了

  1. 创建一个HandlerObject类


public class HandlerObject {

     private Object instance;
     
     private String instanceType;

     public HandlerObject() {
          super();
          // TODO Auto-generated constructor stub
     }

     public HandlerObject(Object instance, String instanceType) {
          super();
          this.instance = instance;
          this.instanceType = instanceType;
     }

     public Object getInstance() {
          return instance;
     }
     public void setInstance(Object instance) {
          this.instance = instance;
     }
     public String getInstanceType() {
          return instanceType;
     }

     public void setInstanceType(String instanceType) {
          this.instanceType = instanceType;
     }

     @Override
     public String toString() {
          return "HandlerObject [instance=" + instance + ", instanceType=" + instanceType + "]";
     }
}
  1. 先判断这个存放完全限定名的集合是否为空
  2. 如果不为空则执行如下操作
    1. 通过反射实例化这个注解
    2. 判断这个类的注解是否为@Controller或者@Service,如果是则创建对象,处理这个队形的完全限定名成一个对象名,并以对象名为键,对象为值存在集合中
  3. 如果为空则不进行任何操作


private Map<String,HandlerObject> ioc = new HashMap<>();

     private void doInstance() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
          if(!paths.isEmpty()) {
              for (String className : paths) {
                   Class<?> clazz = Class.forName(className);
                   if(clazz.isAnnotationPresent(Controller.class)) {
                        Object newInstance = clazz.newInstance();
                        // 获取这个对象的类名
                        String simpleName = newInstance.getClass().getSimpleName();
                        // 将这个类的类名进行首字母小写处理成对象名
                        ioc.put(toLowerFirstWord(simpleName), new HandlerObject(newInstance, BeanType.CONTROLLER.name()));
                   }else if(clazz.isAnnotationPresent(Service.class)) {
                        Object newInstance = clazz.newInstance();
                        String simpleName = newInstance.getClass().getSimpleName();
                        ioc.put(toLowerFirstWord(simpleName), new HandlerObject(newInstance, BeanType.CONTROLLER.name()));

                   }
              }
          }
     }
     
     /**
	 * 把字符串的首字母小写
	 * 
	 * @param name
	 * @return
	 */
	private String toLowerFirstWord(String name) {
		char[] charArray = name.toCharArray();
		charArray[0] += 32;
		return String.valueOf(charArray);
	}

创建完对象后,扫描这些对象,找出所有的被@AutoWired注释的对象,并为其赋值

  1. 判断IOC容器是否为空
    2.如果不为空则进行如下操作
    1. 扫描IOC容器,找到里面的所有对象
    2. 判断对象的属性中是否有@AutoWired,如果有则通过属性名从Ioc容器拿对象,给自己赋值
  2. 如果为空则啥也不做
     private void doAutoWired() throws InstantiationException, IllegalAccessException {
          if(!ioc.isEmpty()) {
              for (HandlerObject handler : ioc.values()) {
                   Object instance = handler.getInstance();
                   // 获取对象中的所有属性
                   Field[] fields = instance.getClass().getDeclaredFields();
                   if(null != fields && fields.length > 0) {
                         for (Field field : fields) {
                            // 判断属性上是否含有@AutoWired,
                             if(field.isAnnotationPresent(AutoWired.class)) {
                                // 如果有,则获取属性名
                                  String name = field.getName();
                                  // 判断IOC容器中是否有这个对象名对应的对象
                                  if(ioc.containsKey(name)) {
                                      // 有,则给自己赋值
                                      field.setAccessible(true);
                                      field.set(instance, ioc.get(name).getInstance());
                                  }else {
                                        // 没有,则打印异常
                                      System.err.println("IOC容器里面没有"+name+"对应的象");
                                  }
                             }
                        }
                   }
             }
          }
     }

创建HandlerMapping对象,将url值为键,HandlerMapping对象为值存入一个Map集合中,这个集合即花名册,往后可以通过url获取HandlerMapping对象,实现对目标方法的调用和赋值。以下我们来创建这个花名册:

  1. 应为要调用对应的方法,所有HandlerMapping对象应有一个Object型的属性以及一个Method属性,便于调用方法。创建HandlerMapping类
public class HandlerMapping {
	private Object instance;
	private Method method;
	
	public HandlerMapping() {
		super();
		// TODO Auto-generated constructor stub
	}
	public HandlerMapping(Object instance, Method method) {
		super();
		this.instance = instance;
		this.method = method;
	}
	public Object getInstance() {
		return instance;
	}
	public void setInstance(Object instance) {
		this.instance = instance;
	}
	public Method getMethod() {
		return method;
	}
	public void setMethod(Method method) {
		this.method = method;
	}
	
	@Override
	public String toString() {
		return "HandlerMapping [instance=" + instance + ", method=" + method + "]";
	}
}
  1. 扫描IOC容器,获取里面的所有的被@Controller注释的类,
  2. 每次扫描到@Controller注释的类时,判断这个类是否有@RequestMapping注释,如果有则获取它的值,拼接到path变量上
  3. 扫描这个类的所有被@RequestMapping注释的方法,获取它的值并拼接到path变量上,得到了访问这个方法的路径path,将这个方法和他的实例放入一个HandlerMapping对象中,并以path为键,HandlerMapping对象为值放入Map集合中

private Map<String,HandlerMapping> handlerMapping = new HashMap<>();
     private void initHandlerMapping() {
          if(!ioc.isEmpty()) {
              for (HandlerObject handler : ioc.values()) {
                   Object instance = handler.getInstance();
                   String path = "";
                   // 判断类上是否有@RequestMapping注解,有则获取它的值
                   if(instance.getClass().isAnnotationPresent(RequestMapping.class)) {
                        RequestMapping declaredAnnotation = instance.getClass().getDeclaredAnnotation(RequestMapping.class);
                        path +=  "/" + declaredAnnotation.value();
                   }
                   //  获取这个类上的所有方法
                   Method[] declaredMethods =instance.getClass().getDeclaredMethods();
                   if(null != declaredMethods && declaredMethods.length > 0) {
                        
                        for (Method method : declaredMethods) {  
                            // 判断这个方法是否有@RequestMapping注解,有则获取它的值
                             if(method.isAnnotationPresent(RequestMapping.class)) {
                                  RequestMapping declaredAnnotation2 = method.getDeclaredAnnotation(RequestMapping.class);
                                  // 拼接路径
                                  path +=  "/" + declaredAnnotation2.value();
                                  HandlerMapping handlerMapping2 = new HandlerMapping();
                                  handlerMapping2.setInstance(instance);
                                  handlerMapping2.setMethod(method);
                                  handlerMapping.put(path, handlerMapping2);
                             }
                        }
                  }
              }
          }
     }

以上第一步就完成了,接下来就是接收请求,并处理请求

2. 请求响应数据的实现

前端向后端发送请求,会先访问它的service方法,service方法在通过解析请求把它发送给doGet方法或doPost方法,这里为简化,把所有的请求都使用doPost方法

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

如何接收请求并处理请求呢?可分为以下几个步骤:

  1. 从handlerMapping集合中获取HandlerMapping对象
  2. 获取HandlerMapping对象中的对象,以及方法
  3. 获取前端传过来中的所有参数值,得到参数值的数组;
  4. 获取参数列表的所有注解
  5. 遍历注解,找到@param注解,并获取它的值
  6. 用这个值给从参数值的数组中获得参数值
  7. 创建数组存放这些参数值
  8. 调用对象,将返回值返回出去
private Object doDispatch(HttpServletRequest req, 
HttpServletResponse resp) {
              String requestURI = req.getRequestURI();
              // 处理URI
              String fileName = req.getServletContext().getContextPath();
              requestURI = requestURI.replaceAll(fileName, "");
              requestURI = requestURI.substring(0, requestURI.lastIndexOf("."));
              // 判断是否存在这个方法
              if(handlerMapping.containsKey(requestURI)) {
                   HandlerMapping handler = handlerMapping.get(requestURI);
                   Method method = handler.getMethod();
                   Object instance = handler.getInstance();

                   // 获取传递的参数值
                   Map<String, String[]> parameterMap = 
req.getParameterMap();
                   // 获取参数列表
                   Parameter[] parameters = method.getParameters();
                   // 获取参数的所有注解
                   Annotation[][] parameterAnnotations = method.getParameterAnnotations();
                   // 创建存放参数值的数组
                   Object[] param = new Object[parameters.length];
                   int index = 0;
                   // 解析注解,通过注解的值获取传过来的参数值,并放入参数值的数组中
                   for (Annotation[] annotations : parameterAnnotations) {
                        for (Annotation annotation : annotations) {
                             if(annotation instanceof Param) {

                                  Param anno = (Param)annotation;
                                  String value = anno.value();

                                 // 判断value值
                                  if(value.equals("request")) {
                                      param[index] = req;
                                  }else if(value.equals("response")) {
                                      param[index] = resp;
                                  }else {
                                      String[] strings = parameterMap.get(value);
                                      param[index] = strings[0];
                                  }

                            }

                        }
                      index++;
                   }
                   // 给method赋值
                   try {
                        Object invoke = method.invoke(instance, param);
                        return invoke;
                   } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                   }
              }
             return null;
          }

处理请求后要给前端发送响应,接收方法的返回值,判断他返回的是字符串还是ModelAndView。再判断它是内部转发还是重定向,再执行相应的操作
首先创建ModelAndView类

public class ModelAndView {

   private String viewName;
 // data中存放的是放在作用域中的值
   private static Map<String,Object> data = new HashMap<>();
   
   // 实现ModelAndView的addAttribute方法
   public void addAttribute(String name, Object obj) {
        data.put(name, obj);
   }

   public ModelAndView() {
        super();
   }

   public ModelAndView(String viewName) {
        super();
        this.viewName = viewName;
   }

   public String getViewName() {
        return viewName;
   }
   
 public void setViewName(String viewName) {
        this.viewName = viewName;
   }

   public Map<String, Object> getData() {
        return data;
   }

   public void setData(Map<String, Object> data) {
        this.data = data;
   }

}

处理响应

@Override

     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          Object obj = doDispatch(req,resp);
          if(null != obj) {
              if(obj instanceof String ) {
                   // 判断是否是重定向
                   if(obj.toString().startsWith("redirect:")) {
                        resp.sendRedirect(obj.toString());
                   }else {
                        req.getRequestDispatcher(obj.toString()).forward(req, resp);
                   }
              }else if(obj instanceof ModelAndView) {
                   ModelAndView model = (ModelAndView) obj;
                   String viewName = model.getViewName();
                   // 判断是否是重定向
                   if(viewName.toString().startsWith("redirect:")) {
                        resp.sendRedirect(viewName.toString());
                   }else {
                   Set<Entry<String, Object>> entrySet = model.getData().entrySet();
                        for (Entry<String, Object> entry : entrySet) {
                             req.setAttribute(entry.getKey(), entry.getValue());

                        }
       
                        req.getRequestDispatcher(viewName.toString()).forward(req, resp);
                   }
              }
          }         
     }

把以上所有的方法组合在一个DispatcherServlet类中,这样就创建好了SpringMVC的一个核心类

总结一下:

  1. IOC容器在加载文件的时候就已经把所有的配置好的类创建好了对象
  2. 对象是通过对象名从IOC容器中获取的;
  3. IOC容器的底层是一个Map集合
  4. Servlet在首次载入时,执行复杂的初始化任务,但不想每个请求都重复使用这些任务时,用init方法,它在servlet初次创建的时候被调用,之后处理每个用户的请求时,则不再调用这个方法。
  5. 前端传过来的请求要先经过前端控制器,前端控制器再通过URL来获取HandlerMapping对象,即花名册,使用花名册找到对应的方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值