零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 項目!

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