前面介紹了 java web 三要素中 filter 的使用指南與常見的易錯事項,接下來咱們來看一下 Servlet 的使用姿式,本篇主要帶來在 SpringBoot 環境下,註冊自定義的 Servelt 的四種姿式git
@WebServlet
註解ServletRegistrationBean
bean 定義ServletContext
動態添加<!-- more -->github
首先咱們須要搭建一個 web 工程,以方便後續的 servelt 註冊的實例演示,能夠經過 spring boot 官網建立工程,也能夠創建一個 maven 工程,在 pom.xml 中以下配置web
<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>
特別說明:spring
爲了緊跟 SpringBoot 的最新版本,從本篇文章開始,博文對應的示例工程中 SpringBoot 版本升級到2.2.1.RELEASE
bash
自定義一個 Servlet 比較簡單,通常常見的操做是繼承HttpServlet
,而後覆蓋doGet
, doPost
等方法便可;然而重點是咱們自定義的這些 Servlet 如何才能被 SpringBoot 識別並使用纔是關鍵,下面介紹四種註冊方式websocket
在自定義的 servlet 上添加 Servlet3+的註解@WebServlet
,來聲明這個類是一個 Servletapp
和 Fitler 的註冊方式同樣,使用這個註解,須要配合 Spring Boot 的@ServletComponentScan
,不然單純的添加上面的註解並不會生效curl
/** * 使用註解的方式來定義並註冊一個自定義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
;爲了讓上面的的註解生效,須要設置下啓動類socket
@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%
在 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%
這種姿式,在實際的 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%
接下來的這種註冊方式,並不優雅,可是也能夠實現 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%
本文主要介紹了四種 Servlet 的註冊方式,至於 Servlet 的使用指南則靜待下篇
常見的兩種註冊 case:
@WebServlet
註解放在 Servlet 類上,而後啓動類上添加@ServletComponentScan
,確保 Serlvet3+的註解能夠被 Spring 識別ServletRegistrationBean
不常見的兩種註冊 case:
ServletContextInitializer
,經過ServletContext.addServlet
來註冊自定義 ServletbeanName + '/'
, 注意後面的'/'必須有盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛