您现在的位置是:首页 > 技术教程 正文

SpringBoot系列教程web篇Servlet 注册的四种姿势

admin 阅读: 2024-04-01
后台-插件-广告管理-内容页头部广告(手机)

本篇主要带来在 SpringBoot 环境下,注册自定义的 Servelt 的四种姿势

  • @WebServlet 注解

  • ServletRegistrationBean bean 定义

  • ServletContext 动态添加

  • 普通的 spring bean 模式

I. 环境配置

1. 项目搭建

首先我们需要搭建一个 web 工程,以方便后续的 servelt 注册的实例演示,可以通过 spring boot 官网创建工程,也可以建立一个 maven 工程,在 pom.xml 中如下配置

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>2.2.1.RELEASE</version>
  5. <relativePath/><!-- lookup parent from repository -->
  6. </parent>
  7. <properties>
  8. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  9. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  10. <java.version>1.8</java.version>
  11. </properties>
  12. <dependencies>
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-web</artifactId>
  16. </dependency>
  17. </dependencies>
  18. <build>
  19. <pluginManagement>
  20. <plugins>
  21. <plugin>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-maven-plugin</artifactId>
  24. </plugin>
  25. </plugins>
  26. </pluginManagement>
  27. </build>
  28. <repositories>
  29. <repository>
  30. <id>spring-snapshots</id>
  31. <name>Spring Snapshots</name>
  32. <url>https://repo.spring.io/libs-snapshot-local</url>
  33. <snapshots>
  34. <enabled>true</enabled>
  35. </snapshots>
  36. </repository>
  37. <repository>
  38. <id>spring-milestones</id>
  39. <name>Spring Milestones</name>
  40. <url>https://repo.spring.io/libs-milestone-local</url>
  41. <snapshots>
  42. <enabled>false</enabled>
  43. </snapshots>
  44. </repository>
  45. <repository>
  46. <id>spring-releases</id>
  47. <name>Spring Releases</name>
  48. <url>https://repo.spring.io/libs-release-local</url>
  49. <snapshots>
  50. <enabled>false</enabled>
  51. </snapshots>
  52. </repository>
  53. </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,否则单纯的添加上面的注解并不会生效

  1. /**
  2. * 使用注解的方式来定义并注册一个自定义Servlet
  3. * Created by @author yihui in 19:08 19/11/21.
  4. */
  5. @WebServlet(urlPatterns = "/annotation")
  6. publicclass AnnotationServlet extends HttpServlet {
  7. @Override
  8. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  9. String name = req.getParameter("name");
  10. PrintWriter writer = resp.getWriter();
  11. writer.write("[AnnotationServlet] welcome " + name);
  12. writer.flush();
  13. writer.close();
  14. }
  15. }

上面是一个简单的测试 Servlet,接收请求参数name, 并返回 welcome xxx;为了让上面的的注解生效,需要设置下启动类

  1. @ServletComponentScan
  2. @SpringBootApplication
  3. publicclass Application {
  4. public static void main(String[] args) {
  5. SpringApplication.run(Application.class);
  6. }
  7. }

然后启动测试,输出结果如:

  1. ➜ ~ curl http://localhost:8080/annotation\?name\=yihuihui
  2. # 输出结果
  3. [AnnotationServlet] welcome yihuihui%

2. ServletRegistrationBean

在 Filter 的注册中,我们知道有一种方式是定义一个 Spring 的 Bean FilterRegistrationBean来包装我们的自定义 Filter,从而让 Spring 容器来管理我们的过滤器;同样的在 Servlet 中,也有类似的包装 bean: ServletRegistrationBean

自定义的 bean 如下,注意类上没有任何注解

  1. /**
  2. * Created by @author yihui in 19:17 19/11/21.
  3. */
  4. publicclass RegisterBeanServlet extends HttpServlet {
  5. @Override
  6. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  7. String name = req.getParameter("name");
  8. PrintWriter writer = resp.getWriter();
  9. writer.write("[RegisterBeanServlet] welcome " + name);
  10. writer.flush();
  11. writer.close();
  12. }
  13. }

接下来我们需要定义一个ServletRegistrationBean,让它持有RegisterBeanServlet的实例

  1. @Bean
  2. public ServletRegistrationBean servletBean() {
  3. ServletRegistrationBean registrationBean = new ServletRegistrationBean();
  4. registrationBean.addUrlMappings("/register");
  5. registrationBean.setServlet(new RegisterBeanServlet());
  6. return registrationBean;
  7. }

测试请求输出如下:

  1. ➜ ~ curl 'http://localhost:8080/register?name=yihuihui'
  2. # 输出结果
  3. [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 容器管理

  1. /**
  2. * Created by @author yihui in 19:49 19/11/21.
  3. */
  4. publicclass ContextServlet extends HttpServlet {
  5. @Override
  6. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  7. String name = req.getParameter("name");
  8. PrintWriter writer = resp.getWriter();
  9. writer.write("[ContextServlet] welcome " + name);
  10. writer.flush();
  11. writer.close();
  12. }
  13. }
  14. /**
  15. * Created by @author yihui in 19:50 19/11/21.
  16. */
  17. @Component
  18. publicclass SelfServletConfig implements ServletContextInitializer {
  19. @Override
  20. public void onStartup(ServletContext servletContext) throws ServletException {
  21. ServletRegistration initServlet = servletContext.addServlet("contextServlet", ContextServlet.class);
  22. initServlet.addMapping("/context");
  23. }
  24. }

测试结果如下

  1. ➜ ~ curl 'http://localhost:8080/context?name=yihuihui'
  2. # 输出结果
  3. [ContextServlet] welcome yihuihui%

4. bean

接下来的这种注册方式,并不优雅,但是也可以实现 Servlet 的注册目的,但是有坑,请各位大佬谨慎使用

看过我的前一篇博文191016-SpringBoot 系列教程 web 篇之过滤器 Filter 使用指南的同学,可能会有一点映象,可以在 Filter 上直接添加@Component注解,Spring 容器扫描 bean 时,会查找所有实现 Filter 的子类,并主动将它包装到FilterRegistrationBean,实现注册的目的

我们的 Servlet 是否也可以这样呢?接下来我们实测一下

  1. @Component
  2. publicclass BeanServlet1 extends HttpServlet {
  3. @Override
  4. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  5. String name = req.getParameter("name");
  6. PrintWriter writer = resp.getWriter();
  7. writer.write("[BeanServlet1] welcome " + name);
  8. writer.flush();
  9. writer.close();
  10. }
  11. }

现在问题来了,上面这个 Servlet 没有定义 urlMapping 规则,怎么请求呢?

为了确定上面的 Servlet 被注册了,借着前面 Filter 的源码分析的关键链路,我们找到了实际注册的地方ServletContextInitializerBeans#addAsRegistrationBean

  1. // 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>)
  2. @Override
  3. public RegistrationBean createRegistrationBean(String name, Servlet source, int totalNumberOfSourceBeans) {
  4. String url = (totalNumberOfSourceBeans != 1) ? "/" + name + "/" : "/";
  5. if (name.equals(DISPATCHER_SERVLET_NAME)) {
  6. url = "/"; // always map the main dispatcherServlet to "/"
  7. }
  8. ServletRegistrationBean<Servlet> bean = new ServletRegistrationBean<>(source, url);
  9. bean.setName(name);
  10. bean.setMultipartConfig(this.multipartConfig);
  11. return bean;
  12. }

从上面的源码上可以看到,这个 Servlet 的 url 要么是/, 要么是/beanName/

接下来进行实测,全是 404

  1. ➜ ~ curl 'http://localhost:8080/?name=yihuihui'
  2. {"timestamp":"2019-11-22T00:52:00.448+0000","status":404,"error":"Not Found","message":"No message available","path":"/"}%
  3. ➜ ~ curl 'http://localhost:8080/beanServlet1?name=yihuihui'
  4. {"timestamp":"2019-11-22T00:52:07.962+0000","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1"}%
  5. ➜ ~ curl 'http://localhost:8080/beanServlet1/?name=yihuihui'
  6. {"timestamp":"2019-11-22T00:52:11.202+0000","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1/"}%

然后再定义一个 Servlet 时

  1. @Component
  2. publicclass BeanServlet2 extends HttpServlet {
  3. @Override
  4. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  5. String name = req.getParameter("name");
  6. PrintWriter writer = resp.getWriter();
  7. writer.write("[BeanServlet2] welcome " + name);
  8. writer.flush();
  9. writer.close();
  10. }
  11. }

再次测试

  1. ➜ ~ curl 'http://localhost:8080/beanServlet1?name=yihuihui'
  2. {"timestamp":"2019-11-22T00:54:12.692+0000","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1"}%
  3. ➜ ~ curl 'http://localhost:8080/beanServlet1/?name=yihuihui'
  4. [BeanServlet1] welcome yihuihui%
  5. ➜ ~ curl 'http://localhost:8080/beanServlet2/?name=yihuihui'
  6. [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)

然后再次启动测试,输出如下

  1. ➜ ~ curl 'http://localhost:8080/?name=yihuihui'
  2. [BeanServlet1] welcome yihuihui%
  3. ➜ ~ curl 'http://localhost:8080?name=yihuihui'
  4. [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 + '/', 注意后面的'/'必须有

标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索