Web 原生对象注入
# 450.Web 原生对象注入
接下来讲讲如何使用原生的 Servlet、Filter 和 Listener
# 使用原生 API
# Servlet
在学习使用 原生的 Servlet (opens new window) 的时候,我们有两种方式定义 Servlet:
- 定义一个 Servlet,然后在 web.xml 中配置
- 使用注解
@WebServlet
而在 SpringBoot 中,想要使用原生的 Servlet,还需使用一个注解 @ServletComponentScan
。
我们来实践下。首先定义一个 Servlet
package com.peterjxl.learnspringbootwebadmin.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后在主程序类中加上 @ServletComponentScan
:
package com.peterjxl.learnspringbootwebadmin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class LearnSpringBootWebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(LearnSpringBootWebAdminApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
其实该注解还可以加个 value 属性,值是 Servlet 所在的类,默认是主程序类所在包及其子包下,因此这里就不写了。
重启,可以看到能正常访问:
# Listener
监听器同理:
package com.peterjxl.learnspringbootwebadmin.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Filter
过滤器同理:假设我们要过滤过滤静态资源(这里仅仅是打印日志,然后就放行了)
package com.peterjxl.learnspringbootwebadmin.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"}) //my
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
重启项目,随便访问一个静态资源(例如 localhost: 9999/css/bootstrap.min.css (opens new window)),可以看到控制台有正常打印:
2023-08-01 10:44:59.523 INFO 15232 --- [nio-9999-exec-1] c.p.l.servlet.MyFilter : MyFilter工作
综上,要使用原生的 Servlet API,得加上 @ServletComponentScan
,然后再使用原生的 @WebServlet
,@WebListener
,@WebFilter
# 使用 RegistrationBean
文档还有这样一段话:
Registering Servlets, Filters, and Listeners as Spring Beans
Any Servlet, Filter, or servlet *Listener instance that is a Spring bean is registered with the embedded container. This can be particularly convenient if you want to refer to a value from your application.properties during configuration.
By default, if the context contains only a single Servlet, it is mapped to /. In the case of multiple servlet beans, the bean name is used as a path prefix. Filters map to /*.
If convention-based mapping is not flexible enough, you can use the
ServletRegistrationBean
,FilterRegistrationBean
, andServletListenerRegistrationBean
classes for complete control.
大意就是可以用 ServletRegistrationBean
, FilterRegistrationBean
, 和 ServletListenerRegistrationBean
来使用原生的 Servlet。
我们可以先注释掉刚刚写的 @WebServlet
,@WebListener
,@WebFilter
这几个注解,然后新建一个配置类:
package com.peterjxl.learnspringbootwebadmin.servlet;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyRegistrationCOnfig {
@Bean
public ServletRegistrationBean myServlet(){
return new ServletRegistrationBean(new MyServlet(), "/my", "/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
// 第一个参数是过滤器,第二个参数是过滤器要过滤的路径,也可以传一个ServletRegistrationBean对象,过滤器就会过滤这个ServletRegistrationBean对象中的路径
return new FilterRegistrationBean(new MyFilter(), myServlet());
}
@Bean
public ServletListenerRegistrationBean myListener(){
return new ServletListenerRegistrationBean(new MyServletContextListener());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在该类中,我们使用注入 bean 的方式,来使用原生 Servlet 对象。关于 Filter 还有其他写法:
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setServletRegistrationBeans(Arrays.asList("/my", "/css"));
return filterRegistrationBean;
}
2
3
4
5
6
7
接下来讲讲两个小细节
# 关于单例模式
由于我们的 Filter,是通过调用 myServlet()
方法来传入 Servlet 的;
因此如果配置了 @Configuration(proxyBeanMethods = false)
,那么每次过滤器工作,都会导致有新的 Servlet 对象被创建。
# DispatchServlet 注入原理
不知道读者有没发现,我们访问 localhost:9999/my
的时候,即使没有登录,也能正常访问;
但我们之前明明设置了过滤器,不登录的情况下,默认会跳转到登录页;为什么这次就不生效了?
这是因为,我们目前的应用有两个 Servlet:
MyServlet
,它要处理的路径是/my
DispatchServlet
,它要处理的路径是/
而如果多个 Servlet 都能处理到同一层路径,那么就看谁更精确,因此会选择 MyServlet
来处理 /my
请求
我们还可以看看 DispatchServlet
是怎么注册生效的。我们打开 DispatcherServletAutoConfiguration
类,它就是配置 DispatcherServlet
的。首先,就是将 DispatcherServlet
注入进来,名字是 dispatcherServlet
:
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
2
3
4
5
6
7
8
9
10
11
12
然后很多属性都是绑定到 WebMvcProperties 类里的(方法的参数里传进来了)。而该类的很多数据,都是取值配置文件(以 spring.mvc 开头的配置):
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
2
然后 DispatcherServletAutoConfiguration
还配置了 DispatcherServletRegistrationBean
:
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
通过源码可知,DispatcherServletRegistrationBean
就是一个 ServletRegistrationBean
:
public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet>
和我们使用 RegistrationBean
配置自定义的 Servlet、Filter 和 Listener 一样,DispatcherServlet
也是这样配置进来的。
在第 7 行,还配置了 DispatcherServlet
的路径:
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
在 webMvcProperties
中,该变量就是 /
:
private String path = "/";
public String getPath() {
return this.path;
}
2
3
4
5
该路径也是可以修改的,例如:
spring.mvc.servlet.path=/mvc
1
总结
- 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
- 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
- 默认映射的是 / 路径。
# 源码
已将本文源码上传到 Gitee (opens new window) 或 GitHub (opens new window) 的分支 demo7,读者可以通过切换分支来查看本文的示例代码