@ServletComponentScan
源码如下:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({ServletComponentScanRegistrar.class}) public @interface ServletComponentScan { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; } 复制代码
@ServletComponentScan 注解中比较关键的是引用了 ServletComponentScanRegistrar 类,该类是 ImportBeanDefinitionRegistrar 接口的实现类,会被 Spring 容器所解析。 ServletComponentScanRegistrar 内部会解析 @ServletComponentScan 注解,然后会在 Spring 容器中注册 ServletComponentRegisteringPostProcessor,是个 BeanFactoryPostProcessor,会去解析扫描出来的类是不是有 @WebServlet、@WebListener、@WebFilter 这三种注解,有的话会把这三种类型的类转换成 ServletRegistrationBean、FilterRegistrationBean 或者ServletListenerRegistrationBean,然后让 Spring 容器去解析。
在 SpringBootApplication 上使用@ServletComponentScan 注解后,Servlet、Filter、Listener 可以直接通过@WebServlet、@WebFilter、@WebListener 注解自动注册,无需其他代码。
上述内容即为@ServletComponentScan 注解的学习归纳,接下来我们主要介绍如何在 Spring Boot 中添加 Servlet、Filter、Listener。
刚才有提到 @WebServlet、@WebFilter、@WebListener 这三个注解,其实是 Servlet3.0 中的新增的注解。
Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中有几项特性的引入让开发者感到非常兴奋,同时也获得了 Java 社区的一片赞誉之声:
- 异步处理支持:在 Servlet3.0之前,Servlet 线程需要一直阻塞,直到业务处理完毕才能再输出响应,最后才结束 Servlet 线程。而 Servlet3.0 中,在接收到请求之后,Servlet 线程可以将耗时的操作委托给另一个线程来完成,自己在不生成响应的情况下返回至容器。针对业务处理较耗时的情况,这将大大减少服务器资源的占用,并且提高并发处理速度。
- 新增的注解支持:该版本新增了若干注解,用于简化 Servlet、过滤器(Filter)和监听器(Listener)的声明,这使得 web.xml 部署描述文件从该版本开始不再是必选的了。
- 可插性支持:熟悉 Struts2 的开发者一定会对其通过插件的方式与包括 Spring 在内的各种常用框架的整合特性记忆犹新。将相应的插件封装成 JAR 包并放在类路径下,Struts2 运行时便能自动加载这些插件。现在 Servlet3.0 提供了类似的特性,开发者可以通过插件的方式很方便的扩充已有 Web 应用的功能,而不需要修改原有的应用。
关于 Servlet3.0 新特性的介绍可以参考:Servlet 3.0 新特性详解
Servlet
在 Servlet3.0 之前,我们只能从 web.xml 文件中配置 Servlet。web.xml 是 Java EE 中可选择用来描述应用部署的文件,使得 Servlet 容器可以加载部署应用,该文件可以用于声明 Servlet,Servlet 的访问映射,配置监听器等信息,描述外部资源。
例如,Tomcat 在启动部署一个 Web 应用的时候,会在初始化阶段加载 web.xml 文件,进而加载 Servlet,加载 Servlet 与 Api 的映射关系,最终才能对外提供服务。在这个阶段,我们每次开发新的功能,增加新的 Servlet 都需要修改 web.xml 文件,配置起来比较繁琐。
在 Servlet3.0 之后,创建 Servlet 可以有以下方法:
- 首先自定义类继承 HttpServlet,然后代码注册通过 ServletRegistrationBean 获得控制。也可以通过实现 ServletContextInitializer 接口直接实现;
- 在 SpringBootApplication 上使用@ServletComponentScan 注解后,Servlet可以直接通过 @WebServlet 注解自动注册,无需其他代码。
通过代码注册Servlet
Spring Boot 启动类中声明 ServletRegistrationBean。
servlet 类:
public class MyServlet extends HttpServlet { private static final long serialVersionUID = -8685285401859800066L; @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(">>>>>>>>>>service()<<<<<<<<<<<"); resp.getWriter().println("receive by MyServlet"); } } 复制代码
先定义一个 Servlet,重写 service 实现自己的业务逻辑。
Spring Boot 启动类:
@SpringBootApplication public class SpbGuide3Application { @Bean public ServletRegistrationBean servletRegistrationBean(){ return new ServletRegistrationBean(new MyServlet(),"/servlet/*"); } public static void main(String[] args) { SpringApplication.run(SpbGuide3Application.class, args); } } 复制代码
然后通过@Bean 注解往 Spring 容器中注入一个 ServletRegistrationBean 类型的 bean 实例,并且实例化一个自定义的 Servlet 作为参数,这样就将自定义的 Servlet 加入 Tomcat 中了。
运行该项目,打开网址: http://localhost:8080/servlet
刚才有提到还可以通过实现 ServletContextInitializer 接口直接实现,如下所示:
Servlet 类:
package com.example.servlet; public class MyServlet2 extends HttpServlet { private static final long serialVersionUID = -8685285401859800066L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(">>>>>>>>>>doGet()<<<<<<<<<<<"); doPost(req,resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(">>>>>>>>>>doPost()<<<<<<<<<<<"); resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello World</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>MyServlet2 doPost()</h1>"); out.println("</body>"); out.println("</html>"); } } 复制代码
HttpServlet 中有三个比较重要的方法,service()、doGet()和 doPost()方法,在 servlet 中默认情况下,无论你是 get 还是 post 提交过来,都会经过 service()方法来处理,然后转向到 doGet 或 doPost 方法。当自定义的 servlet 类继承 HttpServlet 时,如果不覆盖重写 service 方法,就只需要重写 doGet 或者 doPost 方法。
实现自定义 ServletContainerInitializer 配置加载
public class MyServletContainerInitializer implements ServletContextInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { System.out.println("启动加载自定义的MyServletContainerInitializer"); ServletRegistration.Dynamic registration = servletContext.addServlet("servlet2","com.example.servlet.MyServlet2"); registration.setLoadOnStartup(1); registration.addMapping("/servlet2"); } } 复制代码
通过查看 ServletRegistrationBean 类的源码可知,该类继承了 RegistrationBean,而 RegistrationBean 又实现了 ServletContextInitializer 这个接口,并且有一个 onStartup 方法,所以我们可以自定义 ServletContextInitializer 接口实现类,来替代 ServletRegistrationBean。
ServletContextInitializer
是 Servlet 容器初始化的时候,提供的初始化接口。所以,Servlet 容器初始化会获取并触发所有的`FilterRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean 实例中 onStartup 方法
Spring Boot 启动类:
@SpringBootApplication public class SpbGuide3Application { /** * 使用代码注册Servlet(不需要@ServletComponentScan注解) * @return */ @Bean public MyServletContainerInitializer servletContainerInitializer(){ return new MyServletContainerInitializer(); } public static void main(String[] args) { SpringApplication.run(SpbGuide3Application.class, args); } } 复制代码
运行该项目,打开网址: http://localhost:8080/servlet2
使用注解注册Servlet
下面是@WebServlet 的属性列表。
属性名 | 类型 | 描述 |
name | String | 指定Servlet的name属性,等价于,如果没有显示指定,则该Servlet的取值即为类的全限定名。 |
value | String[] | 该属性等价于urlPatterns属性。两个属性不能同时使用。 |
urlPatterns | String[] | 指定一组Servlet的URL匹配模式,等价于标签。 |
loadOnStartup | int | 指定 Servlet 的加载顺序,等价于 标签。 |
initParams | WebInitParam[] | 指定一组 Servlet 初始化参数,等价于标签。 |
asyncSupported | boolean | 声明 Servlet 是否支持异步操作模式,等价于 标签。 |
description | String | 该 Servlet 的描述信息,等价于 标签。 |
displayName | String | 该 Servlet 的显示名,通常配合工具使用,等价于 标签。 |
从上表可见,web.xml 可以配置的 servlet 属性,在@WebServlet 中都可以配置。
在 Spring Boot 中,嵌入式 Servlet 容器通过扫描注解的方式注册 Servlet、Filter 和 Servlet 规范的所有监听器(如 HttpSessionListener 监听器)。
Servlet 类:
@WebServlet(urlPatterns = "/servlet/web", description = "用注解声明Servlet", initParams = {//以下都是druid数据源状态监控的参数 @WebInitParam(name = "allow",value = ""),// IP白名单 (没有配置或者为空,则允许所有访问) @WebInitParam(name = "deny",value = ""),// IP黑名单 (存在共同时,deny优先于allow) @WebInitParam(name = "loginUsername",value = "hresh"),// 用户名 @WebInitParam(name = "loginPassword",value = "123456"),// 密码 @WebInitParam(name = "resetEnable",value = "false")// 禁用HTML页面上的“Reset All”功能 }) public class MyWebServlet extends HttpServlet { private static final long serialVersionUID = -8685285401859800066L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(">>>>>>>>>>MyWebServlet doGet()<<<<<<<<<<<"); ServletConfig config = getServletConfig(); System.out.println(config.getInitParameter("loginUsername")); System.out.println(config.getInitParameter("loginPassword")); doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(">>>>>>>>>>MyWebServlet doPost()<<<<<<<<<<<"); resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Hello World</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>MyServlet2 doPost()</h1>"); out.println("</body>"); out.println("</html>"); } } 复制代码
Spring Boot 启动类:
@SpringBootApplication @ServletComponentScan public class SpbGuide3Application { public static void main(String[] args) { SpringApplication.run(SpbGuide3Application.class, args); } } 复制代码
运行该项目,打开网址: http://localhost:8080/servlet/web
其中输出的 loginUsername 和 loginPassword 是我们在@WebServlet 注解中设置的。
Filter
使用@WebFilter 注解
@WebFilter 的常用属性
属性名 | 类型 | 描述 |
filterName | String | 指定过滤器的name属性,等价于, |
value | String[] | 该属性等价于urlPatterns属性。两个属性不能同时使用。 |
urlPatterns | String[] | 指定一组过滤器的URL匹配模式,等价于标签。 |
dispatcherTypes | DispatcherType | 指定过滤器的转发模式,具体取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST。 |
initParams | WebInitParam[] | 指定一组过滤器初始化参数,等价于标签。 |
asyncSupported | boolean | 声明过滤器是否支持异步操作模式,等价于 标签。 |
description | String | 该过滤器的描述信息,等价于 标签。 |
displayName | String | 该过滤器的显示名,通常配合工具使用,等价于 标签。 |
使用@WebFilter 注解的时候发现注解里面没有提供可以控制执行顺序的参数 , 通过实践发现如果想要控制 filter的执行顺序可以通过控制 filter 的文件名来控制 。比如:UserLoginFilter.java 和 ApiLog.java 这两个过滤器文件,因为这两个文件的首字母A排U之前,导致每次执行的时候都是先执行 ApiLog 过滤器再执行 UserLoginFilter 过滤器,所以我们现在修改两个文件的名称分别为 Filter0_UserLogin.java 和 Filter1_ApiLog.java,就会先执行 Filter0_UserLogin 再执行 Filter1_ApiLog。
示例:
Filter 类:
@WebFilter(filterName = "MyFilterWithAnnotation",urlPatterns = "/api/*", initParams = {//以下都是druid数据源状态监控的参数 @WebInitParam(name = "allow", value = ""),// IP白名单 (没有配置或者为空,则允许所有访问) @WebInitParam(name = "deny", value = ""),// IP黑名单 (存在共同时,deny优先于allow) @WebInitParam(name = "loginUsername", value = "hresh"),// 用户名 @WebInitParam(name = "loginPassword", value = "123456"),// 密码 @WebInitParam(name = "resetEnable", value = "false")// 禁用HTML页面上的“Reset All”功能 }) public class MyFilterWithAnnotation implements Filter { private static final Logger logger = LoggerFactory.getLogger(MyFilterWithAnnotation.class); @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("初始化MyFilterWithAnnotation过滤器:", filterConfig.getFilterName()); System.out.println(filterConfig.getInitParameter("loginUsername")); System.out.println(filterConfig.getInitParameter("loginPassword")); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { logger.info("过滤器开始对请求进行预处理:"); HttpServletRequest request = (HttpServletRequest) servletRequest; String requestUri = request.getRequestURI(); System.out.println("请求的接口为:" + requestUri); long startTime = System.currentTimeMillis(); //通过 doFilter 方法实现过滤功能 filterChain.doFilter(servletRequest,servletResponse); // 上面的 doFilter 方法执行结束后用户的请求已经返回 long endTime = System.currentTimeMillis(); System.out.println("该用户的请求已经处理完毕,请求花费的时间为:" + (endTime - startTime)); } @Override public void destroy() { logger.info("销毁过滤器MyFilterWithAnnotation"); } } 复制代码
自定义 Controller 验证过滤器
@RestController @RequestMapping("/api") public class MyController { @GetMapping("/hello") public String getHello() throws InterruptedException { Thread.sleep(1000); return "hello"; } } 复制代码
Spring Boot 启动类:
@SpringBootApplication @ServletComponentScan public class SpbGuide3Application { public static void main(String[] args) { SpringApplication.run(SpbGuide3Application.class, args); } } 复制代码
运行该项目,打开网址: http://localhost:8080/api/hello
Filter 的创建和销毁由 Web 服务器负责,Web 应用程序启动时,Web 服务器将创建 Filter 的实例对象,并调用其 init 方法,完成对象的初始化功能,从而为后续的用户请求做好拦截的准备工作,filter 对象只会创建一次,即 init 方法只会执行一次。通过 init 方法的参数,可获得代表当前 filter 配置信息的 FilterConfig 对象。同样 Filter 对象的销毁由 destroy 方法执行,进而释放过滤器使用的资源。
继承 HttpFilter
结合上述注册 Servlet 的案例,我们不由得想到是否可以通过自定义类继承 HttpFilter,重写 doFilter方法,具体代码如下:
Filter 类:
@Component public class MyFilter3 extends HttpFilter { private static final Logger logger = LoggerFactory.getLogger(MyFilterWithAnnotation.class); @Override public void init() throws ServletException { FilterConfig filterConfig = this.getFilterConfig(); logger.info("初始化MyFilter3过滤器:", filterConfig.getFilterName()); } @Override public void destroy() { logger.info("销毁过滤器MyFilterWithAnnotation"); } @Override protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String requestUri = request.getRequestURI(); System.out.println("进入MyFilter3 过滤器"); System.out.println("请求的接口为:" + requestUri); long startTime = System.currentTimeMillis(); //通过 doFilter 方法实现过滤功能 super.doFilter(request, response, chain); // 上面的 doFilter 方法执行结束后用户的请求已经返回 long endTime = System.currentTimeMillis(); System.out.println("该用户的请求已经处理完毕,请求花费的时间为:" + (endTime - startTime)); } } 复制代码
Spring Boot 启动类:
@SpringBootApplication @ServletComponentScan public class SpbGuide3Application { public static void main(String[] args) { SpringApplication.run(SpbGuide3Application.class, args); } } 复制代码
与 servlet 不同,不需要在 Spring Boot 启动类中添加 FilterRegistrationBean
运行该项目,打开网址: http://localhost:8080/api/hello
实现 Filter 接口
定义两个 Filter 类,用来实现 filter 排序功能。
@Component public class MyFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(MyFilter.class); @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("初始化MyFilter过滤器:", filterConfig.getFilterName()); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { logger.info("过滤器开始对请求进行预处理:"); HttpServletRequest request = (HttpServletRequest) servletRequest; String requestUri = request.getRequestURI(); System.out.println("请求的接口为:" + requestUri); long startTime = System.currentTimeMillis(); //通过 doFilter 方法实现过滤功能 filterChain.doFilter(servletRequest,servletResponse); // 上面的 doFilter 方法执行结束后用户的请求已经返回 long endTime = System.currentTimeMillis(); System.out.println("该用户的请求已经处理完毕,请求花费的时间为:" + (endTime - startTime)); } @Override public void destroy() { logger.info("销毁过滤器MyFilter"); } } 复制代码
MyFilter2 文件和上述代码一致,只是命名不同。
在配置中注册自定义的过滤器
@Configuration public class MyFilterConfig { @Autowired MyFilter myFilter; @Autowired MyFilter2 myFilter2; @Bean public FilterRegistrationBean<MyFilter> setUpMyFilter(){ FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>(); //setOrder 方法可以决定 Filter 的执行顺序 filterRegistrationBean.setOrder(1); filterRegistrationBean.setFilter(myFilter); filterRegistrationBean.setUrlPatterns(new ArrayList<>(Arrays.asList("/api/*"))); return filterRegistrationBean; } @Bean public FilterRegistrationBean<MyFilter2> setUpMyFilter2(){ FilterRegistrationBean<MyFilter2> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setOrder(2); filterRegistrationBean.setFilter(myFilter2); filterRegistrationBean.setUrlPatterns(new ArrayList<>(Arrays.asList("/api/* "))); return filterRegistrationBean; } } 复制代码
在配置中注册自定义的过滤器,通过 FilterRegistrationBean 的 setOrder 方法可以决定 Filter 的执行顺序。
Spring Boot 启动类:
@SpringBootApplication @ServletComponentScan public class SpbGuide3Application { public static void main(String[] args) { SpringApplication.run(SpbGuide3Application.class, args); } } 复制代码
运行该项目,打开网址: http://localhost:8080/api/hello
Listener
@WebListener
在 servlet3.0 以后,我们可以不用再 web.xml 里面配置 listener,只需要加上@WebListener 注解就可以实现。
该注解用于将类声明为监听器,被 @WebListener 标注的类必须实现以下至少一个接口 :
接口名称 | 作用 |
ServletContextListener | 用于监听Web应用的启动和关闭 |
ServletContextAttributeListener | 用于监听ServletContext范围(application)内属性的改变 |
ServletRequestListener | 用于监听用户请求 |
ServletRequestAttributeListener | 用于监听ServletRequest范围(request)内属性的改变 |
HttpSessionListener | 用于监听用户Session的开始和结束 |
HttpSessionAttributeListener | 用于监听HttpSession范围(session)内属性的改变 |
Listener 类:
@WebListener public class ContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("application started"); System.out.println(sce.getServletContext().getServerInfo()); } @Override public void contextDestroyed(ServletContextEvent sce) { System.out.println("application stopped"); } } 复制代码
Spring Boot 启动类:
@SpringBootApplication @ServletComponentScan public class SpbGuide3Application { public static void main(String[] args) { SpringApplication.run(SpbGuide3Application.class, args); } } 复制代码
运行该项目,结果为:
总结
上述内容总结归纳了关于如何在 Spring Boot 中添加 Servlet、FIlter 和 Listener,其中多次提到 Servlet3.0,关于该部分的讲解,后续我们再做相关内容总结。