文章内容
自定义一个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 |
四、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 |
五、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 |
六、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 | ➜ ~ curl 'http://localhost:8080/?name=Nick' {"timestamp":"2019-11-22T00:52:00.448+0000","status":404,"error":"Not Found","message":"No message available","path":"/"}% ➜ ~ curl 'http://localhost:8080/beanServlet1?name=Nick' {"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 | ➜ ~ curl 'http://localhost:8080/beanServlet1?name=Nick' {"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 | ➜ ~ curl 'http://localhost:8080/?name=Nick' [BeanServlet1] welcome Nick% ➜ ~ curl 'http://localhost:8080?name=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 + ‘/’, 注意后面的 ‘/’ 必须有