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,需要遵循如下约定:
- 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
- 接口实现类所在的jar包放在主程序的classpath中;
- 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
- SPI的实现类必须携带一个不带参数的构造方法;
3.4示例代码
-
定义一组接口,并写出接口(假设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 插入"); } }
-
在 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
-
使用 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行,梳理了一下,实现的流程如下:
- 应用程序调用ServiceLoader.load方法
ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
- lookupIterator(实现迭代器功能)
-
应用程序通过迭代器接口获取对象实例
ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载,实现如下:-
读取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); }
-
通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
-
把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)然后返回实例对象。
-
3.6总结
-
优点:
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
-
缺点:
- 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用ServiceLoader类的实例是不安全的。
3.7SPI机制在servlet3.0中的应用
-
spi简单说明
spi,即service privider interface,是jdk为厂商和插件提供的一种解耦机制。
spi的具体规范为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并通过反射机制实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader -
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/目录下定义了如下文件:
-
SpringServletContainerInitializer
通过源码发现,配合注解@HandlesTypes它可以将其指定的Class对象作为参数传递到onStartup方法中。进而在onStartup方法中获取Class对象的具体实现类,进而调用实现类中的具体方法。SpringServletContainerInitializer类中@HandlesTypes指定的是Class对象是WebApplicationInitializer.Class。利用这个机制,若实现WebApplicationInitializer这个接口,我们就可以自定义的注入Servlet,或者Filter,即可以不再依赖web.xml的配置。
-
@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的源码了。
标签:SPI,SpringMVC,spi,实现,源码,0XML,import,ys,com 来源: https://blog.csdn.net/qq_33762302/article/details/113748157