SpringMVC是Spring框架的一个模块,依托于Spring,基于MVC的设计模式的一个Web MVC框架。SpringMVC作为Java开发最优秀的MVC框架(没有之一),国内几乎做Java开发的都会用到。学会手写自己的SpringMVC框架,做公司最靓的仔。
SpringMVC的运行流程
有关SpringMVC的使用和运行流程网上很多博客都有写到,这里不做具体介绍。
还有另外一个版本:
在Tomcat的conf/server.xml中可以配置处理静态资源的方式(比如处理以.js结尾的请求跳转到xxx)
SpringMVC的核心组件
查看springframework的源码,DispatcherServlet继承了FrameworkServlet,FrameworkServlet继承了HttpServletBean,HttpServletBean继承了HttpServlet同时实现了EnvironmentCapable、EnvironmentAware接口。
HttpServletBean:直接继承自Java的HttpServlet,作用是将Servlet中配置的参数设置到相应的属性。
FrameworkServlet:初始化了WebApplicationContext
DispatcherServlet:初始化了自身的9个组件
图中分为三大块,最下面是Spring MVC,基于Spring和servlet-api实现。
所以说SpringMVC依托于Spring,继承了HttpServlet。
HttpServlet是来自servlet-api Jar包,servlet-api Jar包定义了很多接口、Servlet规范(需要使用Servlet的地方才去实现这些接口,比如Tomcat、netty这些Web容器实现了Servlet规范,我们写的项目/Servlet丢到服务器中才能运行)。
查看源码,DispatcherServlet初始化了九大组件,比较常用的是HandlerMapping和HandlerAdapter
DispatcherServlet是SprinMVC的前端控制器,在Web.xml中配置,是一个中转类,接收请求,再中转给别的类来处理。
HandlerMapping维护了一个Map,请求的URL和处理请求的类的实例之间的映射。
HandlerAdapter:处理类,处理请求,处理完会返回一个ModelAndView的对象
DispatcherServlet再根据HandlerAdapter返回的ModelAndView封装一个ViewResolver
ModelAndView:Model和View两个对象集成,Model是数据,View表示页面属于什么类型(JSP/XML/PDF/JSON)
但是现在开发都是前后端分离的,不会直接返回页面的方式,很少会去直接返回ModelAndView对象,基本上都是使用RESTful API 返回Json和前端/客户端交互,SpringMVC的默认Json解析器是Jackson
SpringMVC的底层加载流程
加载:
- 通过web.xml加载DispatcherServlet
- 加载Spring的xml配置文件(init-param来指定哪个xml文件作为SpringMVC的参数启动)
- 配置Servlet的URL地址,拦截哪些地址(URL-Pattern)
初始化:
- SpringMVC的核心流程:初始化9大组件(init)
- 扫描指定包下的类,扫描指定注解的类(scanpackage=com.xxx.xxx)
- 实例化,把扫描到的类实例化后放在上下文Context中(IOC的功能)
- mapping操作(HandlerMapping),处理请求的url地址和对应类的映射关系
运行:
- DispatcherServlet前端控制器拦截请求,调用doDispatche,从HandlerMapping(map)中获取请求url的method
- 得到method和处理类的全路径后,通过反射来调用执行(invoke)
- response.print()/response.getWriter() 响应输出,可以设置Http头信息,将响应结果输出为什么类型(JSON/XML/HTML/PDF)
设计自己的SpringMVC框架
- 读取配置:通过web.xml加载自己写的MyDispatcherServlet和读取配置文件。
- 初始化,这里不需要把9大组件全部实现,只要实现HandlerMapping就行。
(1)加载配置文件
(2)扫描配置的包下的类
(3)通过反射机制实例化包下的类,并放到ioc容器中(beanName-bean)beanName默认首字母小写;
(4)实例化HandlerMapping - 运行
(1)获取请求传入的参数并处理参数
(2)通过初始化的HandlerMapping中拿出url对应的方法名,反射调用
手写自己的MVC框架
web.xml
<servlet>
<servlet-name>MySpringMvc</servlet-name>
<servlet-class>com.zlin.mvc.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.properties</param-value>
</init-param>
<!-- load-on-startup 赋值为1表示项目丢到服务器中,项目一启动,容器一初始化时就会立马加载这个servlet -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>MySpringMvc</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
application.properties
# 定义扫描包
scanPackage=com.zlin.mvc
自定义注解
package com.zlin.mvc.annotation;
import java.lang.annotation.*;
//jdk自带的注解(对自定义的注解进行注释的,注解的注解) 元注解
// @Target 表示自定义的这个注解能用在哪些地方, 用在类/方法/接口/字段/包..上面
// @Retention 表示注解的生命周期(被jvm加载 => 被jvm执行),
/* RetentionPolicy.RUNTIME 表示class文件被jvm加载成实例后还能获取到annotation的注释信息
(即项目运行起来了能拿到annotation的信息) @Controller("/zlin") 能拿到value "/zlin"
*/
// 自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
/**
* 表示给Controller注册别名
*/
String value() default "";
}
核心类DispatcherServlet
package com.zlin.mvc.servlet;
import com.google.gson.Gson;
import com.zlin.mvc.annotation.Controller;
import com.zlin.mvc.annotation.RequestMapping;
import com.zlin.mvc.annotation.ResponseBody;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
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.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;
/**
* 定义自己的前端控制器
*/
public class DispatcherServlet extends HttpServlet {
private static final long seriaVersionUID = 1L;
// 读取配置
private Properties properties = new Properties();
// 类的全路径名集合
private List<String> classNames = new ArrayList<>();
// ioc
private Map<String, Object> ioc = new HashMap<>();
// handlerMapping, url和对应具体方法的映射关系
private Map<String, Method> handlerMapping = new HashMap<>();
// controllerMap, url和对应controller的映射关系
private Map<String, Object> controllerMap = new HashMap<>();
@Override
public void init(ServletConfig config) throws ServletException {
// 1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
// 2.初始化所有关联的类,扫描配置的包下面所有的类
doScanner(properties.getProperty("scanPackage"));
// 3.拿到扫描到的类,通过反射,实例化,并且放到IOC容器中(k-v beanName-bean) beanName默认首字母小写
doInstance();
// 4.初始化handlerMapping(url和method的映射, 将请求的url能对应上被执行的method)
initHandlerMapping();
}
@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("Server Exception!");
}
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException{
if(handlerMapping.isEmpty()){
return;
}
String url = req.getRequestURI();
String contextPath = req.getContextPath();
// 拼接url 并把多个/替换成一个
url = url.replace(contextPath, "").replaceAll("/+", "/");
if (!this.handlerMapping.containsKey(url)) {
resp.getWriter().write("404 NOT FOUND!");
return;
}
Method method = this.handlerMapping.get(url);
// 获取方法的参数列表
Class<?>[] parameterTypes = method.getParameterTypes();
// 获取请求的参数
Map<String, String[]> parameterMap = req.getParameterMap();
// 保存参数值
Object[] paramValues = new Object[parameterTypes.length];
// 方法的参数列表
for (int i = 0; i < parameterTypes.length; i++) {
// 根据参数名称, 做某些处理
String requestParam = parameterTypes[i].getSimpleName();
if (requestParam.equals("HttpServletRequest")) {
// 参数类型已经明确 这里强转类型
paramValues[i] = req;
continue;
}
if (requestParam.equals("HttpServletResponse")) {
paramValues[i] = resp;
continue;
}
if (requestParam.equals("String")) {
for (Entry<String, String[]> param : parameterMap.entrySet()) {
String value = Arrays.toString(param.getValue()).replaceAll("\\[|\\]", "").replaceAll(",", "");
paramValues[i] = value;
}
}
}
// 利用反射机制来调用
try {
Object object = method.invoke(this.controllerMap.get(url), paramValues);
// 这里使用Gson 官方使用的是jackson
if (method.isAnnotationPresent(ResponseBody.class)) {
String strJson = new Gson().toJson(object);
resp.getWriter().write(strJson);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void doLoadConfig(String location){
if (location.startsWith("classpath:")) {
location = location.replace("classpath:", "");
} else if (location.contains("/")) {
int lastSplitIndex = location.lastIndexOf('/');
location = location.substring(lastSplitIndex+1, location.length());
}
// 把web.xml中的contextConfigLocation对应的value值的文件加载到流里面
InputStream resourcesAsStream = this.getClass().getClassLoader().getResourceAsStream(location);
try {
// 用Properties文件加载文件里的内容
properties.load(resourcesAsStream);
} catch (IOException e){
e.printStackTrace();
} finally {
// 关闭流
if (resourcesAsStream != null) {
try {
resourcesAsStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}
private void doScanner(String packageName){
// 把所有的.替换成/ com.zlin.mvc => com/zlin/mvc
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 {
String className = packageName + "." +file.getName().replace(".class", "");
classNames.add(className);
System.out.println("Spring容器扫描到的类有:" + packageName + "." +file.getName());
}
}
}
private void doInstance(){
if (classNames.isEmpty()) {
return;
}
for (String className : classNames) {
try {
Class<?> clazz = Class.forName(className);
// 通过反射来实例化, 只有加@Controller需要实例化
if (clazz.isAnnotationPresent(Controller.class)) {
Controller controller = clazz.getAnnotation(Controller.class);
String key = controller.value();
if (!"".equals(key) && key != null) {
ioc.put(key, clazz.newInstance());
} else {
// 只拿字节码上含有Controller.class 对象的信息
ioc.put(toLowerFirstWord(clazz.getSimpleName()), clazz.newInstance());
}
} else {
continue;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
/**
* 建立映射关系
*/
private void initHandlerMapping(){
if (ioc.isEmpty()) {
return;
}
try {
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
Class<? extends Object> clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(Controller.class)) {
continue;
}
// 拼接url, Controller头的url + 方法上面的url
String baseUrl = "";
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
baseUrl = requestMapping.value();
}
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String methodUrl = requestMapping.value();
String url = (baseUrl + "/" + methodUrl).replaceAll("/+", "/");
// 这里存放实例和method
handlerMapping.put(url, method);
Object tmpValue = null;
String ctlName = toLowerFirstWord(clazz.getSimpleName());
if (ioc.containsKey(ctlName)) {
tmpValue = ioc.get(ctlName);
} else {
tmpValue = clazz.newInstance();
}
controllerMap.put(url, tmpValue);
// controllerMap.put(url, clazz.newInstance()); 不这样,因为这样可能会重复创建Controller,ioc容器可能已经包含了Controller实例
System.out.println(url + "," +method);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private String toLowerFirstWord(String string){
if (string == null || string.length() == 0) {
return string;
} else {
String upperFirstWord = string.substring(0, 1);
String lowerFirstWord = upperFirstWord.toLowerCase();
string = string.replaceFirst(upperFirstWord, lowerFirstWord);
return string;
}
}
}
定义Controller
package com.zlin.mvc.controller;
import com.zlin.mvc.annotation.Controller;
import com.zlin.mvc.annotation.RequestMapping;
import com.zlin.mvc.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
@RequestMapping("/zlin")
public class MyController {
@RequestMapping("/test")
@ResponseBody
public String test(HttpServletRequest request, HttpServletResponse response) throws IOException {
String name = request.getParameter("name");
return "name:" + name;
}
}
这样自己的SpringMVC框架就完成了,想要添加什么功能可以继续扩展。
和官方的SpringMVC框架肯定没法比,官方的SpringMVC功能强大太多了。当然,了解SpringMVC的底层运行原理和理解其中的设计模式才是最重要的。
项目的GitHub地址:https://github.com/zhonglinliu123/MySpringMvc