源码如下:
@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,关于该部分的讲解,后续我们再做相关内容总结。