原文: 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 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

SpringBoot系列教程web篇Servlet 注册的四种姿势的更多相关文章

  1. SpringBoot系列教程web篇之Post请求参数解析姿势汇总

    作为一个常年提供各种Http接口的后端而言,如何获取请求参数可以说是一项基本技能了,本篇为<190824-SpringBoot系列教程web篇之Get请求参数解析姿势汇总>之后的第二篇,对 ...

  2. SpringBoot系列教程web篇之Get请求参数解析姿势汇总

    一般在开发web应用的时候,如果提供http接口,最常见的http请求方式为GET/POST,我们知道这两种请求方式的一个显著区别是GET请求的参数在url中,而post请求可以不在url中:那么一个 ...

  3. SpringBoot系列教程web篇Listener四种注册姿势

    java web三要素Filter, Servlet前面分别进行了介绍,接下来我们看一下Listener的相关知识点,本篇博文主要内容为SpringBoot环境下,如何自定义Listener并注册到s ...

  4. SpringBoot系列教程web篇之过滤器Filter使用指南扩展篇

    前面一篇博文介绍了在 SpringBoot 中使用 Filter 的两种使用方式,这里介绍另外一种直接将 Filter 当做 Spring 的 Bean 来使用的方式,并且在这种使用方式下,Filte ...

  5. SpringBoot系列教程web篇之过滤器Filter使用指南

    web三大组件之一Filter,可以说是很多小伙伴学习java web时最早接触的知识点了,然而学得早不代表就用得多.基本上,如果不是让你从0到1写一个web应用(或者说即便从0到1写一个web应用) ...

  6. SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver

    关于Web应用的全局异常处理,上一篇介绍了ControllerAdvice结合@ExceptionHandler的方式来实现web应用的全局异常管理: 本篇博文则带来另外一种并不常见的使用方式,通过实 ...

  7. SpringBoot系列教程web篇之重定向

    原文地址: SpringBoot系列教程web篇之重定向 前面介绍了spring web篇数据返回的几种常用姿势,当我们在相应一个http请求时,除了直接返回数据之外,还有另一种常见的case -&g ...

  8. SpringBoot系列教程web篇之如何自定义参数解析器

    title: 190831-SpringBoot系列教程web篇之如何自定义参数解析器 banner: /spring-blog/imgs/190831/logo.jpg tags: 请求参数 cat ...

  9. SpringBoot系列教程Web篇之开启GZIP数据压缩

    本篇可以归纳在性能调优篇,虽然内容非常简单,但效果可能出乎预料的好: 分享一个真实案例,我们的服务部署在海外,国内访问时访问服务时,响应有点夸张:某些返回数据比较大的接口,耗时在 600ms+上,然而 ...

随机推荐

  1. c#对象深复制demo

    public class Person : ICloneable { public string Name; object ICloneable.Clone() { return this.Clone ...

  2. docker基础应用

    环境: centos 7  docker 19.03.0-beta3  192.168.10.10 关于如何安装docker请参考:https://www.cnblogs.com/caesar-id/ ...

  3. javascript(六)运算符

    运算符概述 JavaScript中的运算符用于算术表达式. 比较表达式. 逻辑表达式. 赋值表达式等.需要注意的是, 大多数运算符都是由标点符号表示的, 比如 "+" 和" ...

  4. Vue项目运行或打包时,频繁内存溢出情况CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

    前端使用基于vue的Nuxt框架,但是随着项目功能增多,项目变大,频繁出现此种情况,原因是项目太大,导致内存溢出,排除代码问题外,可参照以下方式解决 解决方案 1.全局安装increase-memor ...

  5. APS系统帮助寻找企业最优库存

    零库存模式的实施要有深厚的民族文化和企业文化为支点.随着对零库存管理研究的深入,就会发现它不仅仅是一种运营管理技术,更是一种文化.一种哲学. 当这种认同文化体现在企业与企业之间时,就会表现出彼此的认同 ...

  6. “You don't have permission to access /phpmyadmin/ on this server.”

    <Directory "I:/1/wamp/apps/phpmyadmin3.4.10.1/"> Options Indexes FollowSymLinks Mult ...

  7. 6.redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?

    作者:中华石杉 面试题 redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的? 面试官心理分析 redis 如果仅仅只是将数据缓存在内存里面,如果 redi ...

  8. Django框架(十二)-- Djang与Ajax

    一.什么是Ajax AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Javascript和XML”.即使用Javascript语言与服务器进行异步交互,传 ...

  9. Linux shell简单创建用户脚本

    前面介绍简单的shell编写规则. 现在开始编写一个简单的shell脚本. Linux shell介绍 编写shell脚本    1.创建脚本文件    2.根据需求,编写脚本    3.测试执行脚本 ...

  10. ThinkPHP5框架引入的css等外部资源文件没有生效

    静态资源文件一般是放在public目录里,不只是css,只要是静态资源文件都没有显示出来. (更好的阅读体验可访问 这里 ) 问题陈述 文件结构 文件内容 三个文件分别为:Index.php.test ...