百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

面试官:说一下Spring MVC的启动流程呗

yuyutoo 2024-10-26 16:08 7 浏览 0 评论

基于XML配置的容器启动过程

我们常用的Spring MVC是基于Servlet规范实现的,所以我们先来回顾一下Servlet相关的内容。

如果我们直接用Servlet来开发web应用,只需要继承HttpServlet,实现service方法即可,HttpServlet继承自Servlet,Servlet中常用的方法如下

public interface Servlet {

    // 初始化,只会被调用一次,在service方法调用之前完成
    void init(ServletConfig config) throws ServletException;
    
    ServletConfig getServletConfig();
    
    // 处理请求
    void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
    
    String getServletInfo();
    
    // 销毁
    void destroy();
}

每个Servlet有一个ServletConfig,用来保存和Servlet相关的配置每个Web应用有一个ServletContext,用来保存和容器相关的配置

考虑到很多小伙伴可能对Servlet的很多用法不熟悉了,简单介绍一下,就用xml配置了,当然你可以用JavaConfig的方式改一下

项目结构如下

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
          http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

  <context-param>
    <param-name>configLocation</param-name>
    <param-value>test</param-value>
  </context-param>

  <servlet>
    <servlet-name>userServlet</servlet-name>
    <servlet-class>com.javashitang.controller.UserServlet</servlet-class>
    <init-param>
      <param-name>helloWord</param-name>
      <param-value>hello sir</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>userServlet</servlet-name>
    <url-pattern>/user.do</url-pattern>
  </servlet-mapping>

  <listener>
    <listener-class>com.javashitang.listener.MyServletContextListener</listener-class>
  </listener>

</web-app>
public class UserServlet extends HttpServlet {

    private String helloWord;

    @Override
    public void init(ServletConfig config) throws ServletException {
        this.helloWord = config.getInitParameter("helloWord");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();
        String userId = req.getParameter("userId");
        out.println(helloWord + " " + userId);
    }
}

xml配置文件中可以用init-param标签给Servlet设置一些配置,然后在init方法中通过ServletConfig来获取这些配置,做初始化

访问
http://localhost:8080/user.do?userId=1
返回
hello sir 1

可以看到我们针对这个servlet还配置了load-on-startup这个标签,那么这个标签有什么用呢?

load-on-startup表示当容器启动时就初始化这个Servlet,数组越小,启动优先级越啊高。当不配置这个标签的时候则在第一次请求到达的时候才会初始化这个Servlet

context-param标签是容器的初始化配置,可以调用容器的getInitParameter方法获取属性值

Listener是一种扩展机制,当Web应用启动或者停止时会发送各种事件,我们可以用Listener来监听这些事件,做一些初始化工作。如监听启动事件,来初始化数据库连接等。

我这个demo只是获取了一下配置文件的位置,并打印出来。

public class MyServletContextListener implements ServletContextListener {

    // 容器启动
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext sc = sce.getServletContext();
        String location = sc.getInitParameter("configLocation");
        System.out.println(location);
    }

    // 容器销毁
    public void contextDestroyed(ServletContextEvent sce) {

    }
}

基于Xml写一个Spring MVC应用

我们基于xml方式写一个spring mvc应用,基于这个应用来分析,项目结构如下

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
          http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
     version="3.1">
     
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-context.xml</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

spring-context.xml(配置service,dao层)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:context="http://www.springframework.org/schema/context"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

  <context:component-scan base-package="com.javashitang.service"/>

</beans>

spring-mvc.xml(配置和spring mvc相关的配置)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:context="http://www.springframework.org/schema/context"
     xmlns:mvc="http://www.springframework.org/schema/mvc"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

  <context:component-scan base-package="com.javashitang.controller"/>
  <mvc:annotation-driven/>

</beans>
@RestController
public class UserController implements ApplicationContextAware {

  @Resource
  private UserService userService;
  private ApplicationContext context;

  @RequestMapping("user")
  public String index(@RequestParam("userId") String userId) {
    return userService.getUsername(userId);
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.context = applicationContext;
    System.out.println("UserController " + context.getId());
  }
}
public interface UserService {

  String getUsername(String userId);
}
@Service
public class UserServiceImpl implements UserService, ApplicationContextAware {

  private ApplicationContext context;

  @Override
  public String getUsername(String userId) {
    return userId;
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.context = applicationContext;
    System.out.println("UserServiceImpl " + context.getId());
  }
}

我们之所以要用2个配置文件,是因为在Spring MVC中有2个容器

父容器由ContextLoaderListener来初始化,一般用来存放一些dao层和service层的Bean

子容器由DispatcherServlet来初始化,一般用来存放controller层的Bean

项目启动后从打印出的值就可以看出来,Service和Controller是从2个容器获取的

UserServiceImpl org.springframework.web.context.WebApplicationContext:
UserController org.springframework.web.context.WebApplicationContext:/dispatcher

子容器可以访问父容器中的Bean,父容器不能访问子容器中的Bean。当从子容器找不到对应的Bean时,会从父容器中找

父容器启动

父容器由ContextLoaderListener来初始化,当tomcat启动的时候,发布启动事件,调用ContextLoaderListener#contextInitialized方法,接着调用initWebApplicationContext方法

子容器启动

子容器的启动在DispatcherServlet#init方法中

DispatcherServlet中并没有重写init方法,那就实在父类中了,HttpServletBean重写了init方法

用流程图总结一下过程

如果你觉得父容器没啥作用的话,可以把所有的Bean都放在子容器中

当配置父子容器的时候还是比较容易踩坑的,比如在子容器中配置了Bean A,在父容器中配置了Bean B,Bean B使用自动注入依赖了Bean A,此时因为父容器无法查找子容器的Bean,就会抛出找不到Bean A的异常。

可能觉得父子容器这种设计并不是特别好,所以在Spirng MVC用JavaConfig的方式配置时或者用Spirng Boot开发时,都只存在单一的ApplicationContext

基于JavaConfig配置的容器启动过程

Servlet3.0以后出了新规范,Servlet容器容器在启动的时候需要回掉javax.servlet.ServletContainerInitializer接口的onStartup方法,方法的实现类放在META-IN/services/javax.servlet.ServletContainerInitializer文件中,典型的spi代码

这个接口特别重要,Spring Boot中不用web.xml也能启动注册DispatcherServlet的奥秘就在这个接口上

Servlet3.0并且还提供了一个@HandlesTypes注解,里面指定一个类型,servlet容器会把该类型的子类或者实现类,放到ServletContainerInitializer#onStartup方法中的webAppInitializerClasses参数中,然后实现自己的逻辑

基于JavaConfig写一个Spring MVC应用

用JavaConfig写一个Spring MVC应用超级简单

public class MyWebApplicationInitializer implements WebApplicationInitializer {


  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    // Load Spring web application configuration
    AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
    context.register(AppConfig.class);

    // Create and register the DispatcherServlet
    DispatcherServlet servlet = new DispatcherServlet(context);
    ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
    registration.setLoadOnStartup(1);
    registration.addMapping("/*");
  }
}
@EnableWebMvc
@ComponentScan("com.javashitang")
public class AppConfig {
}
@RestController
public class UserController {

  @RequestMapping("user")
  public String index(@RequestParam("userId") String userId) {
    return "hello " + userId;
  }
}

启动流程如下

  1. tomcat启动过程中回调ServletContainerInitializer#onStartup方法,并把@HandlesTypes中的WebApplicationInitializer实现类作为参数传入
  2. ServletContainerInitializer#onStartup方法会调用WebApplicationInitializer#onStartup,完成容器的初始化工作(我们只设置了一个容器哈)

启动过程除了Spring容器初始化是在Web容器回掉WebApplicationInitializer接口时发生的,其余的都一样

相关推荐

12、高阶组件:魔法增幅器——React 19 HOC模式

一、魔法增幅器的本质"高阶组件是魔法师用咒语叠加的炼金术,"霍格沃茨魔咒研究院院长凝视着发光的增幅器,"通过函数式能量场的嵌套,让基础组件获得预言家日报式的逻辑继承!"...

深入理解nodejs的异步IO与事件模块机制

一、node为什么要使用异步I/O异步最先诞生于操作系统的底层,在底层系统中,异步通过信号量、消息等方式有广泛的应用。但在大多数高级编程语言中,异步并不多见,这是因为编写异步的程序不符合人习惯的思维逻...

前端时间同步利器:React + useEffect 实现高性能动态时钟

前言在你奋笔疾敲代码的瞬间,是不是突然一低头,发现时间像偷偷跑路的变量,一眨眼就从上午飘到下午?饭没吃、会没开、工位也快被前端猫霸占了。仿佛你写的不是代码,而是“时间穿梭机”。别慌,咱们今天就来用R...

JavaScript 异步编程指南 - 聊聊 Node.js 中的事件循环

作者:五月君来源:编程界|事件循环是一种控制应用程序的运行机制,在不同的运行时环境有不同的实现,上一节讲了浏览器中的事件循环,它们有很多相似的地方,也有着各自的特点,本节讨论下Node.js中...

10个Vue开发技巧「实践」

作者:WahFung转发链接:https://juejin.im/post/5e8a9b1ae51d45470720bdfa路由参数解耦一般在组件内使用路由参数,大多数人会这样做:...

通过番计时器实例学习 React 生命周期函数 componentDidMount

大家好,今天我们将通过一个实例——番茄计时器,学习下如何使用函数生命周期的一个重要函数componentDidMount():componentDidMount(),在组件加载完成,render之后...

SRE监控四大黄金指标,任何一个有异常都会是灾难……

导读...

前端必看!10 个 Vue3 救命技巧,解决你 90% 的开发难题?

写Vue3项目时,是不是总被数据更新延迟、组件间传值混乱、页面加载缓慢这些问题折磨得头秃?别担心!作为摸爬滚打多年的老前端,今天掏出压箱底的10个实战技巧,从性能优化到复杂逻辑处理,每一个都能...

如何用2 KB代码实现3D赛车游戏?2kPlus Jam大赛了解一下

选自frankforce作者:Frank机器之心编译参与:王子嘉、GeekAI控制复杂度一直是软件开发的核心问题之一,一代代的计算机从业者纷纷贡献着自己的智慧,试图降低程序的计算复杂度。然而,将一款...

证明你访问的网站是你想访问的,Safari 真的需要

安全研究员在Safari上找到了一个新漏洞,能让网站在浏览器的地址栏内将自己伪装成另一个网站——得益于Safari地址栏的“智能缩略”功能。在Deusen最近公开的攻击演示(PoC,P...

抓狂!TS 组件性能拉胯到崩溃?4 个绝杀技巧逆风翻盘!

前端兄弟姐妹们五一假期快乐,咱们谁还没被TypeScript组件的性能问题折磨过?页面加载转圈圈,点击按钮没反应,代码改了一轮又一轮,性能却还是原地踏步,分分钟想砸电脑!别慌,今天这4个绝杀技...

让小球做圆周运动,你有几种办法?

最近在阅读外国技术文章中无意中发现了一个神奇的CSS属性motion-path,它可以让Dom元素可以按照自定义的路径移动。又想起了很久之前参加校招面试的时候,面试官问了我一个问题“能不能不借助库实现...

前端基础进阶(十四):深入核心,详解事件循环机制

EventLoopJavaScript的学习零散而庞杂,很多时候我们学到了一些东西,但是却没办法感受到进步!甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直在试图寻...

从0搭建一个WebRTC,实现多房间多对多通话,并实现屏幕录制

这篇文章开始会实现一个一对一WebRTC和多对多的WebRTC,以及基于屏幕共享的录制。本篇会实现信令和前端部分,信令使用fastity来搭建,前端部分使用Vue3来实现。为什么要使用WebRTCWe...

Vue2 开发卡壳?这 10 个实战技巧专治各种不服

干前端开发的兄弟,谁还没被Vue2折腾过?数据不更新、组件通信乱成麻、性能差到想砸电脑……这些痛点,我都懂!今天直接甩出10个超实用的实战技巧,每一个都是从项目“血坑”里爬出来总结的,专...

取消回复欢迎 发表评论: