SpringBoot中Servlet注册的四种方式

自定义一个Servlet比较简单,一般常见的操作是继承HttpServlet,然后覆盖doGet,doPost等方法即可;然而重点是自定义的这些 Servlet 如何才能被 SpringBoot 识别并使用才是关键。

一、Servelt注册的四种方式

在 SpringBoot 环境下,注册自定义的 Servelt的四种方式:

  • @WebServlet 注解
  • ServletRegistrationBean bean 定义
  • ServletContext 动态添加
  • 普通的 spring bean 模式

二、Maven依赖

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

三、@WebServlet

在自定义的 servlet 上添加 Servlet3+的注解@WebServlet,来声明这个类是一个 Servlet 和 Fitler 的注册方式一样,使用这个注解,需要配合 Spring Boot 的 @ServletComponentScan,否则单纯的添加上面的注解并不会生效。

1、定义Servlet

01
02
03
04
05
06
07
08
09
10
11
12
13
/**
 * 使用注解的方式来定义并注册一个自定义Servlet
 */@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();
    }
}

2、设置启动类

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

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

3、启动测试

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

1
2
3
# 输出结果
[AnnotationServlet] welcome Nick%

四、ServletRegistrationBean

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

1、定义Servlet

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

01
02
03
04
05
06
07
08
09
10
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();
    }
}

2、定义ServletRegistrationBean

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

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

3、启动测试

测试请求输出如下:

1
2
3
# 输出结果
[RegisterBeanServlet] welcome Nick%

五、ServletContext

在实际的 Servlet 注册中,其实用得并不太多,主要思路是在 ServletContext 初始化后,借助 javax.servlet.ServletContext#addServlet(java.lang.String, java.lang.Class) 方法来主动添加一个 Servlet。所以需要找一个合适的时机,获取ServletContext实例,并注册 Servlet,在 SpringBoot 生态下,可以借助ServletContextInitializer。

ServletContextInitializer 主要被 RegistrationBean 实现用于往 ServletContext 容器中注册 Servlet,Filter 或者 EventListener。这些 ServletContextInitializer 的设计目的主要是用于这些实例被 Spring IoC 容器管理

1、定义Servlet

01
02
03
04
05
06
07
08
09
10
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();
    }
}

2、定义ServletContextInitializer

1
2
3
4
5
6
7
8
@Component
public class SelfServletConfig implements ServletContextInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        ServletRegistration initServlet = servletContext.addServlet("contextServlet", ContextServlet.class);
        initServlet.addMapping("/context");
    }
}

3、启动测试

测试结果如下:

1
2
3
# 输出结果
[ContextServlet] welcome Nick%

六、bean

接下来的这种注册方式,并不优雅,但是也可以实现 Servlet 的注册目的,但是有坑,谨慎使用。在 Filter 上直接添加@Component注解,Spring 容器扫描 bean 时,会查找所有实现 Filter 的子类,并主动将它包装到FilterRegistrationBean,实现注册的目的。Servlet 是否也可以这样呢?

1、定义Servlet

01
02
03
04
05
06
07
08
09
10
11
@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 规则,怎么请求呢?

2、源码分析

为了确定上面的 Servlet 被注册了,找到实际注册的地方ServletContextInitializerBeans#addAsRegistrationBean:

01
02
03
04
05
06
07
08
09
10
11
12
// 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/

3、启动测试

接下来进行实测,全是 404

1
2
3
4
5
6
{"timestamp":"2019-11-22T00:52:00.448+0000","status":404,"error":"Not Found","message":"No message available","path":"/"}%
{"timestamp":"2019-11-22T00:52:07.962+0000","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1"}%
{"timestamp":"2019-11-22T00:52:11.202+0000","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1/"}%

4、再定义一个 Servlet

然后再定义一个 Servlet

01
02
03
04
05
06
07
08
09
10
11
@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();
    }
}

5、再次测试

1
2
3
4
5
6
{"timestamp":"2019-11-22T00:54:12.692+0000","status":404,"error":"Not Found","message":"No message available","path":"/beanServlet1"}%
[BeanServlet1] welcome Nick%
[BeanServlet2] welcome Nick%

从实际的测试结果可以看出,使用这种定义方式时,这个 servlet 相应的 url 为beanName + ‘/’

6、注意事项

然后问题来了:

只定义一个 Servlet 的时候,根据前面的源码分析,这个 Servlet 应该会相应http://localhost:8080/的请求,然而测试的时候为啥是 404?

这个问题也好解答,主要就是 Servlet 的优先级问题,上面这种方式的 Servlet 的相应优先级低于 Spring Web 的 Servelt 优先级,相同的 url 请求先分配给 Spring 的 Servlet 了,为了验证这个也简单,两步:

  • 先注释BeanServlet2类上的注解@Component
  • 在BeanServlet1的类上,添加注解@Order(-10000)

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

1
2
3
4
[BeanServlet1] welcome Nick%
[BeanServlet1] welcome Nick%

七、总结

本文主要介绍了四种 Servlet 的注册方式。

1、常见的两种注册Case

  • @WebServlet注解放在 Servlet 类上,然后启动类上添加@ServletComponentScan,确保 Serlvet3+的注解可以被 Spring 识别
  • 将自定义 Servlet 实例委托给 bean ServletRegistrationBean

2、不常见的两种注册Case

  • 实现接口ServletContextInitializer,通过ServletContext.addServlet来注册自定义 Servlet
  • 直接将 Serlvet 当做普通的 bean 注册给 Spring
    • 当项目中只有一个此种 case 的 servlet 时,它响应 url:’/’,但是需要注意不指定优先级时,默认场景下 Spring 的 Servlet 优先级更高,所以它接收不到请求
    • 当项目有多个此种 case 的 servlet 时,响应的 url 为 beanName + ‘/’, 注意后面的 ‘/’ 必须有

发表评论

欢迎阅读『SpringBoot中Servlet注册的四种方式|Java、Spring Boot|Nick Tan-梓潼Blog』