零xml配置SpringMVC!内嵌Tomcat带你开辟新的天地!

想必,大多数人都早已经厌倦了繁杂的配置,以及每次都需要一个外部的 Tomcat, 来启动一个还总是乱码的 web 应用程序。

而实际上,SpringMVC 也是完全可以做到零 xml 配置就完好运行的,并且也可以不需要外部 Tomcat,而是像 Springboot 那样,内嵌一个 Tomcat,直接打包成 jar 文件,就能直接运行。

废话不多说,下面就直接开干!

首先,我们都知道 Springboot 是内嵌了一个 Tomcat 的,所以我们就可以看一下 Springboot 是怎么做的:

private final Tomcat tomcat;
......
private void initialize() throws WebServerException {
	......
            this.tomcat.start();
	......
}

然后我们就会惊奇的发现,原来是有个 Tomcat 对象!
竟然是个 Tomcat 对象?我以前怎么没有接触过!

于是,怀着试试看的心情,点开了这个 Tomcat,你就会发现,这个 Tomcat,原来就在 Maven 依赖的一个包里。
原来 Springboot 不过就是从 Maven 依赖了一个 Tomcat!

所以,Springboot 实际上就是用了这个 Tomcat 然后 new 出一个 Tomcat 对象,然后调用这个 tomcat 对象的 API,去启动了一个内嵌的 Tomcat,然后就能运行了。
在这里插入图片描述

于是,我们就可以,抱着试试看的心态,创建一个项目,引入 Maven 依赖,然后再 main 方法启动 Tomcat。

<dependency>
	<groupId>org.apache.tomcat</groupId>
	<artifactId>tomcat-catalina</artifactId>
	<version>8.5.54</version>
</dependency>
public class WebApp {
	public static void main(String[] args) throws Exception {
		Tomcat tomcat = new Tomcat();
		tomcat.setPort(8080);
		// 这里需要指定一个文件路径,不过没什么用,所以我放一个"java.io.tmpdir"临时目录
		Context context = tomcat.addContext("/", System.getProperty("java.io.tmpdir"));
		tomcat.start();
	}
}

然后,你就会发现,我们的 Tomcat 会一闪而过:
在这里插入图片描述

这是怎么回事呢?
Tomcat 不应该启动了之后,就一直开在那,等着浏览器访问吗?
怎么一打开就关了呢?

实际上,如果对 Tomcat 比较了解的话,就会知道:
实际上,我们只要在最后加上一行:

tomcat.getServer().await();

这样,Tomcat 就会阻塞在这个位置,就不会运行完了,就结束了。
这时,我们在运行起来:
在这里插入图片描述

然后,访问我们的 localhost:8080 就会看到我们熟悉的 Tomcat 界面。
在这里插入图片描述

既然,Tomcat 已经准备就绪,那么,SpringMVC 是不是也就该出场了!

我们来到 Spring 的官网,打开 mvc 章节,就会看到,在开头,Spring 就写好了,零 xml 配置的方式。
惊不惊喜,意不意外?

就在 Spring 官网开头,竟然还不知道?
在这里插入图片描述

所以,我们就只要很自然地,把这段代码 copy 下来,SpringMVC 就已经成功地被加载了。
当然,我稍稍做了些修改。

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class); // 记得改成你自己的配置类
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        // Spring官方给的是/app/*,不过我们一般是配置/*或者*.do
        registration.addMapping("/*");
    }
}

我们可以看到,在开头,不过就是 new 了一个 spring 容器;
然后注册一个 AppConfig 配置类;
然后就是注册 DispatcherServlet;

配置类的话,这里暂时不需要什么东西,我们随意给一个空的配置类即可:

@Configuration
@ComponentScan("com.jiang") // 配置包扫描,一会可以去扫描controller
public class MyConfig {
}

这样的话,我们的 Tomcat,就集成了 Spring 环境,和 dispatchServer 这个关键的 servlet。
看起来已经非常像我们 xml 配置的 SpringMVC 了!

所以,按照道理,应该是 Tomcat 启动的时候,会运行这段代码,就会创建出 SpringMVC 的环境。
所以,为了验证,我们就可以在开头加一段 System.out.println();
然后在控制台打印一段话,这样,就能判断出,代码是否执行了,SpringMVC 环境是否创建了。

@Override
public void onStartup(ServletContext servletCxt) {
	// 打印一段话,表示代码运行了
	System.out.println("----------------------");

	......
}

于是,我们尝试着,运行起来看看:
在这里插入图片描述

发现 Tomcat 是运行起来了,但是!
打印的话呢???
那个 ------------------------------- 哪去了???

看样子,这个方法没有被执行,所以,SpringMVC 的环境,应该也没有被配置?

于是,我们尝试,写一个 Controller,给出映射,看看,会不会从浏览器访问到:

@Controller
public class MyController {
	@RequestMapping("/hello")
	@ResponseBody
	public String hello() {
		return "hello";
	}
}

在这里插入图片描述

可以发现,确实,访问不到,也就是我们的环境,并没有运行起来!

这时为什么???
我们明明按照 Spring 官网说的去做了啊!!!

实际上,这不是 Spring 的锅,而是我们 app 程序的锅。
为什么这么说,因为,这不是一个 web 应用程序!

那么,怎么才是一个 web 应用程序???
我说它是,它就是吗?
你说是就是吗?

显然不可能,毕竟 Tomcat 不认,谁认都没用!

那么,怎么才能把我们的程序,改成一个 web 应用程序呢?
其实很简单,我们只要简单修改一行代码:

public static void main(String[] args) throws Exception {
	Tomcat tomcat = new Tomcat();
	tomcat.setPort(8080);
	// 把这行注释掉
    // Context context = tomcat.addContext("/", System.getProperty("java.io.tmpdir"));
    // 改成这行代码,就表示着,这是一个webapp
	tomcat.addWebapp("/", System.getProperty("java.io.tmpdir"));

	tomcat.start();
	tomcat.getServer().await();
}

然后,我们就会发现我们的 web 程序,已经运行起来了,
因为,我们的那句 print 代码,确实已经执行了!
在这里插入图片描述

然后,我们访问一下页面:
在这里插入图片描述
可以发现,页面已经可以成功访问了。

但是,
控制台又报了个错!!!

怎么一直报错?
不要急,我帮你把所有的坑都整理出来,你才能遇到了,也能不慌不乱。

虽然说,这个报错不影响我们的程序正常访问页面,
但是,有个报错在那里,看到了总是感觉不好看对不对。

我们看报错:
在这里插入图片描述

它说找不到一个叫 JspServlet 的类,也就是缺 jsp 嘛。

而我们用外置的 Tomcat 的时候,是从来没有关心过还要引入 jsp 这种东西的,
所以,我们可以大胆的猜测,外置 Tomcat,本身就集成了 jsp;
而内置 Tomcat,也就是 Maven 引入的 tomcat,是本身不带 jsp 的,所以,我们需要手动引入。

<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
	<version>8.5.54</version>
</dependency>

于是,这时,我们再启动我们的程序,就不会报错了,
看起来也就舒服多了。

不过,实际上,由于现在 jsp 已经过时了,我们可能并不会用到 jsp。
那么,我们导这么一个包有什么意义呢?

所以,我们不想导包,但是又不想它报错!
其实也有办法:

public static void main(String[] args) throws Exception {
	Tomcat tomcat = new Tomcat();
	tomcat.setPort(8080);
	// 用这两行代码,就可以不用导额外的包,也不会报错
	Context context = tomcat.addContext("/", System.getProperty("java.io.tmpdir"));
	context.addLifecycleListener((LifecycleListener) Class.forName(tomcat.getHost().getConfigClass()).newInstance());
	// tomcat.addWebapp("/", System.getProperty("java.io.tmpdir"));

	tomcat.start();
	tomcat.getServer().await();
}

然后,你就可以发现,程序完美运行了,我们的 /hello 接口,也能正常访问。

但是,好像有个问题还没有解决,
就是,我们好像只能返回 String 字符串给浏览器,我们没有视图解析器对不对?
包括,我们假设要返回其它 Object 对象,但是没有 json 解析器,对不对?

那么,我们就只能这样,一直返回 String 吗?

肯定不会的。
其实,Spring 官网也有介绍,如何去配置 SpringMVC:
在这里插入图片描述

所以,我们就仿照着官网给的样子,扩展一下我们的配置类,给它继承一个接口:

@Configuration
@ComponentScan("com.jiang")
@EnableWebMvc // 别忘了加这个注解
public class MyConfig implements WebMvcConfigurer {
}

那么,这个接口具体有什么作用?
我们点进去一看:

public interface WebMvcConfigurer {
	default void configurePathMatch(PathMatchConfigurer configurer) {
	}
	default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
	}
	default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
	}
	default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
	}
	
	......
}

可以发现,这个接口提供了各种在 web 应用启动时会回调的方法;
并且,所有的方法,都被赋予了 default,也就是默认都为空;
这样,就省的我们去重写,即使不重写也没事;
所以,我们只要去重写我们需要的方法,从其中去扩展我们的配置即可。

这样,于是,我们就可以尝试,配置一个 json 解析器:
首先,Maven 导包:

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

然后,我们重写接口的 extendMessageConverters 方法,
这样,就可以往容器中添加消息转换器了。

这里,我们就以 json 解析器为例:

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
	converters.add(new FastJsonHttpMessageConverter());
}

为了测试 json 解析器,我们可以给我们的 Controller 加上一个映射,用来返回一个 Map 对象:

@RequestMapping("/map")
@ResponseBody
public Map<String,String> map() {
	Map<String,String> map = new HashMap<>();
	map.put("key", "value");
	return map;
}

然后,我们重启我们的 web 项目,然后沾沾自喜,等待神圣的到来。

但是,别急,你会发现,还没跑个几秒,控制台就直接报错,然后程序凉凉。。
在这里插入图片描述
这又是怎么回事???
我明明按照 Spring 官网配的,一点都没错!
怎么就挂了?

实际上,这个问题比较复杂。
我们直接看报错的内容,就会发现,是因为一个 bean 创建出错,
这是一个 Spring 内部的错误,而不是我们写错了什么。

那么,这该怎么解决?

实际上,我们只要把之前 onstart 方法中的 ac.refresh(); 去掉就可以了:

@Override
public void onStartup(ServletContext servletCxt) {
	......
	
	AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
	ac.register(MyConfig.class);
	// ac.refresh();

	......
}

这时,你就会发现,程序没有挂掉。
然后,我们来访问我们的路径 /map,看看是否成功了:
在这里插入图片描述

可以发现,确实访问成功了,说明我们的扩展配置也没有问题了。

这样的话,了解了这些之后,读者们,你们完全可以自己去按照自己的意愿,去配置 Spring web 应用程序,
并且不需要外部 Tomcat,以及不需要 xml。
这样的话,可以省去很多繁杂的配置,也更不容易出错。

而很多的扩展点,在 Spring 的官网上都有说到,所以,大家只需要浏览官网即可。

那么,讨论完了,如何实现一个零 xml 配置,
但是,它的原理又是什么呢?

我们先来看,我们在 Spring 官网开头,复制过来的类:

它实现了一个接口,然后 web 应用程序启动的时候,就会回调这个方法,从而初始化我们的 Spring 环境。
于是我们点开这个接口:

package org.springframework.web;

public interface WebApplicationInitializer {
    void onStartup(ServletContext var1) throws ServletException;
}

我们可以发现,这就是一个 Spring 的接口啊?

这里,为什么会显得很奇怪?
因为 Spring 项目,是 Spring 公司开发的;
而 Tomcat,是 Apache 产的啊!

Spring 的一个接口,Tomcat 怎么会来调用???

总不会,Tomcat 在开发的时候,就已经导入了 Spring 的相关 jar 包吧。
不可能!

那么,Spring 和 Tomcat,它们是怎么扯上关系的?

其实,这还是关乎到我们的 Servlet 规范!

首先,我们知道,Servlet 规范,是一个规范!
它本身不是一个项目,也不能运行什么东西。

不过,Tomcat,是一个实现了 Servlet 规范的一个 web 容器,
所以 Servlet 规范规定的事,Tomcat 必须要实现!

那么,我提这个,和这有什么关系?

其实,是因为,Servlet3.0 实现了一个规范:
就是,只要一个类,实现了一个 ServletContainerInitializer 接口,
并且,把这个类的全路径名,写在 META-INF/services/javax.servlet.ServletContainerInitializer 这个文件下,
然后,web 容器启动的时候,就要去回调这个接口的方法。

所以,我们这时就可以验证一下,
于是,我们写一个 ServletContainerInitializer 的实现类:

public class MyServletContainerInitializer implements ServletContainerInitializer {
	@Override
	public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
		// 在META-INF的services下的javax.servlet.ServletContainerInitializer文件中写上这个类
		// 并且这个类实现了ServletContainerInitializer的方法,就会再启动时被tomcat调用
		System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
	}
}

在这里插入图片描述
在这里插入图片描述

然后,我们启动我们的 Tomcat,看看,应用启动时,控制台会不会打印对应的信息,
也就是 xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
在这里插入图片描述
可以发现,确实如此,也就是说,我们可以在容器启动的时候,就执行一些任务,比如初始化我们的 web 环境。

不过,还是有问题!
我们之前实现的 Spring web 环境配置的接口,不是 ServletContainerInitializer,而是 WebApplicationInitializer 这个 Spring 提供的接口!

所以,就算回调方法,也不该回调这个方法啊!???

所以,这里还涉及到,另一个 Servlet 规范。
其实,在 Servlet3.0 还有一个规范,就是上面那个接口的实现类,可以加一个注解:
@HandlesTypes

这样的话,在回调那个方法的时候,会传一个参数,一个 set 集合,
集合里面放的,是一个个的 class,
而这些 class,就是这个接口所指定的 class。

所以,我们看 Spring web 的配置文件中,实际上,就有上面的这些:
首先是实现了 ServletContainerInitializer 的类:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这时,我们发现,Spring 确实给这个类加上了 HandlersTypes 这个注解,
所以,在执行 Spring 这个类的 onstartup 方法的时候,就会传入这个注解提供的接口的 Class,
于是,Spring 就会回调这个接口所有实现类的 onstartup() 回调方法。

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();

		if (webAppInitializerClasses != null) {
		    // 遍历所有的类,实例化对象
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
					    // newInstance实例化对象,并加入list
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		// 遍历list,依次调用onstartup方法
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}
}

而我们在最开始写的初始化 Spring 环境的类,就是 WebApplicationInitializer 的实现类。
所以,在 Tomcat 回调配置文件中写的 SpringServletContainerInitializer 类的 onstartup() 方法的时候,
就会把所有实现 WebApplicationInitializer 接口的实现类创建对象,并且调用 onstartup() 方法。

所以,我们的 Spring 环境就会在这个我们写的方法中,被调用;
因此,Spring web 环境,就会被初始化。

我们,也就因此,可以实现:零 xml 配置 SpringMVC 项目!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章