Spring MVC之WebApplicationContext 容器的初始化

news2024/11/17 12:56:26

简介

因为 spring-mvc 项目,是 spring-framework 子项目,所以需要拉取 spring-framework 整个工程,包含 Spring 所有的子项目

前期准备工作

源码拉取

Spring 的 Git 仓库 Fork 项目到自己的 Git 仓库,方便我们在阅读源码的过程中,可添加相应的注释后提交代码,也可以直接拉取我的 https://gitee.com/JavaBigDataStudy/spring-source-analysis 阅读

然后参考 Spring 官方提供的 import-into-idea.md 或者 import-into-eclipse.md 进行导入项目

从官方可以直接下载 ZIP 包,解压后执行下述命令再导入 IDEA 中

打开 IDEA Terminal ,输入如下命令,预编译 spring-oxm 项目:

./gradlew :spring-oxm:compileTestJava

spring-framework 项目下,一共有四个和 web 相关的项目,大体关系如下:

可以看到 spring-webmvc 不是一个单独的项目,他有依赖的 spring-web 父项目,也有两个 spring-webfluxspring-websocket 兄弟项目

随着 Spring BootSpring Cloud 在许多中大型企业中被普及,可能你已经忘记当年经典的 Servlet + Spring MVC 的组合,是否还记得那个 web.xml 配置文件。在开始本文之前,请先抛开 Spring Boot 到一旁,回到从前,一起来看看 Servlet 是怎么和 Spring MVC 集成,怎么来初始化 Spring 容器的,在开始阅读本文之前,最好有一定的 Servlet 和 Spring IOC 容器方面的知识,比较容易理解

在开始看具体的源码实现之前,我们先一起来看看现在“陌生”的 web.xml 文件部分内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <display-name>Archetype Created Web Application</display-name>

    <!-- 【1】 Spring 配置 -->
    <!-- 在容器(Tomcat、Jetty)启动时会被 ContextLoaderListener 监听到,
         从而调用其 contextInitialized() 方法,初始化 Root WebApplicationContext 容器 -->
    <!-- 声明 Spring Web 容器监听器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <!-- Spring 和 MyBatis 的配置文件 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mybatis.xml</param-value>
    </context-param>

    <!-- 【2】 Spring MVC 配置 -->
    <!-- 1.SpringMVC 配置 前置控制器(SpringMVC 的入口)
         DispatcherServlet 是一个 Servlet,所以可以配置多个 DispatcherServlet -->
    <servlet>
        <!-- 在 DispatcherServlet 的初始化过程中,框架会在 web 应用 的 WEB-INF 文件夹下,
             寻找名为 [servlet-name]-servlet.xml 的配置文件,生成文件中定义的 Bean. -->
        <servlet-name>SpringMVC</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>
        <!-- 程序运行时从 web.xml 开始,加载顺序为:context-param -> Listener -> Filter -> Structs -> Servlet
             设置 web.xml 文件启动时加载的顺序(1 代表容器启动时首先初始化该 Servlet,让这个 Servlet 随 Servlet 容器一起启动)
             load-on-startup 是指这个 Servlet 是在当前 web 应用被加载的时候就被创建,而不是第一次被请求的时候被创建  -->
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <!-- 这个 Servlet 的名字是 SpringMVC,可以有多个 DispatcherServlet,是通过名字来区分的
             每一个 DispatcherServlet 有自己的 WebApplicationContext 上下文对象,同时保存在 ServletContext 中和 Request 对象中
             ApplicationContext(Spring 容器)是 Spring 的核心
             Context 我们通常解释为上下文环境,Spring 把 Bean 放在这个容器中,在需要的时候,可以 getBean 方法取出-->
        <servlet-name>SpringMVC</servlet-name>
        <!-- Servlet 拦截匹配规则,可选配置:*.do、*.action、*.html、/、/xxx/* ,不允许:/* -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
【1】 处,配置了 org.springframework.web.context.ContextLoaderListener 对象,它实现了 Servlet 的 javax.servlet.ServletContextListener 接口,能够监听 ServletContext 对象的生命周期,也就是监听 Web 应用的生命周期,当 Servlet 容器启动或者销毁时,会触发相应的 ServletContextEvent 事件,ContextLoaderListener 监听到启动事件,则会初始化一个 Root Spring WebApplicationContext 容器,监听到销毁事件,则会销毁该容器
【2】 处,配置了 org.springframework.web.servlet.DispatcherServlet 对象,它继承了 javax.servlet.http.HttpServlet 抽象类,也就是一个 Servlet。Spring MVC 的核心类,处理请求,会初始化一个 属于它的 Spring WebApplicationContext 容器,并且这个容器是以 【1】 处的 Root 容器作为父容器
为什么有了 【2】 创建了容器,还需要 【1】 创建 Root 容器呢?因为可以配置多个 【2】 呀,当然,实际场景下,不太会配置多个 【2】
再总结一次, 【1】【2】 分别会创建其对应的 Spring WebApplicationContext 容器,并且它们是父子容器的关系

Root WebApplicationContext 容器

在上文的 web.xml中,我们已经看到,Root WebApplicationContext 容器的初始化,通过 ContextLoaderListener 来实现。在 Servlet 容器启动时,例如 Tomcat、Jetty 启动后,则会被 ContextLoaderListener 监听到,从而调用 contextInitialized(ServletContextEvent event) 方法,初始化 Root WebApplicationContext 容器

而 ContextLoaderListener 的类图如下:

ContextLoaderListener

org.springframework.web.context.ContextLoaderListener 类,实现 javax.servlet.ServletContextListener 接口,继承 ContextLoader 类,实现 Servlet 容器启动和关闭时,分别初始化和销毁 WebApplicationContext 容器,代码如下:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

    public ContextLoaderListener() {
    }

    /**
     * As of Spring 3.1, supports injecting the root web application context
     */
    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }


    /**
     * Initialize the root web application context.
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        // <1> 初始化 Root WebApplicationContext
        initWebApplicationContext(event.getServletContext());
    }


    /**
     * Close the root web application context.
     */
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        // <2> 销毁 Root WebApplicationContext
        closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }

}
  1. 监听到 Servlet 容器启动事件,则调用父类 ContextLoader 的 initWebApplicationContext(ServletContext servletContext) 方法,初始化 WebApplicationContext 容器

  1. 监听到 Servlet 销毁启动事件,则调用父类 ContextLoader 的 closeWebApplicationContext(ServletContext servletContext) 方法,销毁 WebApplicationContext 容器

ContextLoader

org.springframework.web.context.ContextLoader 类,真正实现初始化和销毁 WebApplicationContext 容器的逻辑的类

静态代码块
public class ContextLoader {

    /**
     * Name of the class path resource (relative to the ContextLoader class)
     * that defines ContextLoader's default strategy names.
     */
    private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";

    /**
     * 默认的配置 Properties 对象
     */
    private static final Properties defaultStrategies;

    static {
        // Load default strategy implementations from properties file.
        // This is currently strictly internal and not meant to be customized by application developers.
        try {
            ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        }
        catch (IOException ex) {
            throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
        }
    }
}

ContextLoader.properties 中,读取默认的配置 Properties 对象。实际上,正如 Load default strategy implementations from properties file. This is currently strictly internal and not meant to be customized by application developers. 所注释,这是一个应用开发者无需关心的配置,而是 Spring 框架自身所定义的

打开来该文件瞅瞅,代码如下:

这意味着什么呢?如果我们没有在 <context-param /> 标签中指定 WebApplicationContext,则默认使用 XmlWebApplicationContext 类,我们在使用 Spring 的过程中一般情况下不会主动指定

构造方法
public class ContextLoader {
    
    /**
     * Name of servlet context parameter (i.e., {@value}) that can specify the
     * config location for the root context, falling back to the implementation's default otherwise.
     * @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
     */
    public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
    
    /** Map from (thread context) ClassLoader to corresponding 'current' WebApplicationContext. */
    private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread = new ConcurrentHashMap<>(1);

    /** The 'current' WebApplicationContext, if the ContextLoader class is deployed in the web app ClassLoader itself. */
    @Nullable
    private static volatile WebApplicationContext currentContext;


    /** The root WebApplicationContext instance that this loader manages. */
    @Nullable
    private WebApplicationContext context;

    /**
     * Create a new {@code ContextLoader} that will create a web application context
     * based on the "contextClass" and "contextConfigLocation" servlet context-params.
     * See class-level documentation for details on default values for each.
     */
    public ContextLoader() {
    }

    /**
     * Create a new {@code ContextLoader} with the given application context.
     * This constructor is useful in Servlet 3.0+ environments where instance-based
     * registration of listeners is possible through the {@link ServletContext#addListener} API.
     */
    public ContextLoader(WebApplicationContext context) {
        this.context = context;
    }
    
    // ... 省略其他相关配置属性
}
  • 上文web.xml 文件中可以看到定义的 contextConfigLocation 参数为 spring-mybatis.xml 配置文件路径

  • currentContextPerThread:用于保存当前 ClassLoader 类加载器与 WebApplicationContext 对象的映射关系

  • currentContext:如果当前线程的类加载器就是 ContextLoader 类所在的类加载器,则该属性用于保存 WebApplicationContext 对象

  • context:WebApplicationContext 实例对象

关于类加载器涉及到 JVM 的“双亲委派机制”。

initWebApplicationContext

initWebApplicationContext(ServletContext servletContext) 方法,初始化 WebApplicationContext 对象,代码如下:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    // <1> 若已经存在 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 对应的 WebApplicationContext 对象,则抛出 IllegalStateException 异常。
    // 例如,在 web.xml 中存在多个 ContextLoader
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
        throw new IllegalStateException(
                "Cannot initialize context because there is already a root application context present - " +
                "check whether you have multiple ContextLoader* definitions in your web.xml!");
    }

    // <2> 打印日志
    servletContext.log("Initializing Spring root WebApplicationContext");
    Log logger = LogFactory.getLog(ContextLoader.class);
    if (logger.isInfoEnabled()) {
        logger.info("Root WebApplicationContext: initialization started");
    }
    // 记录开始时间
    long startTime = System.currentTimeMillis();

    try {
        // Store context in local instance variable, to guarantee that
        // it is available on ServletContext shutdown.
        if (this.context == null) {
            // <3> 初始化 context ,即创建 context 对象
            this.context = createWebApplicationContext(servletContext);
        }
        // <4> 如果是 ConfigurableWebApplicationContext 的子类,如果未刷新,则进行配置和刷新
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            if (!cwac.isActive()) { // <4.1> 未刷新( 激活 )
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) { // <4.2> 无父容器,则进行加载和设置。
                    // The context instance was injected without an explicit parent ->
                    // determine parent for root web application context, if any.
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                // <4.3> 配置 context 对象,并进行刷新
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
        // <5> 记录在 servletContext 中
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        // <6> 记录到 currentContext 或 currentContextPerThread 中
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        }
        else if (ccl != null) {
            currentContextPerThread.put(ccl, this.context);
        }
        if (logger.isInfoEnabled()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
        }

        // <7> 返回 context
        return this.context;
    }
    catch (RuntimeException | Error ex) {
        logger.error("Context initialization failed", ex);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
        throw ex;
    }
}
  1. 若 ServletContext(Servlet 的上下文)已存在 Root WebApplicationContext 对象,则抛出异常,因为不能再初始化该对象

  1. 打印日志,在启动 SSM 项目的时候,是不是都会看到这个日志“Initializing Spring root WebApplicationContext”

  1. 如果context为空,则调用createWebApplicationContext(ServletContext sc)方法,初始化一个 Root

WebApplicationContext 对象,方法如下:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    // <1> 获得 context 的类(默认情况是从 ContextLoader.properties 配置文件读取的,为 XmlWebApplicationContext)
    Class<?> contextClass = determineContextClass(sc);
    // <2> 判断 context 的类,是否符合 ConfigurableWebApplicationContext 的类型
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    // <3> 创建 context 的类的对象
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
  1. 如果是 ConfigurableWebApplicationContext 的子类,并且未刷新,则进行配置和刷新

  1. 如果未刷新(激活),默认情况下,是符合这个条件的,所以会往下执行

  1. 如果无父容器,则进行加载和设置。默认情况下,loadParentContext(ServletContext servletContext) 方法返回一个空对象,也就是没有父容器了

  1. 调用configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)方法,配置context对象,并进行刷新

  1. context对象保存在 ServletContext 中

  1. context对象设置到currentContext或者currentContextPerThread对象中,差异就是类加载器是否相同,具体用途目前不清楚

  1. 返回已经初始化的context对象

configureAndRefreshWebApplicationContext

configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) 方法,配置 ConfigurableWebApplicationContext 对象,并进行刷新,方法如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    // <1> 如果 wac 使用了默认编号,则重新设置 id 属性
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        // 情况一,使用 contextId 属性
        String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
        if (idParam != null) {
            wac.setId(idParam);
        }
        else { // 情况二,自动生成
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }

    // <2>设置 context 的 ServletContext 属性
    wac.setServletContext(sc);
    // <3> 设置 context 的配置文件地址
    String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
        wac.setConfigLocation(configLocationParam);
    }

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }

    // <4> 对 context 进行定制化处理
    customizeContext(sc, wac);
    // <5> 刷新 context ,执行初始化
    wac.refresh();
}
  1. 如果 wac 使用了默认编号,则重新设置 id 属性。默认情况下,我们不会对 wac 设置编号,所以会执行进去。而实际上,id 的生成规则,也分成使用 contextId<context-param /> 标签中由用户配置,和自动生成两种情况。默认情况下,会走第二种情况

  1. 设置 wac 的 ServletContext 属性

  1. 【关键】设置 context 的配置文件地址。例如我们在上文中的 web.xml 中所看到的

<!-- Spring 和 MyBatis 的配置文件 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mybatis.xml</param-value>
</context-param>
  1. wac 进行定制化处理,暂时忽略

  1. 【关键】触发 wac 的刷新事件,执行初始化。此处,就会进行一些的 Spring 容器的初始化工作,涉及到 Spring IOC 相关内容

closeWebApplicationContext

closeWebApplicationContext(ServletContext servletContext) 方法,关闭 WebApplicationContext 容器对象,方法如下:

public void closeWebApplicationContext(ServletContext servletContext) {
    servletContext.log("Closing Spring root WebApplicationContext");
    try {
        // 关闭 context
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ((ConfigurableWebApplicationContext) this.context).close();
        }
    }
    finally {
        // 移除 currentContext 或 currentContextPerThread
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = null;
        }
        else if (ccl != null) {
            currentContextPerThread.remove(ccl);
        }
        // 从 ServletContext 中移除
        servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    }
}

在 Servlet 容器销毁时被调用,用于关闭 WebApplicationContext 对象,以及清理相关资源对象

Servlet WebApplicationContext 容器

在上文的 web.xml中,我们已经看到,除了会初始化一个 Root WebApplicationContext 容器外,还会往 Servlet 容器的 ServletContext 上下文中注入一个 DispatcherServlet 对象,初始化该对象的过程也会初始化一个 Servlet WebApplicationContext 容器

DispatcherServlet 的类图如下:

可以看到 DispatcherServlet 是一个 Servlet 对象,在注入至 Servlet 容器会调用其 init 方法,完成一些初始化工作

  • HttpServletBean ,负责将 ServletConfig 设置到当前 Servlet 对象中,它的 Java doc:

/**
 * Simple extension of {@link javax.servlet.http.HttpServlet} which treats
 * its config parameters ({@code init-param} entries within the
 * {@code servlet} tag in {@code web.xml}) as bean properties.
 */

FrameworkServlet ,负责初始化 Spring Servlet WebApplicationContext 容器,同时该类覆写了 doGet、doPost 等方法,并将所有类型的请求委托给 doService 方法去处理,doService 是一个抽象方法,需要子类实现,它的 Java doc:

/**
 * Base servlet for Spring's web framework. Provides integration with
 * a Spring application context, in a JavaBean-based overall solution.
 */

DispatcherServlet ,负责初始化 Spring MVC 的各个组件,以及处理客户端的请求,协调各个组件工作,它的 Java doc:

/**
 * Central dispatcher for HTTP request handlers/controllers, e.g. for web UI controllers
 * or HTTP-based remote service exporters. Dispatches to registered handlers for processing
 * a web request, providing convenient mapping and exception handling facilities.
 */

每一层的 Servlet 实现类,负责执行相应的逻辑,条理清晰,我们逐个来看

HttpServletBean

org.springframework.web.servlet.HttpServletBean 抽象类,实现 EnvironmentCapable、EnvironmentAware 接口,继承 HttpServlet 抽象类,负责将 ServletConfig 集成到 Spring 中

构造方法
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {

    @Nullable
    private ConfigurableEnvironment environment;

    /**
     * 必须配置的属性的集合,在 {@link ServletConfigPropertyValues} 中,会校验是否有对应的属性
     * 默认为空
     */
    private final Set<String> requiredProperties = new HashSet<>(4);

    protected final void addRequiredProperty(String property) {
        this.requiredProperties.add(property);
    }

    /**
     * 实现了 EnvironmentAware 接口,自动注入 Environment 对象
     */
    @Override
    public void setEnvironment(Environment environment) {
        Assert.isInstanceOf(ConfigurableEnvironment.class, environment, "ConfigurableEnvironment required");
        this.environment = (ConfigurableEnvironment) environment;
    }

    /**
     * 实现了 EnvironmentCapable 接口,返回 Environment 对象
     */
    @Override
    public ConfigurableEnvironment getEnvironment() {
        if (this.environment == null) {
            // 如果 Environment 为空,则创建 StandardServletEnvironment 对象
            this.environment = createEnvironment();
        }
        return this.environment;
    }

    /**
     * Create and return a new {@link StandardServletEnvironment}.
     */
    protected ConfigurableEnvironment createEnvironment() {
        return new StandardServletEnvironment();
    }
}

关于 xxxAware接口,在 Spring 初始化该 Bean 的时候会调用其setXxx方法来注入一个对象,本文暂不分析

init方法

init()方法,重写 GenericServlet 中的方法,负责将 ServletConfig 设置到当前 Servlet 对象中,方法如下:

@Override
public final void init() throws ServletException {
    // Set bean properties from init parameters.
    // <1> 解析 <init-param /> 标签,封装到 PropertyValues pvs 中
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            // <2.1> 将当前的这个 Servlet 对象,转化成一个 BeanWrapper 对象。从而能够以 Spring 的方式来将 pvs 注入到该 BeanWrapper 对象中
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            // <2.2> 注册自定义属性编辑器,一旦碰到 Resource 类型的属性,将会使用 ResourceEditor 进行解析
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            // <2.3> 空实现,留给子类覆盖,目前没有子类实现
            initBeanWrapper(bw);
            // <2.4> 以 Spring 的方式来将 pvs 注入到该 BeanWrapper 对象中
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            if (logger.isErrorEnabled()) {
                logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }
    // Let subclasses do whatever initialization they like.
    // 交由子类去实现,查看 FrameworkServlet#initServletBean() 方法
    initServletBean();
}

1、解析 Servlet 配置的 <init-param /> 标签,封装成 PropertyValues pvs 对象。其中,ServletConfigPropertyValues 是 HttpServletBean 的私有静态类,继承 MutablePropertyValues 类,ServletConfig 的 封装实现类,该类的代码如下:

private static class ServletConfigPropertyValues extends MutablePropertyValues {
    public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) throws ServletException {
        // 获得缺失的属性的集合
        Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ? new HashSet<>(requiredProperties) : null);

        // <1> 遍历 ServletConfig 的初始化参数集合,添加到 ServletConfigPropertyValues 中,并从 missingProps 移除
        Enumeration<String> paramNames = config.getInitParameterNames();
        while (paramNames.hasMoreElements()) {
            String property = paramNames.nextElement();
            Object value = config.getInitParameter(property);
            // 添加到 ServletConfigPropertyValues 中
            addPropertyValue(new PropertyValue(property, value));
            // 从 missingProps 中移除
            if (missingProps != null) {
                missingProps.remove(property);
            }
        }
        // Fail if we are still missing properties.
        if (!CollectionUtils.isEmpty(missingProps)) {
            throw new ServletException("...");
        }
    }
}

在它的构造方法中可以看到,将<init-param />标签定义的一些配置项解析成 PropertyValue 对象,例如在前面的web.xml中的配置,如下:

<init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mvc.xml</param-value>
</init-param>

2、如果存在<init-param />初始化参数

  1. 将当前的这个 Servlet 对象,转化成一个 BeanWrapper 对象。从而能够以 Spring 的方式来将 pvs 注入到该 BeanWrapper 对象中。简单来说,BeanWrapper 是 Spring 提供的一个用来操作 Java Bean 属性的工具,使用它可以直接修改一个对象的属性

  1. 注册自定义属性编辑器,一旦碰到 Resource 类型的属性,将会使用 ResourceEditor 进行解析

  1. 调用initBeanWrapper(BeanWrapper bw)方法,可初始化当前这个 Servlet 对象,空实现,留给子类覆盖,目前好像还没有子类实现

  1. 遍历 pvs 中的属性值,注入到该 BeanWrapper 对象中,也就是设置到当前 Servlet 对象中,例如 FrameworkServlet 中的 contextConfigLocation 属性则会设置为上面的 classpath:spring-mvc.xml 值了

3、【关键】调用initServletBean()方法,空实现,交由子类去实现,完成自定义初始化逻辑,查看 FrameworkServlet#initServletBean() 方法

FrameworkServlet

org.springframework.web.servlet.FrameworkServlet 抽象类,实现 ApplicationContextAware 接口,继承 HttpServletBean 抽象类,负责初始化 Spring Servlet WebApplicationContext 容器

构造方法
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    // ... 省略部分属性
    
    /** Default context class for FrameworkServlet. */
    public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;

    /** WebApplicationContext implementation class to create. */
    private Class<?> contextClass = DEFAULT_CONTEXT_CLASS;

    /** Explicit context config location. 配置文件的地址 */
    @Nullable
    private String contextConfigLocation;

    /** Should we publish the context as a ServletContext attribute?. */
    private boolean publishContext = true;

    /** Should we publish a ServletRequestHandledEvent at the end of each request?. */
    private boolean publishEvents = true;

    /** WebApplicationContext for this servlet. */
    @Nullable
    private WebApplicationContext webApplicationContext;

    /** 标记是否是通过 {@link #setApplicationContext} 注入的 WebApplicationContext */
    private boolean webApplicationContextInjected = false;

    /** 标记已经是否接收到 ContextRefreshedEvent 事件,即 {@link #onApplicationEvent(ContextRefreshedEvent)} */
    private volatile boolean refreshEventReceived = false;

    /** Monitor for synchronized onRefresh execution. */
    private final Object onRefreshMonitor = new Object();

    public FrameworkServlet() {
    }

    public FrameworkServlet(WebApplicationContext webApplicationContext) {
        this.webApplicationContext = webApplicationContext;
    }
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) {
            this.webApplicationContext = (WebApplicationContext) applicationContext;
            this.webApplicationContextInjected = true;
        }
    }
}
  • contextClass 属性:创建的 WebApplicationContext 类型,默认为 XmlWebApplicationContext.class,在 Root WebApplicationContext 容器的创建过程中也是它

  • contextConfigLocation 属性:配置文件的地址,例如:classpath:spring-mvc.xml

  • webApplicationContext 属性:WebApplicationContext 对象,即本文的关键,Servlet WebApplicationContext 容器,有四种创建方式

  1. 通过上面的构造方法

  1. 实现了 ApplicationContextAware 接口,通过 Spring 注入,也就是 setApplicationContext(ApplicationContext applicationContext) 方法

  1. 通过 findWebApplicationContext() 方法,下文见

  1. 通过 createWebApplicationContext(WebApplicationContext parent) 方法,下文见

initServletBean

initServletBean() 方法,重写父类的方法,在 HttpServletBean 的 init() 方法的最后一步会调用,进一步初始化当前 Servlet 对象,当前主要是初始化Servlet WebApplicationContext 容器,代码如下:

@Override
protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
    if (logger.isInfoEnabled()) {
        logger.info("Initializing Servlet '" + getServletName() + "'");
    }
    long startTime = System.currentTimeMillis();

    try {
        // <1> 初始化 WebApplicationContext 对象
        this.webApplicationContext = initWebApplicationContext();
        // <2> 空实现,留给子类覆盖,目前没有子类实现
        initFrameworkServlet();
    }
    catch (ServletException | RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    }

    if (logger.isDebugEnabled()) {
        String value = this.enableLoggingRequestDetails ?
                "shown which may lead to unsafe logging of potentially sensitive data" :
                "masked to prevent unsafe logging of potentially sensitive data";
        logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
                "': request parameters and headers will be " + value);
    }

    if (logger.isInfoEnabled()) {
        logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
    }
}
  1. 调用 initWebApplicationContext() 方法,初始化 Servlet WebApplicationContext 对象

  1. 调用 initFrameworkServlet() 方法,可对当前 Servlet 对象进行自定义操作,空实现,留给子类覆盖,目前好像还没有子类实现

initWebApplicationContext

initWebApplicationContext() 方法【核心】,初始化 Servlet WebApplicationContext 对象,方法如下:

protected WebApplicationContext initWebApplicationContext() {
    // <1> 获得根 WebApplicationContext 对象
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    // <2> 获得 WebApplicationContext wac 对象
    WebApplicationContext wac = null;

    // 第一种情况,如果构造方法已经传入 webApplicationContext 属性,则直接使用
    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        wac = this.webApplicationContext;
        // 如果是 ConfigurableWebApplicationContext 类型,并且未激活,则进行初始化
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) { // 未激活
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                // 配置和初始化 wac
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    // 第二种情况,从 ServletContext 获取对应的 WebApplicationContext 对象
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    // 第三种,创建一个 WebApplicationContext 对象
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        wac = createWebApplicationContext(rootContext);
    }

    // <3> 如果未触发刷新事件,则主动触发刷新事件
    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        synchronized (this.onRefreshMonitor) {
            onRefresh(wac);
        }
    }

    // <4> 将 context 设置到 ServletContext 中
    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}
  1. 调用 WebApplicationContextUtils#getWebApplicationContext((ServletContext sc) 方法,从 ServletContext 中获得 Root WebApplicationContext 对象,可以回到ContextLoader#initWebApplicationContext方法中的第 5 步,你会觉得很熟悉

  1. 获得 WebApplicationContext wac 对象,有三种情况

  1. 如果构造方法已经传入 webApplicationContext 属性,则直接引用给 wac,也就是上面构造方法中提到的第 1、2 种创建方式

如果 wac 是 ConfigurableWebApplicationContext 类型,并且未刷新(未激活),则调用 configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) 方法,进行配置和刷新,下文见

如果父容器为空,则设置为上面第 1 步获取到的 Root WebApplicationContext 对象

  1. 调用 findWebApplicationContext()方法,从 ServletContext 获取对应的 WebApplicationContext 对象,也就是上面构造方法中提到的第 3 种创建方式

@Nullable
protected WebApplicationContext findWebApplicationContext() {
    String attrName = getContextAttribute();
    // 需要配置了 contextAttribute 属性下,才会去查找,一般我们不会去配置
    if (attrName == null) {
        return null;
    }
    // 从 ServletContext 中,获得属性名对应的 WebApplicationContext 对象
    WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    // 如果不存在,则抛出 IllegalStateException 异常
    if (wac == null) {
        throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
    }
    return wac;
}

一般不会这样做

  1. 调用createWebApplicationContext(@Nullable WebApplicationContext parent)方法,创建一个 WebApplicationContext 对象

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    // <a> 获得 context 的类,XmlWebApplicationContext.class
    Class<?> contextClass = getContextClass();
    // 如果非 ConfigurableWebApplicationContext 类型,抛出 ApplicationContextException 异常
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
                "Fatal initialization error in servlet with name '" + getServletName() +
                "': custom WebApplicationContext class [" + contextClass.getName() +
                "] is not of type ConfigurableWebApplicationContext");
    }
    // <b> 创建 context 类的对象
    ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    // <c> 设置 environment、parent、configLocation 属性
    wac.setEnvironment(getEnvironment());
    wac.setParent(parent);
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    // <d> 配置和初始化 wac
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

<a> 获得 context 的 Class 对象,默认为 XmlWebApplicationContext.class,如果非 ConfigurableWebApplicationContext 类型,则抛出异常

<b> 创建 context 的实例对象

<c> 设置 environmentparentconfigLocation 属性。其中,configLocation 是个重要属性

<d> 调用 configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) 方法,进行配置和刷新,下文见

  1. 如果未触发刷新事件,则调用 onRefresh(ApplicationContext context) 方法,主动触发刷新事件,该方法为空实现,交由子类 DispatcherServlet 去实现

  1. context 设置到 ServletContext 中

configureAndRefreshWebApplicationContext

configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) 方法,配置和初始化 wac 对象,方法如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    // <1> 如果 wac 使用了默认编号,则重新设置 id 属性
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        // 情况一,使用 contextId 属性
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        // 情况二,自动生成
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }

    // <2> 设置 wac 的 servletContext、servletConfig、namespace 属性
    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    // <3> 添加监听器 SourceFilteringListener 到 wac 中
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    // <4>
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }

    // <5> 执行处理完 WebApplicationContext 后的逻辑。目前是个空方法,暂无任何实现
    postProcessWebApplicationContext(wac);
    // <6> 执行自定义初始化 context
    applyInitializers(wac);
    // <7> 刷新 wac ,从而初始化 wac
    wac.refresh();
}

实际上,处理逻辑和ContextLoader#configureAndRefreshWebApplicationContext方法差不多

  1. 如果 wac 使用了默认编号,则重新设置 id 属性

  1. 设置 wac 的 servletContext、servletConfig、namespace 属性

  1. 添加监听器 SourceFilteringListener 到 wac

  1. 配置 Environment 对象,暂时忽略

  1. 执行处理完 WebApplicationContext 后的逻辑,空方法,暂无任何实现

  1. wac 进行定制化处理,暂时忽略

  1. 【关键】触发 wac 的刷新事件,执行初始化。此处,就会进行一些的 Spring 容器的初始化工作,涉及到 Spring IOC 相关内容

onRefresh

onRefresh(ApplicationContext context) 方法,当 Servlet WebApplicationContext 刷新完成后,触发 Spring MVC 组件的初始化,方法如下:

/**
 * Template method which can be overridden to add servlet-specific refresh work.
 * Called after successful context refresh.
 * <p>This implementation is empty.
 * @param context the current WebApplicationContext
 * @see #refresh()
 */
protected void onRefresh(ApplicationContext context) {
    // For subclasses: do nothing by default.
}

这是一个空方法,具体的实现,在子类 DispatcherServlet 中,代码如下:

9大组件初始化

// DispatcherServlet.java
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
protected void initStrategies(ApplicationContext context) {
    // 初始化 MultipartResolver
    initMultipartResolver(context);
    // 初始化 LocaleResolver
    initLocaleResolver(context);
    // 初始化 ThemeResolver
    initThemeResolver(context);
    // 初始化 HandlerMappings
    initHandlerMappings(context);
    // 初始化 HandlerAdapters
    initHandlerAdapters(context);
    // 初始化 HandlerExceptionResolvers 
    initHandlerExceptionResolvers(context);
    // 初始化 RequestToViewNameTranslator
    initRequestToViewNameTranslator(context);
    // 初始化 ViewResolvers
    initViewResolvers(context);
    // 初始化 FlashMapManager
    initFlashMapManager(context);
}

初始化九个组件,这里只是先提一下,在后续的文档中会进行分析

onRefresh方法的触发有两种方式:

  • 方式一:如果refreshEventReceivedfalse,也就是未接收到刷新事件(防止重复初始化相关组件),则在 initWebApplicationContext 方法中直接调用

  • 方式二:通过在 configureAndRefreshWebApplicationContext 方法中,触发 wac 的刷新事件

为什么上面的方式二可以触发这个方法的调用呢?

先看到 configureAndRefreshWebApplicationContext 方法的第 3 步,添加了一个 SourceFilteringListener 监听器,如下:

// <3> 添加监听器 SourceFilteringListener 到 wac 中
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

监听到相关事件后,会委派给 ContextRefreshListener 进行处理,它是 FrameworkServlet 的私有内部类,来看看它又是怎么处理的,代码如下:

private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        FrameworkServlet.this.onApplicationEvent(event);
    }
}

直接将该事件委派给了 FrameworkServlet 的 onApplicationEvent 方法,如下:

public void onApplicationEvent(ContextRefreshedEvent event) {
    // 标记 refreshEventReceived 为 true
    this.refreshEventReceived = true;
    synchronized (this.onRefreshMonitor) {
        // 处理事件中的 ApplicationContext 对象,空实现,子类 DispatcherServlet 会实现
        onRefresh(event.getApplicationContext());
    }
}

先设置 refreshEventReceivedtrue,表示已接收到刷新事件,然后再调用 onRefresh 方法,回到上面的方式一方式二,是不是连通起来了,所以说该方法是一定会被触发的

总结

本分对 Spring MVC 两种容器的创建过程进行分析,分别为 Root WebApplicationContextServlet WebApplicationContext 容器,它们是父子关系,创建过程并不是很复杂。前置是在 Tomcat 或者 Jetty 等 Servlet 容器启动后,由 ContextLoaderListener 监听到相应事件而创建的,后者是在 DispatcherServlet 初始化的过程中创建的,因为它是一个 HttpServlet 对象,会调用其 init 方法,完成初始化相关工作

DispatcherServlet 是 Spring MVC 的核心类,相当于一个调度者,请求的处理过程都是通过它调度各个组件来完成的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/353743.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3年软件测试工作经验裸辞,有点后悔了...

2019年毕业&#xff0c;现在有3年的软件测试工作经验&#xff0c;刚毕业前半年在一家知名上市公司&#xff0c;后面则进入一家传统行业公司待到现在2年半。 由于看不到技术成长以及其他原因&#xff0c;上上周辞职了&#xff0c;目前交接中&#xff0c;下个月中旬就得离开了&a…

基于国产龙芯 CPU 的气井工业网关研究与设计(一)

当前&#xff0c;我国气田的自动化控制程度还未完全普及&#xff0c;并且与世界已普及的气井站的自 动化程度也存在一定的差距。而在天然气资源相对丰富的国家&#xff0c;开采过程中设备研发资 金投入较大&#xff0c;研发周期较长&#xff0c;更新了一代又一代的自动化开采系…

看过来,u盘删除的文件还能找回吗?两种方法,

u盘删除的文件还能找回吗&#xff1f;u盘&#xff0c;全称USB闪存驱动器&#xff0c;它不需物理驱动器&#xff0c;即插即用&#xff0c;且其存储容量远超过软盘&#xff0c;方便携带使用。u盘作为我们常用的存储设备&#xff0c;也是有自己不同功能和划分。以下例举了几种&…

ArcGIS API for JavaScript 4.15系列(8)——Dojo中类的定义

1、前言 JavaScript本质上是基于原型继承的一种编程语言&#xff0c;在ES6标准出现以前&#xff0c;JavaScript定义类的方式往往让人很难理解。而Dojo则很好地解决了这个问题。开发者可以通过dojo/_base/declare模块定义类&#xff0c;也可以通过define引用各个类模块。本文就…

Swift基础语法 - 可选项

可选项&#xff08;Optional&#xff09; 可选项&#xff0c;一般也叫可选类型&#xff0c;它允许将值设置为 nil 在类型名称后面加个问号 ? 来定义一个可选项 var name: String? "CSDN" name nilvar age: Int? //默认就是nil age 30 age nilvar array [2,…

提升电脑运行速度,看这里就够了!

电脑是我们经常使用的工具之一&#xff0c;但是它却很容易出现问题&#xff0c;比如运行速度过慢&#xff0c;那么要如何提升电脑运行速度呢&#xff1f;方法1. 通过系统配置设置启动项1. 按下组合键“WinR”打开“运行”&#xff0c;在运行中输入“msconfig”然后按“回车”。…

LVGL-基于Windows系统Visual Studio模拟器搭建

LVGL-基于Windows系统Visual Studio模拟器搭建简述下载安装Visual Studio下载LVGL源码运行效果简述 LVGL是一个轻量级多功能图形库 丰富且强大的模块化图形组件&#xff1a;按钮 (buttons)、图表 (charts)、列表 (lists)、滑动条 (sliders)、图片 (images) 等高级的图形引擎&…

UUID简介以及java代码获取UUID示例

什么是UUIDUUID 是指&#xff08;UniversallyUnique Identifier&#xff09;通用唯一识别码&#xff0c;128位。RFC 4122描述了具体的规范实现。现实问题我们开发的时候&#xff0c;数据库表总会有一个主键&#xff0c;以前我们可能会使用自增的数字作为主键。这样做去确实查询…

PyTorch使用Visdom绘制训练过程曲线

最近在训练网络&#xff0c;网络的损失和准确率都是在终端输出的&#xff0c;很不直观&#xff0c;也看不出变化&#xff0c;就想着有没有一种工具可以实现实时的绘制模型的训练过程&#xff0c;然后就搜到了Visdom&#xff0c;发现这是个好东西啊&#xff0c;完全满足了我的需…

【Kafka】二.Kafka消息发布/消费流程

Kafka 通过对消费方进行分组管理来支持消息一写多读。 我画的图&#xff1a;工具&#xff08;processon在线画图&#xff09; 这个 Topic 分为 4 个 Partition&#xff0c;就是图中的 P1到 P4&#xff0c;上部的生产方根据规则选择一个 Partition 进行写入&#xff0c;默认规则…

算法练习-链表(一)

算法练习-链表&#xff08;一&#xff09; 文章目录算法练习-链表&#xff08;一&#xff09;解题技巧1. 实现链表1.1 节点的定义1.2 链表的遍历1.3 节点的查找1.4节点的插入1.4.1 链头插入1.4.2 链尾插入1.4.3 在给定节点之后插入1.5 删除节点1.5.1 删除给定节点之后的节点、1…

如何快速完成园区数据的可视化分析?

对于园区运营方来说&#xff0c;如果没有专业针对性的管理方案以及管理系统辅助的话&#xff0c;实现园区可视化管理的难度非常大&#xff0c;而且操作成本会很高。但如果园区运营方选择引进快鲸智慧楼宇推出的园区数据孪生可视化管理系统的话就会简单很多。 快鲸智慧楼宇数据孪…

python项目使用pipenv管理环境,如何使用pycharm调试模式

写在开始 了解pipenv的朋友们应该知道&#xff0c;使用pipenv管理项目环境后&#xff0c;仅pipenv虚拟环境中安装有项目的依赖包&#xff0c;测试机本身并没有安装这个依赖包&#xff0c;这就是pipenv使用的意义。但是&#xff0c;如果想要进入调试模式呢&#xff1f;我们要如…

Coqui TTS docker 使用记录

前言 之前介绍过 Coqui TTS 的安装&#xff0c;不过那个环境被我玩挂掉了…… 这次记录一下 docker 版本的使用。 参考网址&#xff1a;Docker images - TTS 0.11.1 documentation 正文 首先按照官网指示先把镜像 pull 下来。&#xff08;后记&#xff1a;确保 GPU driver…

重生之我是赏金猎人-番外篇-记一次层层突破的攻防演练

0x00 前言 本文简单记述了一下本人在某攻防演练过程中一次层层突破的有趣经历 技术性一般&#xff0c;但是层层突破属实艰难&#xff0c;并用到了比较多的思路&#xff0c;还望各位大佬多多指教 0x01 SSO账号获取 由于目标是某大学&#xff0c;对外开放的服务基本上都是一些…

CSS 预处理工具 Less 的介绍及使用 步骤

文章目录Less是什么Less的使用方法Less 中的注释Less 中的变量Less 中的嵌套Less 中的混合&#xff08;Mixin&#xff09;Less 中的运算Less 中的转译Less 中的作用域Less 中的导入Less实用实例文字超出省略文字垂直居中定位上下左右居中Less是什么 Less 是一门 CSS 预处理语言…

spring的事务控制

1.调用这个方法的对象是否是spring的代理对象&#xff08;$CGLIB结尾的&#xff09; 2.这个方法是否是加了Transactional注释 都符合才可以被事物控制 如果调用方法的对象没有被事物控制&#xff0c;那么被调用的方法即便是加了Transactional也是没用的 事务失效情况&#xf…

基于MATLAB计算MIMO信道容量(附完整代码与分析)

目录 一.介绍 二. 代码 三. 运行结果及分析 3.1 MIMO信道容量&#xff1a;固定发射天线数为4 3.2 MIMO信道容量&#xff1a;固定接收天线数为4 3.3 AWGN信道与瑞利信道容量 四. 总结 一.介绍 本文章将在MATLAB环境中分析MIMO信道容量&#xff0c;AWGN信道容量&#xf…

秒懂SpringBoot之全网最易懂的Spring Security教程

[版权申明] 非商业目的注明出处可自由转载 出自&#xff1a;shusheng007 文章目录概述简介原理默认实现使用Token认证方案JWT认证流程自定义Authentication Provider自定义Provider修改配置认证失败与授权失败处理认证失败处理授权失败处理支持方法级别的授权总结源码概述 现如…

PMP好考吗,有多大的价值?

关于PMP考试题型及考试内容&#xff0c;PMP考试共200道单选题&#xff0c;其中25道题不计分&#xff0c;会被随机抽查&#xff0c;答对106道题以上通过考试&#xff0c;参考比例106/175&#xff0c;60.57%估计答对&#xff08;10625&#xff09;道题及上即可通过&#xff0c;参…