1 Spring Web MVC
本文将介绍Spring Web MVC的几种配置方法。
2 反射和SPI
反射的概念应该都不陌生。应用执行时操作的是指令,指令集则被抽象成代码块。OOP中将一个个代码块组合成对象(Object
),Java语言就是这样。使用Java语言编程其实就是构建这样一个个的对象(Object
),而后对这些对象(Object
)进行操作。
有时候仅仅操作这些对象并不够,在特定语境下需要对操作对象的动作进行抽象和表述,比如调用Car对象的start()方法
。这就是反射,反射其实是使用形式化语言表达对对象的操作过程。形式化语言指的是字符串,对象指的就是Java中的一个一个Object
。反射框架则帮助我们实现通过字符串操作对象的动作。
import java.lang.reflect.Method;
public class Invoker {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("org.example.Invoker$Car");
Method method = clazz.getMethod("start");
boolean started = (Boolean)method.invoke(clazz.newInstance());
System.out.println(started);
}
static class Car {
public boolean start() {return true;}
}
}
上面这段示例代码中,创建了内部类Car
,它有一个start()
方法。在main
方法中我们告诉Java:请创建名称为org.example.Invoker$Car
的对象,并调用其start()
方法告诉我们结果。我们指定了对象名称:org.example.Invoker$Car
,需要调用的方法名称:start()
,如何根据这些信息计算得到结果则由Java反射框架来处理。这就是反射。
那反射到底有什么用呢?在什么场景下我们需要使用形式化语言表达对对象的操作过程呢?
最典型的应用场景便是IoC容器了。控制反转指的是将类与类之间的关系,类的创建、调用、销毁的过程全部交出去,由外部系统来控制。Spring框架的IoC容器便大量地使用了反射。但这个话题有点远,在这篇文章中不展开。
另一个应用场景便是我们要介绍的SPI了,SPI全称是Service Provider Interface,直译就是服务提供接口。SPI是Java标准的一部分,能使Java运行过程中主动调起第三方服务,主要是方便功能集成。
SPI规定在/META-INF/services目录下可以存放一个定义文件。文件名称为希望调起服务(interface
)的完整名称(包含包名),文件内容是纯文本形式,每一行给出该服务(interface
)的一个具体实现类的完整名称(包含包名)。
Java提供了ServiceLoader
用于支持该机制。
下面通过简单示例直观展示下。
首先在org.example.contract
中定义一个Vehicle
接口。
package org.example.contract;
public interface Vehicle {
boolean start();
}
增加两个实现了该接口的具体类NewCar
和BrokenCar
。
package org.example.contract.impl;
import org.example.contract.Vehicle;
public class NewCar implements Vehicle {
@Override
public boolean start() {
return true;
}
}
package org.example.contract.impl;
import org.example.contract.Vehicle;
public class BrokenCar implements Vehicle {
@Override
public boolean start() {
return false;
}
}
然后在/META-INF/services目录下创建名为org.example.contract.Vehicle
的文件,并在文件中写入如下内容。
org.example.contract.impl.NewCar
org.example.contract.impl.BrokenCar
最后写个main
函数测试下。
package org.example;
import org.example.contract.Vehicle;
import java.util.ServiceLoader;
public class SPI {
public static void main(String[] args) throws Exception {
ServiceLoader<Vehicle> vehicles = ServiceLoader.load(Vehicle.class);
for (Vehicle car : vehicles) {
System.out.println(car.start());
}
}
}
执行结果很简单,打印两行,一行显示true,一行显示false,不展示了。
SPI中,如何读取/META-INF/services中的文件,以及如何根据文件内容完成程序执行的过程,其实就用到了反射机制。
3 从源代码中来,又回到源代码中去
技术的发展是个非常奇妙的过程。最开始所有的逻辑都写在了Java源代码中,一段时间之后人们觉得这样不好,就开始想办法将部分逻辑调整到源代码之外,因此诞生了诸如Java的反射机制、Spring的IoC容器、Web应用的web.xml配置文件……
可是后来这种模式又不好用了,各种配置文件越来越大,越来越难以维护,怎么办呢?那还是用代码来管吧,至少源代码可读性好,可以进行抽象分层。于是又使用各种注解(annotation
)和反射机制将原来的配置再次注回到源代码中。
这不又回去了吗?所以技术的发展就是这样的,没有一成不变的框架,没有完全通用的模式。
现在的观点认为Code is everything and code can be everything,代码可以管理一切。从程序逻辑的设计、应用系统的配置、应用发布的脚本、应用监控的维护等都逐步代码化了。代码化的好处是可以提升开发及运维的自动化程度,减少人工投入。
自动化程度的提升也进一步促进了大家对使用源代码维护所有信息的观点的认同。因为现在即使是代码级别的调整和变更,也可以逐步变得和以前应用配置信息调整一样,迅速发布和执行了。
3 Tomcat的另一种启动方式
Tomcat目前的版本已经支持不通过web.xml文件直接启动了。网上很多文章都说Tomcat引入了Java的SPI机制,做到了不通过web.xml直接启动Web应用。最开始我也是这么认为的,但看了下Tomcat的源代码发现其实两者仍有差别。
Tomcat并未直接使用Java SPI,而是借用了它的思想,实现了一套从Java类中读取Servlet容器配置信息的框架。
首先,Tomcat在启动时会扫描classpath
目录下的所有文件,找到所有/META-INF/services目录下名为javax.servlet.ServletContainerInitializer
的文件清单。
package javax.servlet;
import java.util.Set;
/**
* Interface which allows a library/runtime to be notified of a web
* application's startup phase and perform any required programmatic
* registration of servlets, filters, and listeners in response to it.
**/
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
ServletContainerInitializer
是java servlet-api定义的一个接口,onStartup(...)
方法会在Servlet容器启动时调起,这样可以让程序在容器启动时处理相关逻辑。
Tomcat源码中有个ContextConfig
类,该类的作用是在tomcat容器启动时,对其进行相关配置。在容器启动时,会先执行下面这段方法:
public class ContextConfig implements LifecycleListener {
private static final String SCI_LOCATION = "META-INF/services/javax.servlet.ServletContainerInitializer";
protected void processServletContainerInitializers(Set<WebXml> fragments) {
for (WebXml fragment : fragments) {
URL url = fragment.getURL();
Jar jar = null;
InputStream is = null;
List<ServletContainerInitializer> detectedScis = null;
try {
if ("jar".equals(url.getProtocol())) {
jar = JarFactory.newInstance(url);
is = jar.getInputStream(SCI_LOCATION);
} else if ("file".equals(url.getProtocol())) {
...
}
if (is != null) {
detectedScis = getServletContainerInitializers(is);
}
}
...
for (ServletContainerInitializer sci : detectedScis) {
initializerClassMap.put(sci, new HashSet<Class<?>>());
...
}
}
}
}
上面代码中将多余部分删除,仅保留和核心逻辑相关的部分。Tomcat启动时,会扫描classpath
下的所有jar包文件,并读取每个jar包中的META-INF/services/javax.servlet.ServletContainerInitializer
文件(若有),通过反射方式得到文件中定义的所有类detectedScis = getServletContainerInitializers(is)
。这个过程和Java SPI解析services文件的过程相似。
关于initializerClassMap
在容器启动过程中如何被使用,则在另一段代码中有展示。
public class ContextConfig implements LifecycleListener {
protected void webConfig() {
...
// Step 11. Apply the ServletContainerInitializer config to the
// context
if (ok) {
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializerClassMap.entrySet()) {
if (entry.getValue().isEmpty()) {
context.addServletContainerInitializer(entry.getKey(), null);
} else {
context.addServletContainerInitializer(entry.getKey(), entry.getValue());
}
}
}
}
在webConfig()
方法的最后,通过context.addServletContainerInitializer(...)
方法将所有的ServletContainerInitializer
赋值给Tomcat容器。
总结一下,Tomcat启动时会加载classpath
所有jar包中的/META-INF/services/javax.servlet.ServletContainerInitializer文件,并将该文件中的类加载到Tomcat容器中。Tomcat在后续启动过程中,会依次调用这些类的onStartup(...)
方法,进行容器的初始化。
4 后续
还差Spring Web MVC中如何通过Tomcat的上述机制完成Web应用初始化部分的介绍,下一遍再写吧。
5 结论
人们普遍认为的真相可能是真相,也可能是以讹传讹。比如说,Tomcat的SCI加载机制使用了Java SPI。