提示:本文要求已经会SpringMVC并整合SSM
本文目的:
是为了更好的理解SpringMVC的原理,背八股文的时候就不用死记硬背。 更深层次建议阅读源码视频或者看大佬博客
深入理解SpringMVC工作原理
本人能力有限,如有遗漏或错误,敬请指正,谢谢
文章目录
前言
学习一门技术最好使用wwh方法
what:这门技术是什么
why:为什么用这个技术,使用会有什么优化
how:怎么使用
提示:现在很多公司都是用SpringBoot开发,以前的SSM开发很多人应该都忘记了,那么如果公司脱离SpringBoot框架,你还会搭建项目吗?
一、前置知识:用Servlet开发传统web项目
在最初学习JavaWeb的时候,就是创建Servlet的实现类重写service/doPost方法,那么面试题:请求怎么找到对应servlet呢?
1.1 请求到指定Servlet接收过程
使用Servlet的时候,是把Servlet写到web.xml里或者用注解@WebServlet。
//1.注解写法
@WebServlet("/user")
public class UserServlet {
}
//2.web.xml写法
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.hs.servlet.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/user</url-pattern>
</servlet-mapping>
- 当服务器接收到用户请求,根据请求路径获取Servlet资源访问路径,如:请求:http://localhost:8080/user?id=1,获取到Servlet路径:/user。
- 那么在web.xml文件里找到
<servlet-mapping>
,再找到对应的<servlet-class>
全类名 - tomcat会把字节码文件加载进内存,并根据
Class.forName()
创建Class对象,之后newInstance()
出Servlet对象
二、SpringMVC的原理
上述传统Servlet方式开发,每一个功能都要创建一个Servlet,比如注册一个Servlet,登录一个Servlet,就会产生成千上万个Servlet
有没有一个控制器,能根据请求,分发到某个类上的某个方法呢?这时候SpringMVC就来了
DispatcherServlet本质上并不会处理用户请求,它仅仅是作为请求统一的访问点,负责请求处理时的全局流程控制。
①用户发送请求至会先进入DispatcherServlet控制器进行相应处理。
②DispatcherServlet会调用HandlerMapping根据请求路径查找Handler。
③处理器映射器找到具体的处理器后,生成Handler对象及Handler拦截器(如果有则生成),然后返回给DispatcherServlet。
④DispatcherServlet紧接着会调用HandlerAdapter,准备执行Handler。
⑤HandlerAdapter底层会利用反射机制,对前面生成的Handler对象进行执行。
⑥执行完对应的Java方法后,HandlerAdapter会得到一个ModelAndView对象。
⑦HandlerAdapter将ModelAndView再返回给DispatcherServlet控制器。
⑧DisPatcherServlet再调用ViewReslover,并将ModelAndView传递给它。
⑨ViewReslover视图解析器开始解析ModelAndView并返回解析出的View视图。
⑩解析出View视图后,对视图进行数据渲染(即将模型数据填充至视图中)。
⑪DispatcherServlet最终将渲染好的View视图响应给用户浏览器。
SpringMVC的流程相信很多人背过,为了更好的理解原理,接下来我们手写简易版SpringMVC
SpringMVC的核心就是DispatcherServlet,由它去调用各类组件完成工作。而DispatcherServlet其实本质上就是一个Servlet子类,一般WEB层框架本质上都离不开Servlet
2.1 HandlerMapping处理映射器
主要负责根据请求路径查找Handler处理器,也就是根据用户的请求路径找到具体的Java方法
比如:请求http://localhost:8080/user/info
,那么中央控制器DispatcherServlet
就会通过HandlerMapping
找到了UserController
下的info
方法
2.2 Handler处理器
就是包含具体业务操作的Java方法,在上面找到方法后,会封装成Handler对象,返回给DispatcherServlet
2.3 HandlerAdapter处理适配器
封装成Handler对象后,DispatcherServlet会找一个适配器来处理这个对象(执行方法),结果返回ModelAndView或者Json字符串
三、手写简易版SpringMVC
1.自定义注解:
@Controller @RequestMapping @ResponseBody
2.定义全局Servlet:DispatcherServlet
3.定义InvocationHandler组件,HandlerMapping组件
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<!--工具类-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.2</version
</dependency>
3.1 自定义注解
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范围:只能生效于类上面
@Target(ElementType.TYPE)
public @interface Controller {
}
// 声明注解的生命周期:RUNTIME表示运行时期有效
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范围:可应用在类上面、方法上面
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface RequestMapping {
// 允许该注解可以填String类型的参数,默认为空
String value() default "";
}
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范围:只能应用在方法上面
@Target(ElementType.METHOD)
public @interface ResponseBody {
}
3.2 自定义组件
//封装成Handler
public class InvocationHandler {
//比如找到了UserController下的info,那么代表UserController实例对象,反射调用info方法
private Object classObject;
//info方法
private Method method;
public InvocationHandler(Object classObject, Method method) {
this.classObject = classObject;
this.method = method;
}
public InvocationHandler(){
}
public Object getClassObject() {
return classObject;
}
public void setClassObject(Object classObject) {
this.classObject = classObject;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
}
这个组件主要负责扫描包,在项目启动时,将指定的包目录下,带有@Controller
的类所有的请求路径与Java方法形成映射关系。(看代码就懂了)
public class HandlerMapping {
//方法作用:获取路径映射的Map,可以根据请求路径,找到对应的Handler,找到后就可以反射执行方法
//参数:所有带有@Controller的类的Class对象
public Map<String,InvocationHandler> urlMapping(Set<Class<?>> classSet){
//key:请求路径,如/user/info value:Handler对象,因为要根据路径找到具体的handler对象
HashMap<String, InvocationHandler> handlerHashMap = new HashMap<>();
//遍历所有Class对象,为了查询该类的方法哪些有@RequestMapping
for(Class<?> aClass:classSet){
//hutool工具类方法:获取该Class对象上的@RequestMapping的value值,没有返回null
String classReqPath = AnnotationUtil.getAnnotationValue(aClass, RequestMapping.class);
System.out.println(aClass.getName()+"类的请求路径:" + classReqPath);
//反射获取该类所有方法
Method[] methods = aClass.getDeclaredMethods();
if(methods!=null && methods.length>0){
//遍历所有方法
for(Method method : methods){
//查看方法上面有没有@RequestMapping
boolean flag = AnnotationUtil.hasAnnotation(method, RequestMapping.class);
if(flag){
//有注解:获取注解的值,也就是请求路径
String methodReqPath = AnnotationUtil.getAnnotationValue(method, RequestMapping.class);
if(StringUtils.isEmpty(methodReqPath)){
//value没有值则返回""
methodReqPath="";
}
System.out.println(aClass.getName()+":"+method.getName()+"方法上的请求路径:" + methodReqPath);
try {
//类上的@RequestMapping的value为空,返回""
if(StringUtils.isEmpty(classReqPath)){
classReqPath="";
}
// 将得到的值封装成 InvocationHandler 对象
//放入一个当前类的实例对象,用于执行后面的类方法
InvocationHandler invocationHandler = new InvocationHandler(aClass.newInstance(), method);
// 使用 类的请求路径 + 方法的请求路径 作为Key
handlerHashMap.put(classReqPath + methodReqPath, invocationHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
return handlerHashMap;
}
}
3.3 全局Servlet
@WebServlet(name="dispatcherServlet",urlPatterns = "/",loadOnStartup = 1)
@RequestMapping("/")
public class DispatcherServlet extends HttpServlet {
//定义一个静态变量
private static Map<String, InvocationHandler> handlerMap;
//tomcat容器初始化会执行此方法
@Override
public void init() throws ServletException {
//原本是要从xml文件中加载扫描的路径,现在假设固定
// xml中:<context:component-scan base-package="com.hs.controller"/>
HandlerMapping handlerMapping=new HandlerMapping();
String controllerUrl="com.hs.controller";
//hutool工具类方法:扫描指定包下,包含指定注解的类
Set<Class<?>> set = ClassScanner.scanPackageByAnnotation("com.hs.controller", Controller.class);
System.out.println("扫描到有controller注解的类:"+set);
//初始化映射关系
handlerMap=handlerMapping.urlMapping(set);
}
//所有请求都会执行这个方法,上面设定了 @WebServlet(urlPatterns = "/")
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//以请求http://localhost:8080/user/info为例子
//获取URI:/user/info
String uri = req.getRequestURI();
// 获取项目路径:"" 可以设定
String contextPath = req.getContextPath();
System.out.println(contextPath);
//判断是否设定了项目路径,有的话就会影响获取handler,所以要排除掉
if(uri.contains(contextPath)){
//比如设定了项目路径/project,那么请求变成/project/user/info
//映射关系里是不带项目路径的,所以要排除掉变回/user/info
uri=uri.replace(contextPath,"");
}
System.out.println("客户端请求路径:" + uri);
//根据请求路径获取handler:没有找到,下面会抛出异常
InvocationHandler invocationHandler = handlerMap.get(uri);
try{
//获取真正要执行的方法
Method method = invocationHandler.getMethod();
//方法所在的类的实例
Object classObject = invocationHandler.getClassObject();
//执行方法,假设返回String
String result = (String)method.invoke(classObject);
//需要判断是否带@ResponseBody注解,带了是返回JSON字符串
if(AnnotationUtil.hasAnnotation(method,ResponseBody.class)){
resp.getOutputStream().print(result);
return;
}
//没有带@ResponseBody注解代表以跳转视图的形式
String prefix="";
String suffix=".jsp";
//判断是否是请求转发
if(result.contains("forward:")){
req.getRequestDispatcher("/"+prefix+result.replace("forward:","")+suffix).forward(req,resp);
}
//判断是否是重定向
if(result.contains("redirect:")){
resp.sendRedirect("http://"+req.getServerName()+":"+req.getServerPort()+req.getContextPath()+"/"+prefix+result.replace("redirect:","")+suffix);
}
//如果没有带上述两个,默认请求转发
if(!result.contains("forward:") && !result.contains("redirect:")){
req.getRequestDispatcher("/"+prefix+result+suffix).forward(req,resp);
}
//没有找到映射关系,也就是会报404,这里直接抛出ServletException异常
}catch (Exception e){
throw new ServletException("没有找到资源");
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
}
四、测试自定义框架
maven项目如下
创建一个跳转视图的测试类
@Controller
public class IndexController {
@RequestMapping("/index")
public String index(){
return "forward:index";
}
}
创建一个返回JSON的测试类
@Controller
@RequestMapping("/user")
public class UserController {
@ResponseBody
@RequestMapping("/info")
public String info(){
return "user info";
}
}
4.1 启动项目
初始化项目会执行init()
方法,那么就会扫描指定包下的类,给静态变量handlerMap
赋值
4.2 测试
五、总结
SpringMVC流程理解:
1.在把一个JavaWeb
程序打成war
包丢入Tomcat
后,当启动Tomcat
时,它就会先去加载web.xml
文件
2.而加载web.xml
文件时,会碰到DispacherServlet
需要被加载,所以又会去加载它,当加载DispacherServlet
时,其实本质上会把SpringMVC
的组件初始化(执行init
方法),然后将所有Controller
的URL资源都映射到一个容器中存储。
3.当后续客户端发生请求时,首先会根据配置好的路由规则,所有请求会先进入DispacherServlet
,DispacherServlet
会先解析客户端的请求路径,然后根据路径去容器中找到该Url对应的handler,找到之后再调用组件去执行具体的Controller
方法
4.当执行完之后,又会将结果返回给DispacherServlet,此时又会去调用相关组件处理执行后的结果,最后才将渲染后的结果响应。