前面介绍了 java web 三要素中 filter 的使用指南与常见的易错事项,接下来咱们来看一下 Servlet 的使用姿式,本篇主要带来在 SpringBoot 环境下,注册自定义的 Servelt 的四种姿式git
@WebServlet
注解ServletRegistrationBean
bean 定义ServletContext
动态添加首先咱们须要搭建一个 web 工程,以方便后续的 servelt 注册的实例演示,能够经过 spring boot 官网建立工程,也能够创建一个 maven 工程,在 pom.xml 中以下配置github
<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>
复制代码
特别说明:web
为了紧跟 SpringBoot 的最新版本,从本篇文章开始,博文对应的示例工程中 SpringBoot 版本升级到2.2.1.RELEASE
spring
自定义一个 Servlet 比较简单,通常常见的操做是继承HttpServlet
,而后覆盖doGet
, doPost
等方法便可;然而重点是咱们自定义的这些 Servlet 如何才能被 SpringBoot 识别并使用才是关键,下面介绍四种注册方式bash
在自定义的 servlet 上添加 Servlet3+的注解@WebServlet
,来声明这个类是一个 Servletwebsocket
和 Fitler 的注册方式同样,使用这个注解,须要配合 Spring Boot 的@ServletComponentScan
,不然单纯的添加上面的注解并不会生效app
/** * 使用注解的方式来定义并注册一个自定义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
;为了让上面的的注解生效,须要设置下启动类curl
@ServletComponentScan
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
复制代码
而后启动测试,输出结果如:socket
➜ ~ curl http://localhost:8080/annotation\?name\=yihuihui
# 输出结果
[AnnotationServlet] welcome yihuihui%
复制代码
在 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%
复制代码
这种姿式,在实际的 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%
复制代码
接下来的这种注册方式,并不优雅,可是也能够实现 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%
复制代码
本文主要介绍了四种 Servlet 的注册方式,至于 Servlet 的使用指南则静待下篇
常见的两种注册 case:
@WebServlet
注解放在 Servlet 类上,而后启动类上添加@ServletComponentScan
,确保 Serlvet3+的注解能够被 Spring 识别ServletRegistrationBean
不常见的两种注册 case:
ServletContextInitializer
,经过ServletContext.addServlet
来注册自定义 ServletbeanName + '/'
, 注意后面的'/'必须有尽信书则不如,以上内容,纯属一家之言,因我的能力有限,不免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的我的博客,记录全部学习和工做中的博文,欢迎你们前去逛逛