手写简易版SpringMVC(避免死记硬背八股文)

提示:本文要求已经会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>
  1. 当服务器接收到用户请求,根据请求路径获取Servlet资源访问路径,如:请求:http://localhost:8080/user?id=1,获取到Servlet路径:/user。
  2. 那么在web.xml文件里找到<servlet-mapping>,再找到对应的<servlet-class>全类名
  3. 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.当后续客户端发生请求时,首先会根据配置好的路由规则,所有请求会先进入DispacherServletDispacherServlet会先解析客户端的请求路径,然后根据路径去容器中找到该Url对应的handler,找到之后再调用组件去执行具体的Controller方法

4.当执行完之后,又会将结果返回给DispacherServlet,此时又会去调用相关组件处理执行后的结果,最后才将渲染后的结果响应。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值