1. 简单功能分析
1.1 静态资源访问
1、静态资源目录
当前项目类路径下: /static
、/public
、/resources
、META/resources
都默认作为静态资源目录
如下示例:
在中四个静态资源目录中放入四张图片,然后直接启动项目,看能否直接访问到静态资源:
- http://localhost:8080/bug.jpg
- http://localhost:8080/time.jpg
- http://localhost:8080/dog.jpg
- http://localhost:8080/cat.jpg
结果都能访问到,说明这四个目录下的文件都可以作为静态资源读取
那如果静态资源路径和请求路径冲突时会如何?
我们新建个 Controller:
@RestController
public class HelloController {
@RequestMapping("/bug.jpg")
public String hello() {
return "aaaaa";
}
}
重启后,访问 http://localhost:8080/bug.jpg 时,界面如下:
原理:
默认静态资源映射的是 /**
,动态请求默认也会拦截所有请求. 请求进来,先去找 Controller
看能不能处理. 不能处理的所有请求又都交给静态资源处理器. 静态资源就回去上面的四个静态目录中查找,静态资源也找不到,就会返回 404.
我们还可以改变默认的静态资源目录位置:
spring:
web:
resources:
static-locations: classpath:/haha
# static-locations: [classpath:/haha, classpath:/hehe/] 设置多个静态资源目录时的列表写法
2、静态资源访问前缀
默认无前缀.
spring:
mvc:
static-path-pattern: /res/**
以后访问地址就是: http://localhost:8080/res/bug.jpg
方便拦截器编写,放行带访问前缀的请求
3、WebJars
还有一种特殊的静态资源目录的映射:/webjars/**
把 js 文件打成 jar 包的形式,和 java 库统一格式被 Maven 管理:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
此时我们访问 http://localhost:8080/webjars/jquery/3.6.0/jquery.js:
可以看到可以访问到 jquery.js 文件
1.2 欢迎页支持
1、静态资源路径下 index.html
我们在静态资源目录下新建一个 index.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>飞鸟尽,良弓藏,狡兔死,走狗烹</h1>
</body>
</html>
此时访问 http://localhost:8080:
可以配置静态资源路径,但是不能配置前缀,否则导致欢迎页失效
2、controller 能处理的 /index
请求
可以转发或重定向到某个页面
1.3 自定义 Favicon
将网站图标命名为 favicon.ico
放到静态资源目录中,会自动解析该图标为站点图标
最好编译之前 maven clean 一下,不然有时候会有改了但是不起作用的情况
1.4 静态资源配置原理
-
SpringBoot 启动默认加载
XXXAutoConfiguration
(自动配置类) -
Spring MVC 功能的自动配置类大多集中在
WebMvcAutoConfiguration
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration {}
-
给容器中配了什么
@Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
-
配置文件的相关属性和 XX 进行了绑定:
WebMvcProperties(prefix = "spring.mvc")
、ResourceProperties(prefix = "spring.resources")
1、静态内部类(也是配置类)只有一个有参构造器:
// 有参构造器,其所有参数值都会从容器中确定
/**
* ResourceProperties resourceProperties:获取和 spring.resources 绑定的所有的值的对象
* WebMvcProperties mvcProperties: 获取和 spring.mvc 绑定的所有的值的对象
* ListableBeanFactory beanFactory: 获取 Spring 的 beanFactory
* HttpMessageConverters: 找到所有的 HttpMessageConverters
* ResourceHandlerRegistrationCustomizer 找到资源处理器的自定义器
* DispatcherServletPath
* ServletRegistrationBean:给应用注册 Servlet、Filter....
*/
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
2、静态资源处理的默认规则
// WebMvcAutoConfiguration 类中
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
ServletContext servletContext = getServletContext();
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
}
我们在该方法一开始打上断点:
可以看到,关键在于 isAddMappings()
返回的是 true
还是 false
,如果是 true
,则执行后面的默认配置,否则直接返回.
然后我们进入该方法可以看到:
它返回的其实就是个属性,默认为 true
:
与配置文件中 spring.web.resources.add-mappings
一一对应:
设为 false
后将禁用静态资源的范围.
下面看看默认的静态资源处理的默认规则,首先获取 Servlet
上下文环境:
然后首先注册处理 webjars 的相关规则:
进入该方法:
可以看到内部调用了同名的重载方法,多传了一个 lambda 表达式:
(registration) -> registration.addResourceLocations(locations)
进入该重载方法:
如果对该 pattern
注册过 handler
了,则直接返回,否则注册一个新的 handler
来处理该 pattern
对应的静态资源请求.
这里没注册过,所以会继续执行:
进入 addResourceHandler()
方法内部可以发现:
其实就是创建资源处理器实例,里面主要是封装的 pattern
信息,该处理器在接收到 requests
时自动匹配处理.
pattern
可以是一个或多个,只要处理的过程是一样的就行.
这里第二行对应前面的
registry.hasMappingForPattern(pattern)
方法,注册进去了才能找得到. 我们可以进入该方法内部看看:
创建完 /webjars/**
对应的资源处理器后,继续执行:
这里是一个函数式接口 Consumer
的 accept()
方法,该方法简单来说,就是消费(使用)一个东西. 具体执行的函数过程是传进来 lambda 表达式:
(registration) -> registration.addResourceLocations(locations)
我们前面注册了资源处理器,但是它的资源路径并没有封装进去,这里就是将指定类型的请求映射到本地资源路径地址前缀,具体来说,这里是:
/webjars/** ==> classpath:/META-INF/resources/webjars/
下一步是设置该资源在浏览器的缓存时间,配置文件中可以自定义该时间:
spring:
web:
resources:
cache:
period: 1234
然后,为了兼容性,将缓存控制转换为 Http 的缓存控制:
上面都是默认配置,下面看看有没有个性化的配置,有则覆盖:
下面继续,看静态资源路径的配置规则:
可以发现,整体流程和处理 webjars 的配置规则大体是一样的:
-
首先,
pattern
由/webjars/**
变为this.mvcProperties.getStaticPathPattern()
,这个是个什么玩意儿呢?其实点进去之后发现就是/**
,而且这里适合配置文件绑定的:spring: mvc: static-path-pattern: /res/**
这里的赋值会覆盖默认的
/**
,其实就更改了静态资源访问前缀! -
其次,传入的 lambda 表达式不同,这里传进去的处理过程是:
(registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (servletContext != null) { registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION)); } }
具体过程继续 debug,进入到 addResourceHandler()
方法内:
一样的先判断容器中是否已经存在该请求路径对应的处理器(注意,拦截器默认也是 /**
,优先 Controller
处理,没有才到这):
然后创建对应 static-path-pattern
(默认 /**
)的资源处理器:
下一步就是利用消费者接口执行 lambda 表达式的方法了:
这一步是给该资源处理器添加本地要映射的路径,默认是:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
优先级就是源码里这四个位置的顺序
再下面还会判断 Servlet
上下文是否为空,如果不为空,还要给它映射路径添加一个 /
,这也就是为什么Controller
能处理资源路径请求的原因,这里注册进去了,而且优先级高!
之后也是一样的配置缓存,个性化定制覆盖默认配置等.
3、欢迎页的处理规则
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
这里需要了解 HadddlerMapping
,详细内容见 https://e-thunder.blog.csdn.net/article/details/113699510.
简单来说,它就是处理映射器,保存了每一个 Handler
能处理哪些请求.
可以看到首先 new
一个 WelcomePageHandlerMapping
,有一个参数是 this.mvcProperties.getStaticPathPattern()
,这个 staticPathPattern
默认是 /**
,它也与配置文件关联,可以配置如下参数覆盖原配置:
spring:
mvc:
static-path-pattern: /res/**
这个在之前也说过,就是更改静态资源访问前缀.
我们进入该构造方法中:
可以看到,如果找到 index.html
并且 static-path-pattern=/**
才能启动欢迎页,这里底层写死了,所以就解释为啥使用访问前缀时,无法启动欢迎页了,但是,也不绝对,从代码上看,如果存在欢迎页模板,也可以启动(实际上是调用 Controller
处理),这里等后面看到了再研究.
4、favicon
这个是浏览器默认的使用当前项目下的 favicon:
项目地址/favicon.*
如果加了静态访问前缀:
项目地址/访问前缀/favicon.*
则自然找不到.
2. 请求参数处理
2.1 请求映射
1、Rest 原理
以前的方式是通过 URI 来区分请求的类别:
/getUser 获取用户
/deleteUser 删除用户
/editUser 修改用户
/saveUser 保存用户
现在我们可以通过 HTTP 的请求方式来区分:
/user GET-获取用户
/user DELETE-删除用户
/user PUT-修改用户
/user POST-保存用户
其核心是 HiddenHttpMethodFilter
因为表单中的提交方法只有 GET 和 POST,如果想提交 PUT 和 DELETE,需要利用其它手段.
在表单中,我们需要加一个隐藏域
<input name="_method" type="hidden" value="方法"/>
这里方法就是 DELETE
或 PUT
,但是,必须得开启这个功能:
spring:
mvc:
hiddenmethod:
filter:
enabled: true
因为:
@Bean
// 没有自定义配置,SpringBoot 会帮你配
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
// 这个属性为 true 才开启,默认是 false,所以当然要人为开启了
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
例如下面的 HTML:
<form action="/user" method="get">
<input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="POST">
<input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="DELETE"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
</form>
表示的就是表单提交的四种请求方式.
Rest 原理(表单提交要使用 Rest 的时候):
- 表单提交会带上
_method=PUT
- 请求过来会被
HiddenHttpMethodFilter
拦截- 判断请求是不是 POST 方式并且不包含错误
- 如果都满足则可以获取到
_method
的值,如果该值有长度(就是不为空),则先将其全部转为大写字母 - 兼容以下请求:
PUT
、DELETE
、PATCH
,当传入的请求在这三个允许的请求之一 - 原生 request(post),包装模式
HttpMethodRequestWrapper
重写了getMethod()
方法,返回的是传入的值 - 此后,包装过的请求的请求方式变成
_method
的传入的请求方式的值.
- 如果都满足则可以获取到
- 判断请求是不是 POST 方式并且不包含错误
// HiddenHttpMethodFilter 类
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 原生 request
HttpServletRequest requestToUse = request;
// 请求是 POST 且没有错误
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 获取 _method 的值
String paramValue = request.getParameter(this.methodParam);
// 如果 _method 的值不为空
if (StringUtils.hasLength(paramValue)) {
// 转换大写
String method = paramValue.toUpperCase(Locale.ENGLISH);
// _method 的值在 PUT、DELETE 和 PATCH 之一时,执行包装方法
if (ALLOWED_METHODS.contains(method)) {
// 该包装方法将请求方式替换为 _method 里设置的方式
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
// 此时继续执行的请求就是包装后的请求了
filterChain.doFilter(requestToUse, response);
}
如何改变默认的 _method
呢?例如改为 _m
也能实现一样的功能:
<form action="/user" method="post">
<input name="_m" type="hidden" value="PUT"/>
<input value="REST-PUT 提交" type="submit"/>
</form>
我们可以回头看看:
容器中没有配置 HiddenHttpMethodFilter
组件才执行,而 HiddenHttpMethodFilter
类中,默认定义的是:
所以我们可以自己放一个 HiddenHttpMethodFilter
来覆盖原有默认配置:
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
val hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
}
2、请求映射原理
SpringBoot 底层也是 SpringMVC,所以核心也是 DispatcherServlet
,之前在 SpringMVC 的学习笔记里记录了从浏览器发送请求到响应的全过程,这里再重新过一遍增加记忆.
首先我们可以看看 DispatcherServlet
的继承树:
它实际上也是个 Servlet
,所以必然会重写 doGet()
、doPost()
等方法:
可以看到,这些方法是在 FrameworkServlet
类中重写的,并且重写的方法体中都调用了同一个函数 processRequest(request, response)
,我们进入该函数看看:
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 初始化过程
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
doService(request, response); // 核心
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
可以看到核心就是调用 doService()
方法,而在 FrameworkServlet
中,该方法是抽象方法:
所以肯定是调用它的重写方法,即该方法在 DispatcherServlet
中. 而在 doService()
方法中,去除初始化的一些代码,核心是:
即调用 doDispatch()
方法.
至此我们可以得出结论:SpringMVC 的功能分析都从 DispatcherServlet
的 doDispatch()
方法开始.
我们在这个方法上打上断点,以测试前面 Rest 测试的 GET 请求(/user
)为例:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 初始化操作
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 检查是否是文件上传请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 决定是哪个 Handler 的哪个方法来处理当前请求
mappedHandler = getHandler(processedRequest);
// ......
}
进入 getHandler()
方法:
可以发现,它是通过 HandlerMapping
(处理器映射) 来寻找当前请求由谁处理(for 循环匹配),默认有 5 个 HandlerMapping
.
RequestMappingHandlerMapping
:保存了所有@RequestMapping
(请求) 和 handler(处理器) 的映射规则.
这里也反应了首页的欢迎页为甚可以访问到,因为首先会去在 RequestMappingHandlerMapping
里找,但是没人能处理 /
,所以会再次进入到 WelcomePageHandlerMapping
寻找对应的 handler:
我们也可以自定义 HandlerMapping
2.2 普通参数与基本注解
1、注解
-
@PathVariable
:路径变量@RestController public class ParameterTestController { @GetMapping("/car/{id}/owner/{username}") public Map<String, Object> getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, // Rest 风格绑定参数 @PathVariable Map<String, String> kv) { // 也可以直接用一个 Map<String,String>接收所有 Rest 变量 Map<String, Object> map = new HashMap<>(); map.put("id", id); map.put("name", name); map.put("kv", kv); return map; } }
此时访问 http://localhost:8080/car/1/owner/ice,可以看到:
-
@RequestHeader
:获取请求头@RestController public class ParameterTestController { @GetMapping("/car") public Map<String, Object> getCar(@RequestHeader("User-Agent") String userAgent, // 可以获取指定请求头的内容 @RequestHeader Map<String,String> header) { // 也可以直接用一个 Map<String,String>接收所有请求头的键值对 Map<String, Object> map = new HashMap<>(); map.put("userAgent", userAgent); map.put("header", header); return map; } }
此时访问 http://localhost:8080/car,可以看到:
-
@RequestParam
:获取请求参数@RestController public class ParameterTestController { @GetMapping("/car") public Map<String, Object> getCar(@RequestParam("age") Integer age, @RequestParam("sex") String sex, @RequestParam("inters") List<String> inters, @RequestParam Map<String, String> params) { Map<String, Object> map = new HashMap<>(); map.put("age", age); map.put("sex", sex); map.put("inters", inters); map.put("params", params); return map; } }
此时访问 http://localhost:8080/car?age=18&sex=%E7%94%B7&inters=Basketball&inters=game,可以看到:
注意,使用 Map 整体接收参数时,inters 只保存了一个值
-
@CookieValue
:获取 Cookie 的值@RestController public class ParameterTestController { @GetMapping("/car") public Map<String, Object> getCar(@CookieValue("_ga") String _ga, @CookieValue("_ga") Cookie cookie) { Map<String, Object> map = new HashMap<>(); map.put("_ga", _ga); map.put("cookie", cookie); return map; } }
此时访问 http://localhost:8080/car,可以看到:
-
@RequestBody
:获取请求体这个是针对 POST 请求的.
【HTML】
<form action="/save" method="post"> 测试@RequestBody获取数据 <br/> 用户名:<input name="userName"/> <br> 邮箱:<input name="email"/> <input type="submit" value="提交"/> </form>
【Controller】
@RestController public class ParameterTestController { @PostMapping("/save") public Map<String, Object> postMethod(@RequestBody String content) { Map<String, Object> map = new HashMap<>(); map.put("content", content); return map; } }
我们通过表单提交来测试:
结果为:
-
@RequestAttribute
:获取 request 域属性@Controller public class RequestController { @GetMapping("/goto") public String goToPage(HttpServletRequest request) { request.setAttribute("msg", "成功了"); request.setAttribute("code", 200); return "forward:/success"; } @ResponseBody @GetMapping("/success") public Map<String, Object> success(@RequestAttribute("msg") String msg, @RequestAttribute("code") Integer code, HttpServletRequest request) { Object msg1 = request.getAttribute("msg"); Object code1 = request.getAttribute("code"); Map<String, Object> map = new HashMap<>(); map.put("requestMethod_msg",msg1); map.put("annotation_msg",msg); map.put("requestMethod_code",code1); map.put("annotation_code",code); return map; } }
此时访问 http://localhost:8080/goto,可以看到:
-
@MatrixVariable
:矩阵变量传统请求:/cars/{path}?xxx=xxx&aaa=aaa
矩阵变量:/cars/{path;low=34,brand=byd,audi,yd}
<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a> <a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a> <a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>
SpringBoot 默认禁用矩阵变量功能,需要手动开启:
@Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false); // 不移除分号后面的内容 configurer.setUrlPathHelper(urlPathHelper); } }
【Controller】:
@GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") String low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path) { Map<String, Object> map = new HashMap<>(); map.put("low", low); map.put("brand", brand); map.put("path", path); return map; }
访问 URL:http://localhost:8080/cars/sell;low=34;brand=byd,audi,yd:
访问 URL:http://localhost:8080/cars/sell;low=34;brand=byd;brand=audi;brand=yd:
还有一种麻烦的情况,访问:http://localhost:8080/boss/1;age=20/2;age=10:
此时 Controller 应该这么写:
@GetMapping("/boss/{bossId}/{empId}") public Map boss(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age", pathVar = "empId") Integer empAge) { Map<String, Object> map = new HashMap<>(); map.put("bossAge", bossAge); map.put("empAge", empAge); return map; }
2、Servlet API
WebRequest
、ServletRequest
、MultipartRequest
、HttpSession
、javax.servlet.http.PushBuilder
、Principal
、InputStream
、Reader
、HttpMethod
、Locale
、TimeZone
、ZoneId
类型都可以得到支持,是在 ServletRequestMethodArgumentResolver
类的 supportsParameter()
方法判断的:
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
3、复杂参数
Map
、Errors
、BindingResult
、Model
、RedirectAttributes
、ServletResponse
、SessionStatus
、UriComponentsBuilder
、ServletUriComponentsBuilder
map、model 里面的数据会被放在 request 的请求域 request.setAttribute
【例】
@GetMapping("/params")
public String testParam(Map<String, Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response) {
map.put("hello", "world666");
model.addAttribute("world", "hello666");
request.setAttribute("message", "hello world");
Cookie cookie = new Cookie("c1", "v1");
response.addCookie(cookie);
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map<String, Object> success(HttpServletRequest request) {
Object hello = request.getAttribute("hello");
Object world = request.getAttribute("world");
Object message = request.getAttribute("message");
Map<String, Object> map = new HashMap<>();
map.put("hello", hello);
map.put("world", world);
map.put("message", message);
return map;
}
结果:
4、自定义对象参数
自定义的 POJO 等
2.3 参数处理原理
HandlerMapping
中找到能处理请求的Handler
(Controller.method()
)- 为当前
Handler
找一个适配器HandlerAdapter
(RequestMappingHandlerAdapter) - 适配器执行目标方法并确定方法参数的每一个值
以下面这个请求为例:
@GetMapping("/car/{id}/owner/{username}")
public Map<String, Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String, String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String, String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String, String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie) {
Map<String, Object> map = new HashMap<>();
map.put("age", age);
map.put("inters", inters);
map.put("params", params);
map.put("_ga", _ga);
System.out.println(cookie.getName() + "===>" + cookie.getValue());
return map;
}
访问 URL 是:http://localhost:8080/car/1/owner/ice?age=18&inters=baksetball&inters=game
前的面步骤已经基介绍过了,这里从下图所示位置开始看:
这里返回了用来处理该请求的 HandlerAdapter
然后执行拦截器方法 mappedHandler.applyPreHandle(processedRequest, response)
,没有问题才执行真正的处理方法:
进入该方法:
再进入内部调用的方法,也就是进入了 RequestMappingHandlerAdapter
的 handleInternal()
方法:
进入该方法:
默认有 27 个参数解析器:
其作用是确定将要执行的目标方法的每一个参数的值是什么
SpringMVC 的目标方法能写多少种参数类型,就取决于这里有多少种参数解析器!
这些参数解析器实际上实现自一个接口:
它首先通过 supportsParameter()
判断是否支持这种参数,如果支持,则执行 resolveArgument()
进行解析
与此同时,紧接着后面的是返回值处理器,能决定目标方法能写多少种返回值:
可以看到,默认有 15 种返回值处理器:
再往下,看真正执行目标方法的地方:
进入该方法:
通过 debug 可以发现 invokeForRequest()
才是真正执行目标方法的地方,我们进入该方法:
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 获取所有方法参数
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
// 反射调用目标方法
return doInvoke(args);
}
我们进入 getMethodArgumentValues()
看它如何获取的:
// InvocableHandlerMethod 类
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 获取方法所有参数的详细信息,包括注解
MethodParameter[] parameters = getMethodParameters();
// 判断参数是否为空,如果为空当然不用处理,直接返回
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
// 声明 Object 数组长度与参数列表长度一致
Object[] args = new Object[parameters.length];
// for 循环中的过程就是如何根据参数信息,解析出实参
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 判断现有的参数解析器是否支持这种参数类型
// 其内部是通过增强 for 循环遍历所有的解析器,来判断是否支持
// 核心代码在 HandlerMethodArgumentResolverComposite 类的 getArgumentResolver()方法
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 解析变量的值
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
目标方法执行完,将所有的数据都放在 ModelAndViewContainer
,包含要去的页面地址 View
,还包含 Model
数据
最后要处理派发结果:
3. 内容协商
根据客户端接收能力不同,返回不同媒体类型的数据.
1、支持返回 xml 的依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2、原理
-
判断当前响应头中是否有确定的媒体类型(MediaType)
-
获取客户端支持接收的内容类型(获取客户端请求头 Accept 字段)
-
遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
-
找到支持操作Person的converter,把converter支持的媒体类型统计出来
-
客户端需要【application/xml】,服务端能力【10种、json、xml】
-
进行内容协商的最佳匹配媒体类型
-
用支持将对象转为最佳匹配媒体类型的 converter,调用它进行转化
3、开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能
spring:
mvc:
contentnegotiation:
favor-parameter: true
此时,访问 http://localhost:8080/test/person 默认返回:
访问 http://localhost:8080/test/person?format=xml 返回:
访问 http://localhost:8080/test/person?format=json 返回:
4、自定义 MessageConverter
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// ...
}
}
}
4. 模板引擎 Thymeleaf
4.1 基本语法
1、表达式
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${...} | 获取请求域、session域、对象等值 |
选择变量 | *{...} | 获取上下文对象值 |
消息 | #{...} | 获取国际化等值 |
链接 | @{...} | 生成链接 |
片段表达式 | ~{...} | jsp:include 作用,引入公共页面片段 |
2、字面量
- 文本值
- 数字
- 布尔值(true,false)
- 空值(null)
- 变量(不能有空格)
3、文本操作
字符串拼接:+
变量替换:the name is ${name}
4、数学运算
+
,-
,*
,/
,%
5、布尔运算
and
、or
、!
、not
6、比较运算
>
,<
,>=
,<=
,==
,!=
7、条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
8、特殊操作
无操作:_
4.2 设置属性值-th:attr
设置单个值:
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
设置多个值:
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法:th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有 h5 兼容的标签写法:
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
4.3 迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod, iterStat : ${prods}" th:class="${iterStat.odd} ? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock} ? #{true} : #{false}">yes</td>
</tr>
4.4 条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
4.5 属性优先级
4.6 Thymeleaf 的使用
1、引入 starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2、自动配置好了 Thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {}
自动配好的策略:
-
所有 Thymeleaf 的配置值都在 ThymeleafProperties
前缀、后缀、编码都已经指定好了
-
配置好了 SpringTemplateEngine
-
配置好了 ThymeleafViewResolver
-
我们只需要直接开发页面
3、页面开发
【success.html】
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="http://www.atguigu.com" th:href="${link}">去百度</a> <br>
<a href="http://www.atguigu.com" th:href="@{link}">去百度</a>
</h2>
</body>
</html>
注意,要引入命名空间:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
【ViewController】
@Controller
public class ViewTestController {
@RequestMapping("/nuist")
public String nuist(Model model){
model.addAttribute("msg","你好");
model.addAttribute("link","https://www.baidu.com");
return "success";
}
}
启动项目,访问:http://localhost:8080/nuist:
检查网页源代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>你好</h1>
<h2>
<a href="https://www.baidu.com">去百度</a> <br>
<a href="link">去百度</a>
</h2>
</body>
</html>
可以看到,
@{}
传入的只是字符串,它是相对项目根路径访问地址,比如这里是 http://localhost:8080/link
5. 视图解析原理
1、目标方法处理的过程中,所有数据都会被放在 ModelAndViewontainer
里面,包括数据和视图地址
2、方法的参数是一个自定义类型对象(从请求参数中确定的),把它重新放在 ModelAndViewContainer
3、任何目标方法执行完以后都会返回 ModelAndView
对象(数据和视图地址)
4、doDispatch()
中的 processDispatchResult()
处理派发结果(页面如何响应)
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
// ...
if (mv != null && !mv.wasCleared()) {
render(mv, request, response); // 渲染视图
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
// ...
}
可以看到,其内部是调用 render()
方法来渲染视图的:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
View view;
String viewName = mv.getViewName(); // 得到 Controller 返回的视图字符串,如:redirect:/main.html
if (viewName != null) {
// 利用视图解析器解析视图名,返回 View 对象
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// ...
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
// 调用 view 的 render 方法来这真正渲染视图
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
下面先看看 resolveViewName()
方法是如何根据视图名解析视图的:
可以发现,是遍历默认的视图解析器 XxxViewResolver
,调用其 resolveViewName()
方法来解析视图名,如果能解析,则返回该解析的 View
对象
接着再看看 view 的 render()
方法如何渲染视图的:
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
// ...
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); // 渲染视图
}
唉,又套娃了,继续看看 renderMergedOutputModel()
:
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws IOException {
// ...
// Redirect
sendRedirect(request, response, targetUrl, this.http10Compatible);
}
继续 sendRedirect()
:
protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
String targetUrl, boolean http10Compatible) throws IOException {
String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl));
if (http10Compatible) {
HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
if (this.statusCode != null) {
response.setStatus(this.statusCode.value());
response.setHeader("Location", encodedURL);
}
else if (attributeStatusCode != null) {
response.setStatus(attributeStatusCode.value());
response.setHeader("Location", encodedURL);
}
else {
// Send status code 302 by default.
response.sendRedirect(encodedURL);
}
}
else {
HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
response.setStatus(statusCode.value());
response.setHeader("Location", encodedURL);
}
}
底层归根到底就是 Servlet 的底层 response.sendRedirect(encodedURL);
进行重定向.
其他的视图解析类似.
6. 拦截器
Spring MVC 拦截器的顶级接口是 HandlerInterceptor
:
public interface HandlerInterceptor {
// 目标方法执行前执行
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
// 目标方法执行完执行
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 请求完成之后(视图渲染之后)执行
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
以登录拦截为例:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录检查逻辑
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
if (user!=null){
return true;
}
response.sendRedirect("/");
return false;
}
}
将拦截器配置到 Spring 容器中
@Configuration
// 自定义 MVC 配置都要实现 WebMvcConfigurer 接口,重写方法即可
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 所以请求都被拦截了,包括静态资源
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); // 放行的请求
}
}
-
一般是按照顺序执行拦截器
preHandle()
方法的,当遇到返回为false
时,它会倒序执行已经执行过的拦截器的afterCompletion()
方法 -
后面遇到任何异常都会倒序执行拦截器的
afterCompletion()
方法 -
页面完成渲染之后,也会倒序触发拦截器
afterCompletion()
方法
7. 文件上传
前端提交文件的元素为:
<input type="file" name="headerImage" id="exampleInputFile"> <!-- 单文件上传 -->
<input type="file" name="photos" multiple> <!-- 多文件上传 -->
并且,表单 form
必须设为 post
提交 以及 `enctype=“multipart/form-data”:
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
处理上传文件的 Controller 如下所示:
@Controller
public class FormTestController {
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImage") MultipartFile headerImage,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
if (!headerImage.isEmpty()) {
String originalFilename = headerImage.getOriginalFilename();
headerImage.transferTo(new File("E:/cache/" + originalFilename));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("E:/cache/" + originalFilename));
}
}
}
return "main";
}
}
E:/cache
地址确定存在,否则报错!
当然,上传文件还有一些配置,比如文件最大的大小,我们可以在配置文件中直接设置属性值:
spring:
servlet:
multipart:
max-file-size: 10MB
8. 错误处理
8.1 默认规则
-
默认情况下,Spring Boot 提供
/error
处理所有错误的映射 -
对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息。对于浏览器客户端,响应一个 “whitelabel” 错误视图,以 HTML 格式呈现相同的数据
-
浏览器客户端
-
Postman 客户端
-
-
error/
下的4xx
,5xx
页面会被自动解析
8.2 异常处理的自动配置原理
-
ErrorMvcAutoConfiguration
自动配置了异常处理规则-
配置了
DefaultErrorAttributes
类型组件 id:errorAttributespublic class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
-
配置了
BasicErrorController
类型组件id:basicErrorController
(内容协商适配响应)- 处理默认
/error
路径的请求,页面响应new ModelAndView("error", model)
- 处理默认
-
配置了
View
类型组件 id:error@Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; // 默认是一个我白页 StaticView }
-
配置了组件
BeanNameViewResolver
(视图解析器),按照返回的视图名作为组件的 id 去容器中找View
对象 -
配置了组件
DefaultErrorViewResolver
,id:conventionErrorViewResolver-
如果发生错误,会以 HTTP 的状态码作为视图页地址,找到真正的页面:
String errorViewName = "error/" + viewName;
-
-
8.3 定制错误处理逻辑
1、自定义错误页
error/404.html、error/5xx.html,有精确的错误状态码页面就匹配精确,没有就找 4xx.html,如果都没有就触发白页
2、@ControllerAdvice
+ @ExceptionHandler
处理异常
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class}) // 能处理的异常
public String handleMathException(Exception exception) {
System.out.println("异常是:" + exception);
return "login"; // 视图地址
}
}
3、@ResponseStatus
+ 自定义异常
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "you reason") // 传入该异常的返回码以及原因
public class XException extends RuntimeException {
public XException(String msg) {
super(msg);
}
}
4、自定义异常解析器
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
response.sendError(511,"我喜欢的错误"); // response.sendError() /error 请求就会转给 controller
} catch (IOException e) {
e.printStackTrace();
}
// return new ModelAndView(); 或者返回要跳转的页面及数据
}
}
9. Web 原生组件注入(Servlet、Filter、Listener)
9.1 使用 Servlet API
首先编写一个 Servlet:
@WebServlet(urlPatterns = "/my") // servlet 3.0 以上提供的注解
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
然后在主启动类添加注解 @ServletComponentScan
:
@SpringBootApplication
@ServletComponentScan(basePackages = "com.ice.admin")
public class SpringbootWebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebAdminApplication.class, args);
}
}
访问 http://localhost:8080/my:
它直接响应,没有经过 Spring 的拦截器
拦截器和监听器也一样:
【拦截器】
@WebFilter(urlPatterns = {"/css/*","/images/*"}) // Servlet 的写法是 *,Spring 是* *
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("MyFilter 初始化完成...");
}
@Override
public void destroy() {
System.out.println("MyFilter 销毁...");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("MyFilter 执行...");
filterChain.doFilter(servletRequest, servletResponse);
}
}
【监听器】
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("监听到项目初始化完成...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("监听到项目销毁...");
}
}
注意,他们都要统一咋主启动类添加 @ServletComponentScan
注解扫描原生组件!
9.2 使用 RegistrationBean
@Configuration
public class MyRegisterConfig {
@Bean
public ServletRegistrationBean myServlet() {
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet, "/my", "/my01");
}
@Bean
public FilterRegistrationBean myFilter1() {
MyFilter myFilter = new MyFilter();
return new FilterRegistrationBean(myFilter, myServlet()); // 拦截指定 Servlet
}
@Bean
public FilterRegistrationBean myFilter2() {
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my", "/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener() {
MyServletContextListener myServletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean(myServletContextListener);
}
}