写一个简易迷你的SpringMVC框架(A版)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_42614447/article/details/89703964

SpringMVC简介

SpringMVC是当前最优秀的MVC框架,自从Spring 2.5版本发布后,由于支持注解配置,易用性有了大幅度的提高。Spring 3.0更加完善,实现了对Struts 2的超越。现在越来越多的开发团队选择了Spring MVC。

  • Spring为展现层提供的基于MVC设计理念的优秀的Web框架,是目前最主流的MVC框架之一
  • Spring3.0后全面超越Struts2,成为最优秀的MVC框架
  • Spring MVC通过一套MVC注解,让POJO成为处理请求的控制器,而无须实现任何接口。
  • 支持REST风格的URL请求
  • 采用了松散耦合可插拔组件结构,比其他MVC框架更具扩展性和灵活性
    在这里插入图片描述
    迷你版的Spring MVC,我将在一个干净的web工程开始开发,不引入Spring,完全通过JDK来实现。
执行过程如图所示:

⑴用户发送请求至前端控制器DispatcherServlet。
⑵ DispatcherServlet收到请求调用HandlerMapping处理器映射器。
⑶ 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
⑷ DispatcherServlet通过HandlerAdapter处理器适配器调用处理器。
⑸ 执行处理器(Controller,也叫后端控制器)。
⑹ Controller执行完成返回ModelAndView。
⑺ HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
⑻ DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
⑼ ViewReslover解析后返回具体View。
⑽ DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)。
⑾ DispatcherServlet响应用户。
从上面可以看出,DispatcherServlet有接收请求,响应结果,转发等作用。有了DispatcherServlet之后,可以减少组件之间的耦合度。

SpringMVC九大组件

HandlerMapping

是用来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行处理,这就是HandlerMapping需要做的事。

HandlerAdapter

从名字上看,它就是一个适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
小结:Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。

HandlerExceptionResolver

其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。

ViewResolver

ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。

RequestToViewNameTranslator

ViewName是根据ViewName查找View,但有的Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。RequestToViewNameTranslator在Spring MVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。

LocaleResolver

解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocaleResolver用于从request解析出Locale,Locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。

ThemeResolver

用于解析主题。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。

MultipartResolver

用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。

FlashMapManager

用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

下面直接上已完成的代码!

工程目录结构

在这里插入图片描述

第一,在annotation包下,我将提供自定义的注解,为了方便理解,就和Spring MVC保持一致。

第二,为了模拟Spring MVC的方法调用链,我这里提供Controller/Service/Dao层进行测试

第三,提供自己的DispatcherServlet完成核心处理逻辑

关于自定义注解

JDK提供了几个元注解,比如:

@Documented : JavaDoc文档

@Target:标志此注解可以修饰在哪些地方,类,成员变量,方法...

@Retention:Annotation的生命周期,一般情况下,我们自定义注解的话,显然需要在运行期获取注解的一些信息。

@Controller注解

模拟Spring MVC的@Controller注解

package com.mymvc.annotation;

import java.lang.annotation.*;

/**
 * 控制层注解
 */
@Documented // JAVADOC
@Target(ElementType.TYPE) // 作用于类上
@Retention(RetentionPolicy.RUNTIME) // 限制Annotation的生命周期,需要运行时保留
public @interface Controller {

	/**
	 * 作用于该类上的注解有一个value属性,就是controller的名称
	 */
	public String value();
}

@Qualifier提供依赖注入

package com.mymvc.annotation;

import java.lang.annotation.*;

/**
 * 
 */
@Documented
@Target(ElementType.FIELD) // 作用于字段上,实现注入
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifier {
	public String value();
}

@RequestMapping提供URL地址处理映射

package com.mymvc.annotation;

import java.lang.annotation.*;

/**
 * 地址映射处理注解
 */
@Documented
@Target({ElementType.METHOD, ElementType.TYPE}) // 该注解可用于类以及方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    public String value();
}

@Repository Dao层注解

package com.mymvc.annotation;

import java.lang.annotation.*;

/**
 * 持久化层注解
 */
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
    public String value();
}

@Service 层注解

package com.mymvc.annotation;

import java.lang.annotation.*;

/**
 * 业务层注解
 */
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
    public String value();
}

DispatcherServlet (编写核心控制器)

在Spring MVC中,DispatcherServlet是核心,下面我们来实现它。首先来说,Spring MVC中的DispatcherServlet说到底,还是HttpServlet的子类,因此我这边自己的DispatcherSerlvet需要extends HttpServlet。

先添加pom依赖,提供servlet依赖
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.1.0</version>
</dependency>
DispatcherServlet
package com.mymvc.servlet;

import com.mymvc.annotation.*;
import com.mymvc.controller.UserController;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@WebServlet(name = "dispatcherServlet", urlPatterns = "/*", loadOnStartup = 1, initParams = {
		@WebInitParam(name = "base-package", value = "com.mymvc") })
public class DispatcherServlet extends HttpServlet {

	// 扫描的基包
	private String basePackage = "";
	// 基包下面所有的带包路径权限定类名
	private List<String> packageNames = new ArrayList<String>();
	// 注解实例化,注解上的名称:实例化对象
	private Map<String, Object> instanceMap = new HashMap<String, Object>();
	// 带包路径的权限定名称:注解上的名称
	private Map<String, String> nameMap = new HashMap<String, String>();
	// URL地址和方法的映射关系,SpringMvc就是方法的调用链
	private Map<String, Method> urlMethodMap = new HashMap<String, Method>();
	// Method和权限定类型映射关系,主要是为了通过Method找到该方法的对象利用反射执行
	private Map<Method, String> methodPackageMap = new HashMap<Method, String>();

	/**
	 * 初始化
	 *
	 * @param config
	 * @throws ServletException
	 */
	@Override
	public void init(ServletConfig config) throws ServletException {
		basePackage = config.getInitParameter("base-package");
		try {
			// 1.扫描基包得到全部的带包路径权限定名
			scanBasePackage(basePackage);
			// 2.把带有@Controller/@Service/@Repository的类实例化放入map中,key为注解的名称
			instance(packageNames);
			// 3.Spring IOC 注入
			SpringIOC();
			// 4.完成URL地址与发放的映射关系
			handlerUrlMethodMap();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
	}

	private void scanBasePackage(String basePackage) {
		// 注意为了得到基包下面的url路径,需要对basepackage做转换:将.替换为/
		URL url = this.getClass().getClassLoader().getResource(basePackage.replaceAll("\\.", "/"));
		File basePackageFile = new File(url.getPath());
		System.out.println("scan:" + basePackageFile);
		File[] childFiles = basePackageFile.listFiles();
		for (File file : childFiles) {
			if (file.isDirectory()) { // 目录继续递归扫描
				scanBasePackage(basePackage + "." + file.getName());
			} else if (file.isFile()) {
				// 类似这种:com.mymvc.service.impl.UserServiceImpl.class 去掉class
				packageNames.add(basePackage + "." + file.getName().split("\\.")[0]);
			}
		}
	}

	private void instance(List<String> packageNames)
			throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		if (packageNames.size() < 1) {
			return;
		}

		for (String string : packageNames) {
			Class c = Class.forName(string);
			if (c.isAnnotationPresent(Controller.class)) {
				Controller controller = (Controller) c.getAnnotation(Controller.class);
				String controllerName = controller.value();

				instanceMap.put(controllerName, c.newInstance());
				nameMap.put(string, controllerName);
				System.out.println("Controller : " + string + " , value : " + controller.value());
			} else if (c.isAnnotationPresent(Service.class)) {
				Service service = (Service) c.getAnnotation(Service.class);
				String serviceName = service.value();

				instanceMap.put(serviceName, c.newInstance());
				nameMap.put(string, serviceName);
				System.out.println("Service : " + string + " , value : " + service.value());
			} else if (c.isAnnotationPresent(Repository.class)) {
				Repository repository = (Repository) c.getAnnotation(Repository.class);
				String repositoryName = repository.value();

				instanceMap.put(repositoryName, c.newInstance());
				nameMap.put(string, repositoryName);
				System.out.println("Controller : " + string + " , value : " + repository.value());
			}
		}
	}

	private void SpringIOC() throws ClassNotFoundException, IllegalAccessException {
		for (Map.Entry<String, Object> entry : instanceMap.entrySet()) {
			Field[] fields = entry.getValue().getClass().getDeclaredFields();
			for (Field field : fields) {
				if (field.isAnnotationPresent(Qualifier.class)) {
					String name = field.getAnnotation(Qualifier.class).value();
					field.setAccessible(true);
					field.set(entry.getValue(), instanceMap.get(name));
				}
			}
		}
	}

	private void handlerUrlMethodMap() throws ClassNotFoundException {
		if (packageNames.size() < 1) {
			return;
		}

		for (String string : packageNames) {
			Class c = Class.forName(string);
			if (c.isAnnotationPresent(Controller.class)) {
				Method[] methods = c.getMethods();
				StringBuffer baseUrl = new StringBuffer();
				if (c.isAnnotationPresent(RequestMapping.class)) {
					RequestMapping requestMapping = (RequestMapping) c.getAnnotation(RequestMapping.class);
					baseUrl.append(requestMapping.value());
				}

				for (Method method : methods) {
					if (method.isAnnotationPresent(RequestMapping.class)) {
						RequestMapping requestMapping = (RequestMapping) method.getAnnotation(RequestMapping.class);
						baseUrl.append(requestMapping.value());

						urlMethodMap.put(baseUrl.toString(), method);
						methodPackageMap.put(method, string);
					}
				}
			}
		}
	}

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

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		String uri = req.getRequestURI();
		String contextPath = req.getContextPath();
		String path = uri.replaceAll(contextPath, "");

		// 通过path找到method
		Method method = urlMethodMap.get(path);
		if (method != null) {
		    // 通过method拿到controller对象,准备反射执行
            String packageName = methodPackageMap.get(method);
            String controllerName = nameMap.get(packageName);

            // 拿到controller对象
            UserController userController = (UserController) instanceMap.get(controllerName);
            try {
                method.setAccessible(true);
                method.invoke(userController);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
	}
}

DispatcherServlet
@WebServlet是什么?
其实,以前我们定义一个Servlet,需要在web.xml中去配置,不过在Servlet3.0后出现了基于注解的Servlet。
仔细观察,你会发现,这个DispatcherServlet是自启动,而且传入了一个参数。
要知道,在Spring MVC中,要想基于注解,需要在配置中指明扫描的包路径,就像这个样子:
<context:component-scan base-package=“com.zfz.myspringmvc”>
</context:component-scan>
为了方便,我这里就通过初始化参数直接将需要扫描的基包路径传入。

init()
其实,在init中,我们主要是完成了什么呢?
第一,我们应该去扫描基包下的类,得到信息A
第二,对于@Controller/@Service/@Repository注解而言,我们需要拿到对应的名称,并初始化它们修饰的类,形成映射关系B
第三,我们还得扫描类中的字段,如果发现有@Qualifier的话,我们需要完成注入
第四,我们还需要扫描@RequestMapping,完成URL到某一个Controller的某一个方法上的映射关系C
其实,Spring MVC的处理流程,就是类似这样的!

扫描基包(scanBasePackage)
注意,基包是X.Y.Z的形式,而URL是X/Y/Z的形式,需要转换。

实例化(instance)
从这里你可以看出,我们完成了被注解标注的类的实例化,以及和注解名称的映射。

依赖注入(SpringIOC)
以前,我们总是说Spring IOC,上面不就是在做这个事情么?

URL映射处理(handlerUrlMethodMap)
URL,我们需要提取出来,映射到Controller的Method上。

doGet/doPost
在doPost方法中,非常简单,我们只需要提取出URL,通过URL映射到Method上,然后通过反射的方式进行调用即可。

Run

controller

package com.mymvc.controller;

import com.mymvc.annotation.Controller;
import com.mymvc.annotation.Qualifier;
import com.mymvc.annotation.RequestMapping;
import com.mymvc.service.UserService;

@Controller("userController")
@RequestMapping("/user")
public class UserController {

    @Qualifier("userServiceImpl")
    private UserService userService;

    @RequestMapping("/insert")
    public void insert() {
        userService.insert();
    }

}

service

package com.mymvc.service;

public interface UserService {
    public void insert();
}

serviceImpl

package com.mymvc.service;

import com.mymvc.annotation.Qualifier;
import com.mymvc.annotation.Service;
import com.mymvc.dto.mapper.UserMapper;

@Service("userServiceImpl")
public class UserServiceImpl implements UserService {

    @Qualifier("userMapperImpl")
    private UserMapper userMapper;

    @Override
    public void insert() {
        System.out.println("UserServiceImpl.insert() start");
        userMapper.insert();
        System.out.println("UserServiceImpl.insert() end");
    }
}

mapper

package com.mymvc.dto.mapper;

public interface UserMapper {

    public void insert();

}

mapperImpl

package com.mymvc.dto.mapper;

import com.mymvc.annotation.Repository;

@Repository("userMapperImpl")
public class UserMapperImpl implements UserMapper {
    @Override
    public void insert() {
        System.out.println("execute UserMapperImpl.insert()");
    }
}

运行结果

http://localhost:8080/user/insert
在这里插入图片描述

供参考!

猜你喜欢

转载自blog.csdn.net/weixin_42614447/article/details/89703964