源码如下:
@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 容器去解析。
上述内容即为@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 3.0 新特性详解
在 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 注解自动注册,无需其他代码。
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
下面是@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 注解中设置的。
@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 方法执行,进而释放过滤器使用的资源。
结合上述注册 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 排序功能。
@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
在 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,关于该部分的讲解,后续我们再做相关内容总结。