#Spring #SpringMVC #Mybatis #源码

# 添加 Sring+SpringMVC 相关依赖

因为使用的 maven,web 容器使用的 tomcat,所以先在 pom.xml 中加上:

<packaging>war</packaging>

这样项目就会自动将依赖放进 WEB-INF 下面的 lib 中,否则会出现一系列 NoClassFound 等错误。

添加 spring-webmvc 依赖:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>${spring.version}</version>
</dependency>
<!-- 方便查看源码 -->
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>4.0.1</version>
</dependency>

只添加一个 spring-webmvc 就够了

image.png

可以看到已经将相关依赖全部加载进去了。

这里有一个问题,@Contoller 和 @GetMapping 等注解是在 spring-web 中,然而只有这两个注解,不去配置 DispatcherServlet 是不会处理请求的,也就是说如果只导入 spring-web 依赖,而没有 spring-webmvc,即使有 @Controller,也只是个普通的注解。

# 配置 spring

在 web.xml 中配置

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 采用注解方式配置 -->
<context-param>
  <param-name>contextClass</param-name>
  <param-value>
    <!-- 使用注解实现类 -->
    org.springframework.web.context.support.AnnotationConfigWebApplicationContext
  </param-value>
</context-param>
<context-param>
  <param-name>contextConfigLocation</param-name>
  <!-- 配置类 -->
  <param-value>com.sk.day31.config.ApplicationConfig</param-value>
</context-param>

ContextLoaderListener 实现了 ServletContextListener 接口,ServletContextListener 在文档中的解释是

该接口的实现可以接收关于它们所处的 Web 应用程序的 Servlet 上下文变化的通知。为了接收通知事件,必须在 Web 应用程序的部署描述符中配置该实现类。

ServletContextListener 是 Java EE 标准接口之一,类似 Jetty,Tomcat,JBoss 等 java 容器启动时便会触发该接口的 contextInitialized。

# spring 加载流程

  1. 在 Web 容器启动后,触发 ContextLoaderListener,contextInitialized 方法被调用,接着调用 initWebApplicationContext 方法,并将 ServletContext 当作参数传入
@Override
public void contextInitialized(ServletContextEvent event) {
    initWebApplicationContext(event.getServletContext());
}
  1. 进入 initWebApplicationContext 方法后,调用 createWebApplicationContext
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        ...
    try {
      // Store context in local instance variable, to guarantee that
      // it is available on ServletContext shutdown.
      if (this.context == null) {
        this.context = createWebApplicationContext(servletContext);
      }
        ...
  }
  1. 进入 createWebApplicationContext 方法, 源码如下:
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    Class<?> contextClass = determineContextClass(sc);
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
      throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
          "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
  }

determineContextClass 用来返回一个实现了 WebApplicationContext 接口的类:

protected Class<?> determineContextClass(ServletContext servletContext) {
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    if (contextClassName != null) {
      try {
        return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
      }
      catch (ClassNotFoundException ex) {
        throw new ApplicationContextException(
            "Failed to load custom context class [" + contextClassName + "]", ex);
      }
    }
    else {
      contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
      try {
        return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
      }
      catch (ClassNotFoundException ex) {
        throw new ApplicationContextException(
            "Failed to load default context class [" + contextClassName + "]", ex);
      }
    }
  }

determineContextClass 根据 web.xml 中定义的 contextClass,来决定返回的实现类,如果没有配置,则返回默认的 XmlWebApplicationContext,最后通过 BeanUtils.instantiateClass (contextClass) 创建对象。

那么就出现一个问题,XmlWebApplicationContext 是什么?

首先要先讲一下 BeanFactory,根据官方文档,BeanFactory 接口提供了一个高级配置机制,能够管理任何类型的对象。简单来说就是 Spring 的容器,用来管理各种 Bean。

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
public class MainApp {
   public static void main(String[] args) {
      XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("Beans.xml"));
      HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
      obj.getMessage();
   }
}

上述例子就是用了 BeanFactory 的其中一个实现类 XmlBeanFactory 来创建 Bean。

而 ApplicationContext 是 BeanFactory 的扩展,提供了更多的功能,例如:

  • 用于访问应用程序组件的 Bean 工厂方法。从 ListableBeanFactory 继承而来。

  • 以通用方式加载文件资源的能力。从 ResourceLoader 接口继承而来。

  • 向注册的听众发布事件的能力。从 ApplicationEventPublisher 接口继承而来。

  • 解决消息的能力,支持国际化。继承自 MessageSource 接口。

ApplicationContext 常用的实现类有 ClassPathXmlApplicationContext,以及上面提到的 WebApplicationContext,这些实现类相当于特化,比如 ClassPathXmlApplicationContext 就是从类路径中获取配置文件:

public class TestSpring {
    @Test
    public void test(){
        ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");        
        Hello hello=(Hello)context.getBean("helloWorld");
        hello.say();
    }
}

WebApplicationContext 是专门为 web 应用准备的,它允许从相对于 Web 根目录的路径中装载资源配置文件完成初始化工作。而 WebApplicationContext 最常用的实现类也就是上面的 XmlWebApplicationContext 了,此实现类会默认从 WEB-INF 的目录下读取 applicationContext.xml 来创建容器,当然这个路径也可以自定义。

  1. 再回到前面,determineContextClass 返回了 XmlWebApplicationContext 类,并实例化,将其强转为 ConfigurableWebApplicationContext,并执行 configureAndRefreshWebApplicationContext
if (this.context instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
        if (!cwac.isActive()) {
                    ...
          configureAndRefreshWebApplicationContext(cwac, servletContext);
        }
      }
  1. 进入 configureAndRefreshWebApplicationContext 方法,通过 ServletContext 获取 web.xml 中的 contextConfigLocation 值,设置到 XmlWebApplicationContext 中。
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
      wac.setConfigLocation(configLocationParam);
    }
    ...
    wac.refresh();
  1. 执行 refresh 方法
public void refresh() throws BeansException, IllegalStateException {
    Object var1 = this.startupShutdownMonitor;
    synchronized(this.startupShutdownMonitor) {
       // 容器预先准备,记录容器启动时间和标记
      prepareRefresh();
      // 创建 bean 工厂,里面实现了 BeanDefinition 的装载
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
      // 配置 bean 工厂的上下文信息,如类装载器等
      prepareBeanFactory(beanFactory);
      try {
        // 在 BeanDefinition 被装载后,提供一个修改 BeanFactory 的入口
        postProcessBeanFactory(beanFactory);
        // 在 bean 初始化之前,提供对 BeanDefinition 修改入口,PropertyPlaceholderConfigurer 在这里被调用
        invokeBeanFactoryPostProcessors(beanFactory);
        // 注册各种 BeanPostProcessors,用于在 bean 被初始化时进行拦截,进行额外初始化操作
        registerBeanPostProcessors(beanFactory);
        // 初始化 MessageSource
        initMessageSource();
        // 初始化上下文事件广播
        initApplicationEventMulticaster();
        // 模板方法
        onRefresh();
        // 注册监听器
        registerListeners();
        // 初始化所有未初始化的非懒加载的单例 Bean
        finishBeanFactoryInitialization(beanFactory);
        // 发布事件通知
        finishRefresh();
        } catch (BeansException var5) {
            if(this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var5);
            }
            this.destroyBeans();
            this.cancelRefresh(var5);
            throw var5;
        }
    }
}

完成 bean 的加载。

# JavaConfig 方式配置 ApplicationConfig

@Configuration
@ComponentScan(basePackages = "com.sk.day31", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)})
public class ApplicationConfig {
}

效果类似

<context:component-scan base-package="com.sk.day31">
  <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>-->
</context:component-scan>

# 配置 spring-webmvc

在 web.xml 中配置:

<servlet>
  <servlet-name>dispatcherServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextClass</param-name>
    <param-value>
      <!-- 使用注解实现类 -->
      org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
  </init-param>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>com.sk.day31.config.SpringMvcConfig</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>dispatcherServlet</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>
@Configuration
@ComponentScan(basePackages = "com.sk.day31.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("classpath:/static/")
                .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
    }
  
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

@EnableWebMvc 效果等于 mvc:annotation-driven/

configureDefaultServletHandling 方法作用等于 mvc:default-servlet-handler/

# 配置 mybatis

# 添加依赖

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>${spring.version}</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.28</version>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.22</version>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.0.7</version>
</dependency>
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.9</version>
</dependency>

spring-jdbc 必须添加,不然会报错 java.lang.NoClassDefFoundError: org/springframework/dao/support/DaoSupport。

# 配置类

@Configuration
@PropertySource("classpath:druid.properties")
@MapperScan(basePackages = "com.sk.day31.mapper")
public class MybatisConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
  
    @Bean
    public DruidDataSource dataSource(){
        DruidDataSource dataSource=new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
  
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setTypeAliasesPackage("com.sk.day31.entity");
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        factoryBean.setDataSource(dataSource());
        return factoryBean.getObject();
    }
}

到此就算是整合完毕了