SpringBoot系列教程web篇Servlet 註冊的四種姿勢
前面介紹了 java web 三要素中 filter 的使用指南與常見的易錯事項,接下來我們來看一下 Servlet 的使用姿勢,本篇主要帶來在 SpringBoot 環境下,註冊自定義的 Servelt 的四種姿勢
-
@WebServlet
註解 -
ServletRegistrationBean
bean 定義 -
ServletContext
動態新增 - 普通的 spring bean 模式
I. 環境配置
1. 專案搭建
首先我們需要搭建一個 web 工程,以方便後續的 servelt 註冊的例項演示,可以通過 spring boot 官網建立工程,也可以建立一個 maven 工程,在 pom.xml 中如下配置
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding >UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId >
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
複製程式碼
特別說明:
為了緊跟 SpringBoot 的最新版本,從本篇文章開始,博文對應的示例工程中 SpringBoot 版本升級到2.2.1.RELEASE
II. Servlet 註冊
自定義一個 Servlet 比較簡單,一般常見的操作是繼承HttpServlet
,然後覆蓋doGet
,doPost
等方法即可;然而重點是我們自定義的這些 Servlet 如何才能被 SpringBoot 識別並使用才是關鍵,下面介紹四種註冊方式
1. @WebServlet
在自定義的 servlet 上新增 Servlet3+的註解@WebServlet
,來宣告這個類是一個 Servlet
和 Fitler 的註冊方式一樣,使用這個註解,需要配合 Spring Boot 的@ServletComponentScan
,否則單純的新增上面的註解並不會生效
/**
* 使用註解的方式來定義並註冊一個自定義Servlet
* Created by @author yihui in 19:08 19/11/21.
*/
@WebServlet(urlPatterns = "/annotation")
public class AnnotationServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException {
String name = req.getParameter("name");
PrintWriter writer = resp.getWriter();
writer.write("[AnnotationServlet] welcome " + name);
writer.flush();
writer.close();
}
}
複製程式碼
上面是一個簡單的測試 Servlet,接收請求引數name
,並返回 welcome xxx
;為了讓上面的的註解生效,需要設定下啟動類
@ServletComponentScan
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
複製程式碼
然後啟動測試,輸出結果如:
➜ ~ curl http://localhost:8080/annotation\?name\=yihuihui
# 輸出結果
[AnnotationServlet] welcome yihuihui%
複製程式碼
2. ServletRegistrationBean
在 Filter 的註冊中,我們知道有一種方式是定義一個 Spring 的 Bean FilterRegistrationBean
來包裝我們的自定義 Filter,從而讓 Spring 容器來管理我們的過濾器;同樣的在 Servlet 中,也有類似的包裝 bean: ServletRegistrationBean
自定義的 bean 如下,注意類上沒有任何註解
/**
* Created by @author yihui in 19:17 19/11/21.
*/
public class RegisterBeanServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,IOException {
String name = req.getParameter("name");
PrintWriter writer = resp.getWriter();
writer.write("[RegisterBeanServlet] welcome " + name);
writer.flush();
writer.close();
}
}
複製程式碼
接下來我們需要定義一個ServletRegistrationBean
,讓它持有RegisterBeanServlet
的例項
@Bean
public ServletRegistrationBean servletBean() {
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.addUrlMappings("/register");
registrationBean.setServlet(new RegisterBeanServlet());
return registrationBean;
}
複製程式碼
測試請求輸出如下:
➜ ~ curl 'http://localhost:8080/register?name=yihuihui'
# 輸出結果
[RegisterBeanServlet] welcome yihuihui%
複製程式碼
3. ServletContext
這種姿勢,在實際的 Servlet 註冊中,其實用得並不太多,主要思路是在 ServletContext 初始化後,藉助javax.servlet.ServletContext#addServlet(java.lang.String,java.lang.Class<? extends javax.servlet.Servlet>)
方法來主動新增一個 Servlet
所以我們需要找一個合適的時機,獲取ServletContext
例項,並註冊 Servlet,在 SpringBoot 生態下,可以藉助ServletContextInitializer
ServletContextInitializer 主要被 RegistrationBean 實現用於往 ServletContext 容器中註冊 Servlet,Filter 或者 EventListener。這些 ServletContextInitializer 的設計目的主要是用於這些例項被 Spring IoC 容器管理
/**
* Created by @author yihui in 19:49 19/11/21.
*/
public class ContextServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,IOException {
String name = req.getParameter("name");
PrintWriter writer = resp.getWriter();
writer.write("[ContextServlet] welcome " + name);
writer.flush();
writer.close();
}
}
/**
* Created by @author yihui in 19:50 19/11/21.
*/
@Component
public class SelfServletConfig implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
ServletRegistration initServlet = servletContext.addServlet("contextServlet",ContextServlet.class);
initServlet.addMapping("/context");
}
}
複製程式碼
測試結果如下
➜ ~ curl 'http://localhost:8080/context?name=yihuihui'
# 輸出結果
[ContextServlet] welcome yihuihui%
複製程式碼
4. bean
接下來的這種註冊方式,並不優雅,但是也可以實現 Servlet 的註冊目的,但是有坑,請各位大佬謹慎使用
看過我的前一篇博文191016-SpringBoot 系列教程 web 篇之過濾器 Filter 使用指南的同學,可能會有一點映象,可以在 Filter 上直接新增@Component
註解,Spring 容器掃描 bean 時,會查詢所有實現 Filter 的子類,並主動將它包裝到FilterRegistrationBean
,實現註冊的目的
我們的 Servlet 是否也可以這樣呢?接下來我們實測一下
@Component
public class BeanServlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,IOException {
String name = req.getParameter("name");
PrintWriter writer = resp.getWriter();
writer.write("[BeanServlet1] welcome " + name);
writer.flush();
writer.close();
}
}
複製程式碼
現在問題來了,上面這個 Servlet 沒有定義 urlMapping 規則,怎麼請求呢?
為了確定上面的 Servlet 被註冊了,藉著前面 Filter 的原始碼分析的關鍵鏈路,我們找到了實際註冊的地方ServletContextInitializerBeans#addAsRegistrationBean
// org.springframework.boot.web.servlet.ServletContextInitializerBeans#addAsRegistrationBean(org.springframework.beans.factory.ListableBeanFactory,java.lang.Class<T>,java.lang.Class<B>,org.springframework.boot.web.servlet.ServletContextInitializerBeans.RegistrationBeanAdapter<T>)
@Override
public RegistrationBean createRegistrationBean(String name,Servlet source,int totalNumberOfSourceBeans) {
String url = (totalNumberOfSourceBeans != 1) ? "/" + name + "/" : "/";
if (name.equals(DISPATCHER_SERVLET_NAME)) {
url = "/"; // always map the main dispatcherServlet to "/"
}
ServletRegistrationBean<Servlet> bean = new ServletRegistrationBean<>(source,url);
bean.setName(name);
bean.setMultipartConfig(this.multipartConfig);
return bean;
}
複製程式碼
從上面的原始碼上可以看到,這個 Servlet 的 url 要麼是/
,要麼是/beanName/
接下來進行實測,全是 404
➜ ~ curl 'http://localhost:8080/?name=yihuihui'
{"timestamp":"2019-11-22T00:52:00.448+0000","status":404,"error":"Not Found","message":"No message available","path":"/"}%
➜ ~ curl 'http://localhost:8080/beanServlet1?name=yihuihui'
{"timestamp":"2019-11-22T00:52:07.962+0000","path":"/beanServlet1"}%
➜ ~ curl 'http://localhost:8080/beanServlet1/?name=yihuihui'
{"timestamp":"2019-11-22T00:52:11.202+0000","path":"/beanServlet1/"}%
複製程式碼
然後再定義一個 Servlet 時
@Component
public class BeanServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req,IOException {
String name = req.getParameter("name");
PrintWriter writer = resp.getWriter();
writer.write("[BeanServlet2] welcome " + name);
writer.flush();
writer.close();
}
}
複製程式碼
再次測試
➜ ~ curl 'http://localhost:8080/beanServlet1?name=yihuihui'
{"timestamp":"2019-11-22T00:54:12.692+0000","path":"/beanServlet1"}%
➜ ~ curl 'http://localhost:8080/beanServlet1/?name=yihuihui'
[BeanServlet1] welcome yihuihui%
➜ ~ curl 'http://localhost:8080/beanServlet2/?name=yihuihui'
[BeanServlet2] welcome yihuihui%
複製程式碼
從實際的測試結果可以看出,使用這種定義方式時,這個 servlet 相應的 url 為beanName + '/'
注意事項
然後問題來了,只定義一個 Servlet 的時候,根據前面的原始碼分析,這個 Servlet 應該會相應http://localhost:8080/
的請求,然而測試的時候為啥是 404?
這個問題也好解答,主要就是 Servlet 的優先順序問題,上面這種方式的 Servlet 的相應優先順序低於 Spring Web 的 Servelt 優先順序,相同的 url 請求先分配給 Spring 的 Servlet 了,為了驗證這個也簡單,兩步
- 先註釋
BeanServlet2
類上的註解@Component
- 在
BeanServlet1
的類上,新增註解@Order(-10000)
然後再次啟動測試,輸出如下
➜ ~ curl 'http://localhost:8080/?name=yihuihui'
[BeanServlet1] welcome yihuihui%
➜ ~ curl 'http://localhost:8080?name=yihuihui'
[BeanServlet1] welcome yihuihui%
複製程式碼
5. 小結
本文主要介紹了四種 Servlet 的註冊方式,至於 Servlet 的使用指南則靜待下篇
常見的兩種註冊 case:
-
@WebServlet
註解放在 Servlet 類上,然後啟動類上新增@ServletComponentScan
,確保 Serlvet3+的註解可以被 Spring 識別 - 將自定義 Servlet 例項委託給 bean
ServletRegistrationBean
不常見的兩種註冊 case:
- 實現介面
ServletContextInitializer
,通過ServletContext.addServlet
來註冊自定義 Servlet - 直接將 Serlvet 當做普通的 bean 註冊給 Spring
- 當專案中只有一個此種 case 的 servlet 時,它響應 url: '/',但是需要注意不指定優先順序時,預設場景下 Spring 的 Servlet 優先順序更高,所以它接收不到請求
- 當專案有多個此種 case 的 servlet 時,響應的 url 為
beanName + '/'
, 注意後面的'/'必須有
II. 其他
0. 專案
web 系列博文
- 191120-SpringBoot 系列教程 Web 篇之開啟 GZIP 資料壓縮
- 191018-SpringBoot 系列教程 web 篇之過濾器 Filter 使用指南擴充套件篇
- 191016-SpringBoot 系列教程 web 篇之過濾器 Filter 使用指南
- 191012-SpringBoot 系列教程 web 篇之自定義異常處理 HandlerExceptionResolver
- 191010-SpringBoot 系列教程 web 篇之全域性異常處理
- 190930-SpringBoot 系列教程 web 篇之 404、500 異常頁面配置
- 190929-SpringBoot 系列教程 web 篇之重定向
- 190913-SpringBoot 系列教程 web 篇之返回文字、網頁、圖片的操作姿勢
- 190905-SpringBoot 系列教程 web 篇之中文亂碼問題解決
- 190831-SpringBoot 系列教程 web 篇之如何自定義引數解析器
- 190828-SpringBoot 系列教程 web 篇之 Post 請求引數解析姿勢彙總
- 190824-SpringBoot 系列教程 web 篇之 Get 請求引數解析姿勢彙總
- 190822-SpringBoot 系列教程 web 篇之 Beetl 環境搭建
- 190820-SpringBoot 系列教程 web 篇之 Thymeleaf 環境搭建
- 190816-SpringBoot 系列教程 web 篇之 Freemaker 環境搭建
- 190421-SpringBoot 高階篇 WEB 之 websocket 的使用說明
- 190327-Spring-RestTemplate 之 urlencode 引數解析異常全程分析
- 190317-Spring MVC 之基於 java config 無 xml 配置的 web 應用構建
- 190316-Spring MVC 之基於 xml 配置的 web 應用構建
- 190213-SpringBoot 檔案上傳異常之提示 The temporary upload location xxx is not valid
專案原始碼
1. 一灰灰 Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個人部落格 blog.hhui.top
- 一灰灰 Blog-Spring 專題部落格 spring.hhui.top