目录
一、什么是Spring MVC ?
Spring MVC 是 Spring 的模块之一,Spring MVC 实现了MVC 的设计思想,并继承了 Servlet API 的WEB 框架。当用户在游览器地址栏上输入 url 后,Spring MVC就可以处理用户的请求
Spring 和 Spring MVC 的区别?
Spring 是一个框架,这个框架由不同的模块组成,其中一个模块 就是Spring MVC,Spring 核心是IOC 控制反转,IOC 容器负责对象的创建和依赖注入。
Spring MVC 是基于 MVC 设计来开发web 应用,Spring MVC 将前端发送的请求分发给适当的控制器 Controller,然后根据结果选择合适的视图进行渲染返回
Spring MVC 的运行流程?
- 用户发送HTTP请求
- 请求到达服务器后,Spring MVC 的中央分发器拦截请求
- 中央分发器根据 请求的路径找到对应的 HandlerMapping,确定由哪个 Controller 控制器处理
- HandlerMaping 根据请求信息映射到对应的 Controller ,然后返回给中央分发器
- 中央分发器把请求交给对应的Controller 控制器,Controller 是Spring MVC的一个组件,它负责处理请求以及响应结果
- Controller 控制器调用合适的业务层或 Mapper 层获取数据
- Controller 把数据封装成一个ModelAndView对象,然后返回给中央分发器
- 中央分发器把ModelAndView对象传给 ViewResolver
- ViewResolver 根据视图名称解析出一个 View 对象,然后返回给中央分发器
- 中央分发器把 view 对象渲染出来返回给客户端
在 手写 Spring IOC 的基础上再进行扩展,手写一个 Spring MVC
二、实现步骤
1. DispatcherServlet
1. 创建一个中央分发器
package com.shao.MVC;
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.IOException;
public class DispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("dispatcherServlet 初始化");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet 开始执行任务了");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet doPost");
}
}
拦截所有请求
在 Tomcat 的 web.xml 配置文件中配置自定义的中央分发器,拦截所有请求
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>com.shao.MVC.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
测试
在地址栏随便输入一个请求,中央分发器会进行拦截,然后执行初始化和相关方法,初始化只会执行一次
2. 接管 IOC 容器
在 手写 spring IOC 的时候,为了方便测试是在 Servlet 的 doGet 方法中初始化 IOC 容器,现在改为在中央分发器初始化的时候启动 IOC 容器
1. 创建配置文件
在配置文件中配置扫描包路径,然后启动中央分发器的时候把配置文件传过去
2. 修改 web.xml 配置文件
3. 启动 IOC 容器
在中央分发器的初始化方法中启动 IOC 容器
package com.shao.MVC;
import com.shao.IOC.ApplicationContext;
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.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
public class DispatcherServlet extends HttpServlet {
// 存储 IOC 容器
private ApplicationContext applicationContext;
private Properties Prop = new Properties();
/**
* 初始化
*/
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("dispatcherServlet 初始化");
// 获取传过来的配置文件
String configLocation = config.getInitParameter("contextConfigLocation");
String fileName = configLocation.replace("classpath:", "");
// 调用 loadConfig 方法,传入配置文件名,返回扫描包路径
String packagePath = loadConfig(fileName);
try {
// 启动 IOC 容器
applicationContext = new ApplicationContext(packagePath);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet 开始执行任务了");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet doPost");
}
/**
* 加载配置文件,解析配置文件,返回扫描包路径
*/
public String loadConfig(String path) {
// 以流的方式加载配置文件
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(path);
String basePackage = "";
try {
// 解析配置文件中的属性,以键值对的方式存储到 Prop 中
Prop.load(resourceAsStream);
basePackage = Prop.getProperty("basePackage");
} catch (IOException e) {
e.printStackTrace();
}
return basePackage;
}
}
2. HandlerMapping
HandlerMapping 根据注解的值映射到对应的 Controller 和方法
将 url 和 controller 里面的方法进行映射,存到 HashMap 中,key 是 url ,value 是一个对象,这个对象有 url 对应的 controller 对象和对应的方法
1. 添加映射
1. 创建 RequestMapping 注解
2. 创建映射Bean
package com.shao.MVC;
import java.lang.reflect.Method;
public class RequestMappingBean {
/**
* controller 对象
*/
private Object controller;
/**
* controller 的方法
*/
private Method method;
public RequestMappingBean(Object controller, Method method) {
this.controller = controller;
this.method = method;
}
/**
* 获取
*
* @return controller
*/
public Object getController() {
return controller;
}
/**
* 设置
*
* @param controller
*/
public void setController(Object controller) {
this.controller = controller;
}
/**
* 获取
*
* @return method
*/
public Method getMethod() {
return method;
}
/**
* 设置
*
* @param method
*/
public void setMethod(Method method) {
this.method = method;
}
public String toString() {
return "RequestMappingBean{controller = " + controller + ", method = " + method + "}";
}
}
3. 使用 RequestMapping 注解
4. 添加映射
package com.shao.MVC;
import com.shao.Annotation.Controller;
import com.shao.Annotation.RequestMapping;
import com.shao.IOC.ApplicationContext;
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.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Properties;
public class DispatcherServlet extends HttpServlet {
// 存储 IOC 容器
private ApplicationContext applicationContext;
private Properties Prop = new Properties();
private HashMap<String, RequestMappingBean> mappingMap = new HashMap<>();
/**
* 初始化
*/
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("dispatcherServlet 初始化");
// 获取传过来的配置文件
String configLocation = config.getInitParameter("contextConfigLocation");
String fileName = configLocation.replace("classpath:", "");
// 调用 loadConfig 方法,传入配置文件名,返回扫描包路径
String packagePath = loadConfig(fileName);
try {
// 启动 IOC 容器
applicationContext = new ApplicationContext(packagePath);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 添加映射
AddRequestMapping();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet 开始执行任务了");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet doPost");
}
/**
* 加载配置文件,解析配置文件,返回扫描包路径
*/
public String loadConfig(String path) {
// 以流的方式加载配置文件
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(path);
String basePackage = "";
try {
// 解析配置文件中的属性,以键值对的方式存储到 Prop 中
Prop.load(resourceAsStream);
basePackage = Prop.getProperty("basePackage");
} catch (IOException e) {
e.printStackTrace();
}
return basePackage;
}
/**
* 添加映射
* 1. 从 IOC 容器中获取所有带 RequestMapping 注解的 Controller 对象
* 2. 获取 Controller 对象中带 RequestMapping 注解的方法
* 3. 将 Controller 对象和方法封装为 RequestMappingBean 对象
* 4. 构建映射关系,key 是 url,value 是 映射Bean 对象,包括 Controller 对象和方法
*/
public void AddRequestMapping() {
// 获取 IOC 容器的 Bean Map
HashMap<String, Object> beanMap = applicationContext.getBeanMap();
for (Object bean : beanMap.values()) {
// 获取 bean 的 Class 对象
Class<?> aClass = bean.getClass();
// 判断是否有 @Controller 注解
if (!aClass.isAnnotationPresent(Controller.class)) {
continue;
}
// 判断是否有 @RequestMapping 注解
if (!aClass.isAnnotationPresent(RequestMapping.class)) {
continue;
}
// 获取类的 @RequestMapping 注解的值
String basePath = aClass.getAnnotation(RequestMapping.class).value();
// 获取 Controller 对象中的所有方法
Method[] methods = aClass.getDeclaredMethods();
for (Method method : methods) {
// 判断方法上有没有带 @RequestMapping 注解
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
String path = method.getAnnotation(RequestMapping.class).value();
// 封装为 映射Bean 对象
RequestMappingBean mappingBean = new RequestMappingBean(bean, method);
// 构建映射,添加到 Map 中
mappingMap.put(basePath + path, mappingBean);
}
}
System.out.println("映射添加完成");
System.out.println(mappingMap);
}
}
2. 处理映射
1. 创建 ResponseBody 注解
2. 创建一个 HTML 文件
3. 使用 ResponseBody 注解
4. 处理映射
为了方便测试,只处理了 GET 方法的映射
package com.shao.MVC;
import com.alibaba.fastjson2.JSON;
import com.shao.Annotation.Controller;
import com.shao.Annotation.RequestMapping;
import com.shao.Annotation.ResponseBody;
import com.shao.IOC.ApplicationContext;
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.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Properties;
public class DispatcherServlet extends HttpServlet {
// 存储 IOC 容器
private ApplicationContext applicationContext;
private Properties Prop = new Properties();
private HashMap<String, RequestMappingBean> mappingMap = new HashMap<>();
/**
* 初始化
*/
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("dispatcherServlet 初始化");
// 获取传过来的配置文件
String configLocation = config.getInitParameter("contextConfigLocation");
String fileName = configLocation.replace("classpath:", "");
// 调用 loadConfig 方法,传入配置文件名,返回扫描包路径
String packagePath = loadConfig(fileName);
try {
// 启动 IOC 容器
applicationContext = new ApplicationContext(packagePath);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 添加映射
AddRequestMapping();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet 开始执行任务了");
// 处理请求
HandlerMapping(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("dispatcherServlet doPost");
}
/**
* 加载配置文件,解析配置文件,返回扫描包路径
*/
public String loadConfig(String path) {
// 以流的方式加载配置文件
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(path);
String basePackage = "";
try {
// 解析配置文件中的属性,以键值对的方式存储到 Prop 中
Prop.load(resourceAsStream);
basePackage = Prop.getProperty("basePackage");
} catch (IOException e) {
e.printStackTrace();
}
return basePackage;
}
/**
* 添加映射
* 1. 从 IOC 容器中获取所有带 RequestMapping 注解的 Controller 对象
* 2. 获取 Controller 对象中带 RequestMapping 注解的方法
* 3. 将 Controller 对象和方法封装为 RequestMappingBean 对象
* 4. 构建映射关系,key 是 url,value 是 映射Bean 对象,包括 Controller 对象和方法
*/
public void AddRequestMapping() {
// 获取 IOC 容器的 Bean Map
HashMap<String, Object> beanMap = applicationContext.getBeanMap();
for (Object bean : beanMap.values()) {
// 获取 bean 的 Class 对象
Class<?> aClass = bean.getClass();
// 判断是否有 @Controller 注解
if (!aClass.isAnnotationPresent(Controller.class)) {
continue;
}
// 判断是否有 @RequestMapping 注解
if (!aClass.isAnnotationPresent(RequestMapping.class)) {
continue;
}
// 获取类的 @RequestMapping 注解的值
String basePath = aClass.getAnnotation(RequestMapping.class).value();
// 获取 Controller 对象中的所有方法
Method[] methods = aClass.getDeclaredMethods();
for (Method method : methods) {
// 判断方法上有没有带 @RequestMapping 注解
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
String path = method.getAnnotation(RequestMapping.class).value();
// 封装为 映射Bean 对象
RequestMappingBean mappingBean = new RequestMappingBean(bean, method);
// 构建映射,添加到 Map 中
mappingMap.put(basePath + path, mappingBean);
}
}
System.out.println("映射添加完成");
System.out.println(mappingMap);
}
/**
* 处理请求,根据 url 找到对应的映射对象,调用对应的方法,返回结果
*/
public void HandlerMapping(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取请求的路径,这个请求路径中有项目名称
String requestURI = req.getRequestURI();
// 获取项目名
String contextPath = req.getContextPath();
// 去掉项目名
String url = requestURI.replace(contextPath, "");
// 获取 url 对应的映射对象
RequestMappingBean mappingBean = mappingMap.get(url);
Object controller = mappingBean.getController();
Method method = mappingBean.getMethod();
Object res = null;
try {
// 调用映射对象中的方法
res = method.invoke(controller);
} catch (Exception e) {
e.printStackTrace();
}
// 判断方法是否有返回内容
if (res == null) {
return;
}
// 判断方法是否有 @ResponseBody 注解
if (method.isAnnotationPresent(ResponseBody.class)) {
resp.setContentType("application/json;charset=utf-8");
// 响应数据
resp.getWriter().write(JSON.toJSONString(res));
} else {
// 获取编译后的项目根目录
String path = this.getClass().getClassLoader().getResource("../../").getPath();
// 路径前面有一个 /,比如: /D:/xxx,需要去掉,然后拼接静态资源名称
String filePath = path.substring(1) + res;
try {
// 解码,如果路径有空格或者中文,会出现 16 进制的字符
filePath = URLDecoder.decode(filePath, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 获取静态资源内容
byte[] fileContents = StaticResourceHandler.getFileContents(filePath);
// 获取文件媒体类型
String mimeType = StaticResourceHandler.getFileMimeType(filePath);
resp.setContentType(mimeType + ";charset=utf-8");
// 响应内容
resp.getWriter().write(new String(fileContents));
}
}
}
3. 测试
如果显示乱码可以添加以下命令试一下
-Dfile.encoding=UTF-8