SpringMVC源码系列(二)0XML搭建SpringMVC环境的原理

1.写在前面

笔者上一篇博客介绍了基于xml搭建SpringMVC的环境,笔者这篇博客打算用0xml的方式来配置springMVC,因为后面打算讲springMVC的源码,所以springMVC的这几种的搭建方式都要知道。好了废话不多说,直接上代码。

2.SpringMVC的0xml方式搭建

至于怎么搭建,我们还是要看官网,记住官网是学习这项技术的最好的地方,让我们直接打开spring的官网,可以看到如下内容

在这里插入图片描述

上面的截图的内容中代码,其实就是等同于我们昨天配置的web.xml中的内容

<web-app>
    <!--初始化spring的环境-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</servlet-name>
        <!--初始化springMVC的环境-->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>

上面的xml的配置,就是让Tomcat容器在一启动的时候就初始化spring和springMVC的环境,就是Tomcat提供给用户的一个入口,那么我们有没有别的入口呢?当然有,就是笔者上面的截图中代码就是一个入口,至于原理,笔者后面会讲,我们直接手动搭建一个0xml的SpringMVC的环境吧,具体的代码如下,先是pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.74</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <!-- 配置Tomcat插件 -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <port>80</port>
                    <path>/</path>
                    <uriEncoding>UTF-8</uriEncoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

然后是启动的类,具体的代码如下:

package com.ys.config;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        System.out.println("Tomcat启动的时候调用了");
        //加载spring web的环境
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);

        // 注册DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("*.do");
    }
}

接下来是配置类,具体的代码如下:

package com.ys.config;

import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.*;

import java.util.List;

@Configuration
@EnableWebMvc
@ComponentScan("com.ys.controller")
public class AppConfig implements WebMvcConfigurer {
    
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/page/", ".html");
    }
    
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter httpMessageConverter = new FastJsonHttpMessageConverter();
        converters.add(httpMessageConverter);
    }
   
}

上面的配置类,可以配置springMVC中的所有东西,笔者这儿只配置了一个跳转的前缀和后缀路径,同时的配置了一个消息转换器。最后就是我们的controller类,具体的代码如下:

package com.ys.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
@RequestMapping("/test")
public class TestController {

    @RequestMapping("/index.do")
    public String index(){
        return "index";
    }

    @RequestMapping("/model.do")
    @ResponseBody
    public Map<String,String> model() {
        Map<String, String> map = new HashMap<>();
        map.put("name", "king");
        map.put("age", "18");
        return map;
    }
}

笔者这儿写了两个方法,一个是测试跳转页面的,一个是测试消息转换器的,然后我们看运行的结果如下:

在这里插入图片描述

可以发现我们要打印的语句已经打印了。

在这里插入图片描述

页面的跳转也是没有问题的。

在这里插入图片描述

消息装换器也是没有问题的。到此整个springMVC的0XML配置已经完成了,至于原理是什么呢?下小节会讲。

3.SpringMVC的0XML方式的原理

springMVC的中原理,就是利用了SPI。

3.1SPI是什么?

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

整体机制图如下:

在这里插入图片描述

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦

3.2使用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

比较常见的例子:

  • 数据库驱动加载接口实现类的加载
    JDBC加载不同类型数据库的驱动
  • 日志门面接口实现类加载
    SLF4J加载不同提供商的日志实现类
  • Spring
    Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo
    Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口。

3.3使用介绍

要使用Java SPI,需要遵循如下约定:

  1. 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
  2. 接口实现类所在的jar包放在主程序的classpath中;
  3. 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  4. SPI的实现类必须携带一个不带参数的构造方法;

3.4示例代码

  1. 定义一组接口,并写出接口(假设com.ys.spi.Dao)的的一个或多个实现类(假设是com.ys.spi.impl.OracleDaoImpl、com.ys.spi.impl.MySqlDaoImpl)。具体的代码如下:

    package com.ys.spi;
    
    public interface Dao {
        void insert();
    }
    
    
    package com.ys.spi.impl;
    
    import com.ys.spi.Dao;
    
    public class MySqlDaoImpl implements Dao {
        @Override
        public void insert() {
            System.out.println("MySql 插入");
        }
    }
    
    
    package com.ys.spi.impl;
    
    import com.ys.spi.Dao;
    
    public class OracleDaoImpl implements Dao {
        @Override
        public void insert() {
            System.out.println("Oracle 插入");
        }
    }
    
    
  2. 在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (com.ys.spi.Dao文件),内容是要应用的实现类(这里是com.ys.spi.impl.OracleDaoImpl和com.ys.spi.impl.MySqlDaoImpl,每行一个类)。

    文件位置

    - src
        -main
            -resources
                - META-INF
                    - services
                        - com.ys.spi.Dao
    

    文件内容

    com.ys.spi.impl.OracleDaoImpl
    com.ys.spi.impl.MySqlDaoImpl
    
  3. 使用 ServiceLoader 来加载配置文件中指定的实现。

    package com.ys;
    
    import com.ys.spi.Dao;
    
    import java.util.ServiceLoader;
    
    public class Main {
        public static void main(String[] args) {
            ServiceLoader<Dao> shouts = ServiceLoader.load(Dao.class);
            for (Dao s : shouts) {
                s.insert();
            }
        }
    }
    
    

    输出的结果如下:

在这里插入图片描述

3.5原理解析

首先看ServiceLoader类的签名类的成员变量:

public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";

    // 代表被加载的类或者接口
    private final Class<S> service;

    // 用于定位,加载和实例化providers的类加载器
    private final ClassLoader loader;

    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;

    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private LazyIterator lookupIterator;
  
    ......
}

参考具体ServiceLoader具体源码,代码量不多,加上注释一共587行,梳理了一下,实现的流程如下:

  1. 应用程序调用ServiceLoader.load方法

ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:

  • loader(ClassLoader类型,类加载器)
  • acc(AccessControlContext类型,访问控制器)
  • providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
  • lookupIterator(实现迭代器功能)
  1. 应用程序通过迭代器接口获取对象实例
    ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载,实现如下:

    1. 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下:

      try {
      	String fullName = PREFIX + service.getName();
          if (loader == null)
              configs = ClassLoader.getSystemResources(fullName);
          else
              configs = loader.getResources(fullName);
      } catch (IOException x) {
          fail(service, "Error locating configuration files", x);
      }
      
    2. 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。

    3. 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)然后返回实例对象。

3.6总结

  • 优点:

    使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

  • 缺点:

    • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
    • 多个并发多线程使用ServiceLoader类的实例是不安全的。

3.7SPI机制在servlet3.0中的应用

  1. spi简单说明
    spi,即service privider interface,是jdk为厂商和插件提供的一种解耦机制。
    spi的具体规范为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并通过反射机制实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

  2. spring-web中的具体应用
    从servlet3.0开始,web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes等,servlet规范中通过ServletContainerInitializer实现此功能。每个框架要使用ServletContainerInitializer就必须在对应的jar包的META-INF/services 目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。

    一般伴随着ServletContainerInitializer一起使用的还有HandlesTypes注解,通过HandlesTypes可以将感兴趣的一些类注入到ServletContainerInitializerde的onStartup方法作为参数传入。

    spring-web的jar定义了一个具体的实现类,SpringServletContainerInitializer,并且在META-INF/services/目录下定义了如下文件:

    在这里插入图片描述

  3. SpringServletContainerInitializer
    通过源码发现,配合注解@HandlesTypes它可以将其指定的Class对象作为参数传递到onStartup方法中。进而在onStartup方法中获取Class对象的具体实现类,进而调用实现类中的具体方法。SpringServletContainerInitializer类中@HandlesTypes指定的是Class对象是WebApplicationInitializer.Class。

    利用这个机制,若实现WebApplicationInitializer这个接口,我们就可以自定义的注入Servlet,或者Filter,即可以不再依赖web.xml的配置。

  4. @HandlesTypes的实现原理:

    首先这个注解最开始令我非常困惑,他的作用是将注解指定的Class对象作为参数传递到onStartup(ServletContainerInitializer)方法中。

    然而这个注解是要留给用户扩展的,他指定的Class对象并没有要继承ServletContainerInitializer,更没有写入META-INF/services/的文件(也不可能写入)中,那么Tomcat是怎么扫描到指定的类的呢。

    答案是Byte Code Engineering Library (BCEL),这是Apache Software Foundation 的Jakarta 项目的一部分,作用同ASM类似,是字节码操纵框架。

    webConfig() 在调用processServletContainerInitializers()时记录下注解的类名,然后在Step 4和Step 5中都来到processAnnotationsStream这个方法,使用BCEL的ClassParser在字节码层面读取了/WEB-INF/classes和某些jar(应该可以在叫做fragments的概念中指定)中class文件的超类名和实现的接口名,判断是否与记录的注解类名相同,若相同再通过org.apache.catalina.util.Introspection类load为Class对象,最后保存起来,于Step 11中交给org.apache.catalina.core.StandardContext,也就是tomcat实际调用

    ServletContainerInitializer.onStartup()的地方

**总结:**就是利用了servlet的新的特性,SPI的技术,来调用实现WebApplicationInitializer接口中的onStartup的方法。

4.写在最后

本篇博客大概的介绍了spring的0xml的启动配置,以及原理,同时介绍了SPI,后面的博客会介绍springMVC的源码了。

参考文章: https://blog.csdn.net/belongtocode/article/details/103335851

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值