文章目录
一 配置文件
1.1 配置提示
自定义的类和配置文件绑定一般没有提示; 添加下面的依赖可以 实现自定义配置类的yaml文件编写时也能够自动提示.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
二 web开发
2.1 springmvc自动配置概览
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 内容协商视图解析器和BeanName视图解析器
- Support for serving static resources, including support for
WebJars
(covered later in this document)).- 静态资源(包括
webjars
)
- 静态资源(包括
- Automatic registration of
Converter
,GenericConverter
, andFormatter
beans.- 自动注册
Converter,GenericConverter,Formatter
- 自动注册
- Support for
HttpMessageConverters
(covered later in this document).- 支持
HttpMessageConverters
(后来我们配合内容协商理解原理)
- 支持
- Automatic registration of
MessageCodesResolver
(covered later in this document).- 自动注册
MessageCodesResolver
(国际化用)
- 自动注册
- Static index.html support.
- 静态
index.html
页支持
- 静态
- Custom Favicon support (covered later in this document).
- 自定义 Favicon
- Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document).- 自动使用
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)
- 自动使用
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration
class of type WebMvcConfigurer
but without @EnableWebMvc
.
不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer
自定义规则
If you want to provide custom instances of RequestMappingHandlerMapping
, RequestMappingHandlerAdapter
, or ExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations
and use it to provide custom instances of those components.
声明 WebMvcRegistrations 改变默认底层组件
If you want to take complete control of Spring MVC, you can add your own @Configuration
annotated with EnableWebMvc
, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration
as described in the Javadoc of @EnableWebMvc
.
使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration
全面接管SpringMVC
2.2 简单功能分析
2.2.0 静态资源
只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources
- 访问 : 当前项目根路径/ + 静态资源名
- 原理: 静态映射/**。
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面 - 改变默认的静态资源路径
spring:
mvc:
static-path-pattern: "/static/**"
web:
resources:
static-locations: ['classpath:/resources/']
2.2.1 欢迎页
- 静态资源路径下加上
index.html
- 可以配置静态资源路径
- 但不能配置静态资源的访问前缀,否则
index.html
不能被默认访问
- Controller能处理
/index
2.2.2 静态资源配置原理
- SpringBoot 默认启动加载
xxxAutoConfiguration
列 - SpringMVC的自动配置类是
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, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
- 配置文件的相关属性和 xxx 进行了绑定
WebMvcProperties
(spring.mvc
)
ResourceProperties
(spring.resources
)
2.2.3 原理
- 配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath
//ServletRegistrationBean 给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
- 资源处理的默认规则
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
//webjars的规则
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
//
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
禁用所有静态资源规则:
spring:
# mvc:
# static-path-pattern: /res/**
resources:
add-mappings: false 禁用所有静态资源规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
- 欢迎页的处理规则
HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。
@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;
}
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,必须是/**
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 调用Controller /index
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
2.3 请求参数处理
2.3.1 表单支持REST风格
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
- 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
!!!注意::: 以下是 页面表单使用 restful 风格,如果安卓,postman 客户端工具,这里的不涉及.表单只支持 GET METHOD两种头.
- 核心Filter;
HiddenHttpMethodFilter
- 用法: 表单
method=post
,隐藏域_method=put
- SpringBoot中手动开启
- 用法: 表单
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
// 核心 的源码部分在这里
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
Rest原理(表单提交要使用REST的时候)
- 表单提交会带上_method=PUT
- 请求过来被HiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取到_method的值。
- 兼容以下请求:
PUT.DELETE.PATCH
- 原生request(post),包装模式
requesWrappe
r重写了getMethod
方法,返回的是传入的值。 - 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
- 请求是否正常,并且是POST
Rest使用客户端工具, 如PostMan直接发送Put、delete等方式请求,无需Filter。
2.3.2 请求映射原理
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet --> doDispatch()
开始.
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(Controller的方法)处理
mappedHandler = getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
RequestMappingHandlerMapping
:保存了所有@RequestMapping
和handler的映射规则。
所有的请求映射都在HandlerMapping
中。
- SpringBoot自动配置欢迎页的
WelcomePageHandlerMapping
。访问 /能访问到index.html
; - SpringBoot自动配置了默认 的
RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的
HandlerMapping
看是否有请求信息。- 如果有就找到这个请求对应的handler
- 如果没有就是下一个
HandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放
HandlerMapping
。自定义HandlerMapping
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
2.3.3 普通参数和注解
@PathVariable
@RequestHeader
@ModelAttribute
@RequestParam
@MatrixVariable
@CookieValue
@RequestBody
@RequestAttribute
- 举个覆盖全的例子:
@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;
}
- 有几个用的不是很多的注解举例: 比如
@RequestAttribute
,通常用来转发请求, 注意两个方法中的 http servlet 是同一个对象
2.3.4 复杂参数(原理略)
Map
Model(map、model里面的数据会被放在request的请求域 request.setAttribute
)Errors/BindingResult
RedirectAttributes( 重定向携带数据)
ServletResponse(response)
SessionStatus
UriComponentsBuilder
ServletUriComponentsBuilder
2.3.5 Servlet API
WebRequest
ServletRequest
MultipartRequest
HttpSession
javax.servlet.http.PushBuilder
Principal
InputStream
Reader
HttpMethod
Locale
TimeZone
ZoneId
ServletRequestMethodArgumentResolver
以上的部分参数.
@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) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
2.3.6 自定义对象参数
1 . 可以自动类型转换与格式化,可以级联封装.
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
- POJO的封装过程
ServletModelAttributeMethodProcessor
- 参数处理原理
HandlerMapping
中找到能处理请求的Handler(Controller.method())
- 为当前Handler 找一个适配器
HandlerAdapter
,RequestMappingHandlerAdapter
- 适配器执行目标方法并确定方法参数的每一个值
2.3.7 参数绑定
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
-
WebDataBinder
:web数据绑定器,将请求参数的值绑定到指定的JavaBean里面 -
WebDataBinder
利用它里面的Converters
将请求数据转成指定的数据类型,再次封装到JavaBean中 -
GenericConversionService
:在设置每一个值的时候,找它里面的所有converter
那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean -- Integer
)
(byte -- > file
)
2.3.8 扩展
未来我们可以给WebDataBinder
里面放自己的Converter
;
private static final class StringToNumber<T extends Number> implements Converter<String, T>
比如:
//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除;后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
// 啊猫,3
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}
2.4 数据响应和内容协商
通过SpringMVC 的内容协商功能:
- **根据请求头 Accept 决定返回的数据协议: 比如Json, XML **
- 也可以根据 请求 url的 _format=json 字段决定返回的数据格式
- 甚至用户能够自定义converter
SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】
2.5 视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
2.5.1 视图解析原理流程:
- 目标方法处理的过程中,所有数据都会被放在
ModelAndViewContainer
里面,包括数据和视图地址 - 方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在
ModelAndViewContainer
- 任何目标方法执行完成以后都会返回
ModelAndView
(数据和视图地址) processDispatchResult
处理派发结果(页面改如何响应)
render(mv, request, response); 进行页面渲染逻辑
根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】- 所有的视图解析器尝试是否能根据当前返回值得到View对象
- 得到了
redirect:/main.html
-->Thymeleaf new RedirectView()
ContentNegotiationViewResolver
里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。view.render(mv.getModelInternal(), request, response)
视图对象调用自定义的render
进行页面渲染工作RedirectView
如何渲染【重定向到一个页面】- 1、获取目标url地址
- 2、
response.sendRedirect(encodedURL)
2.5.2 视图解析:
- 返回值以
forward:
开始:new InternalResourceView(forwardUrl);
--> 转发request.getRequestDispatcher(path).forward(request, response);
- 返回值以
redirect:
开始:new RedirectView() --> render就是重定向
- 返回值是普通字符串:
new ThymeleafView()---> 自定义视图解析器+自定义视图
2.6 拦截器
举个栗子:
- 写个登录的拦截器
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
- 配置
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //放行的请求
}
}
- 拦截器原理
- 根据当前请求,找到
HandlerExecutionChain
【可以处理请求的handler以及handler的所有 拦截器】 - 先来顺序执行 所有拦截器的 preHandle方法
- 如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
- 如果任何一个拦截器返回false。直接跳出不执行目标方法
- 所有拦截器都返回True。执行目标方法
- 倒序执行所有拦截器的postHandle方法。
- 前面的步骤有任何异常都会直接倒序触发 afterCompletion
- 页面成功渲染完成以后,也会倒序触发 afterCompletion
- 根据当前请求,找到