SpringBoot系列教程web篇Servlet 注冊的四種姿勢


原文: 191122-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, 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
    • 當項目中只有一個此種 case 的 servlet 時,它響應 url: '/', 但是需要注意不指定優先級時,默認場景下 Spring 的 Servlet 優先級更高,所以它接收不到請求
    • 當項目有多個此種 case 的 servlet 時,響應的 url 為beanName + '/', 注意后面的'/'必須有

II. 其他

0. 項目

web 系列博文

項目源碼

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛

一灰灰blog


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM