191122-SpringBoot系列教程web篇Servlet 註冊的四種姿勢
前面介紹了java web三要素中filter的使用指南與常見的易錯事項,接下來我們來看一下Servlet的使用姿勢,本篇主要帶來在SpringBoot環境下,註冊自定義的Servelt的四種姿勢
@WebServlet ServletRegistrationBean ServletContext
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, HttpServletResponse resp) throws ServletException, 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, HttpServletResponse resp) throws ServletException, 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, HttpServletResponse resp) throws ServletException, 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","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1"}% ➜ ~ curl 'http://localhost:8080/beanServlet1/?name=yihuihui' {"timestamp":"2019-11-22T00:52:11.202+0000","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1/"}%
然後再定義一個Servlet時
@Component public class BeanServlet2 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, 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","status":404,"error":"Not Found","message":"No message available","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
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
項目源碼
- 工程: https://github.com/liuyueyi/spring-boot-demo
- 項目: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/211-web-servlet
1. 一灰灰Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰Blog個人博客 https://blog.hhui.top
- 一灰灰Blog-Spring專題博客 http://spring.hhui.top
打賞 如果覺得我的文章對您有幫助,請隨意打賞。