3. WEB
3.1 DispatcherServlet 的初始化时机
DispatcherServlet 是被 spring 容器进行管理的,但是 DispatcherServlet 的初始化等是被 TomcatServlet 进行生命周期管理的
DispatcherServlet 初始化时机可以是,TomcatServlet 第一次被访问时,也可以是 TomcatServlet启动时开始初始化操作
-
默认的 DispatcherServlet 是在第一次被 TomcatServlet 访问时执行初始化。 也可以通过配置修改为 Tomcat 启动后就初始化(registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup())、spring.mvc.servlet.load-on-startup=1)
-
DispatcherServlet 在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化
protected void initStrategies(ApplicationContext context) { this.initMultipartResolver(context); // 处理文件上传 this.initLocaleResolver(context); // 处理国际化设置 this.initThemeResolver(context); // 处理主题切换 this.initHandlerMappings(context); // 根据请求映射找到处理器方法 this.initHandlerAdapters(context); // 执行处理器方法 this.initHandlerExceptionResolvers(context); // 处理异常 this.initRequestToViewNameTranslator(context); // 将请求转换为视图名称 this.initViewResolvers(context); // 根据视图名称返回视图 this.initFlashMapManager(context); // 处理重定向后的临时数据 }
3.2 映射器和调用器
RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter:
-
RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter 俩是一对
- RequestMappingHandlerMapping 处理 @RequestMapping 映射
- RequestMappingHandlerAdapter 调用控制器方法、并处理方法参数与方法返回值
-
RequestMappingHandlerMapping 初始化时,会收集所有 @RequestMapping 映射信息,封装为 Map,获取了所有的 HandleMethod 方法,其中
- key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
- value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
- 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet
-
RequestMappingHandlerAdapter 初始化时,会准备 HandlerMethod 调用时需要的各个组件,如:
-
HandlerMethodArgumentResolver 解析控制器方法参数
-
HandlerMethodReturnValueHandler 处理控制器方法返回值
-
HandlerAdapter(RequestMappingHandlerAdapter)作用就是调用控制器方法(HandlerMethod),HandlerMethod 可以通过 getHandlerMethods 获得一个 set 集合,也可以通过 getHandlerMethod 并传递 request 获取指定的 HandlerMethod
-
3.2.1 自定义参数解析器
总结:
-
自定义参数解析器
- 参数解析器需要实现 HandlerMethodArgumentResolver 接口,完成 supportsParameter(检查是否支持某个参数),resolveArgument(获取参数的值)两个方法
- 将实现的类加入到 HandlerAdapter 中( HandlerAdapter提供setCustomArgumentResolvers)
-
参数解析器的作用:
- 看是否支持某种参数(supportsParameter 方法)
- 获取参数的值(resolveArgument 方法)
3.2.2 自定义返回值处理器
总结:
- 自定义返回值处理器
- 参数解析器需要实现 HandlerMethodReturnValueHandler 接口,完成 supportsParameter(检查是否支持某个参数),resolveArgument(获取参数的值)两个方法
- 将实现的类加入到 HandlerAdapter 中(HandlerAdapter提供setCustomReturnValueHandlers)
- 自定义返回值处理器的作用:
- 看是否支持某种参数(supportsReturnType 方法)
- 返回值处理逻辑(handleReturnValue 方法)
3.3 参数解析器(组合器设计模式) \textcolor{green}{3.3\ 参数解析器(组合器设计模式)} 3.3 参数解析器(组合器设计模式)
总结:
-
初步了解 RequestMappingHandlerAdapter 的调用过程
- 控制器方法被封装为 HandlerMethod
- 准备对象绑定与类型转换
- 准备 ModelAndViewContainer 用来存储中间 Model 结果
- 利用 HandlerMethodArgumentResolverComposite 解析每个参数值
-
解析参数依赖的就是各种参数解析器,它们都有两个重要方法
- supportsParameter 判断是否支持方法参数
- resolveArgument 解析方法参数
-
常见参数的解析
@RequestParam("name1") String name1, // 参数绑定 String name2, // 省略 @RequestParam @RequestParam("age") int age, // 类型转换 @RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // 设置默认值 @RequestParam("file") MultipartFile file, // 文件上传 @PathVariable("id") int id, // 路径参数 @RequestHeader("Content-Type") String header, // Header信息 @CookieValue("token") String token, // @Value("${JAVA_HOME}") String home2, // spring 获取数据,插值表达式 HttpServletRequest request, // request, response, session ... @ModelAttribute("abc") User user1, // 对象绑定,用对象接受参数 User user2, // 省略 @ModelAttribute @RequestBody User user3 // 从请求体中获取数据
-
常见的参数解析器
// false 表示必须有 @RequestParam new RequestParamMethodArgumentResolver(beanFactory, false), new PathVariableMethodArgumentResolver(), new RequestHeaderMethodArgumentResolver(beanFactory), new ServletCookieValueMethodArgumentResolver(beanFactory), new ExpressionValueMethodArgumentResolver(beanFactory), new ServletRequestMethodArgumentResolver(), new ServletModelAttributeMethodProcessor(false), // 必须有 @ModelAttribute new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())), // 位置必须放在这两个之间,不然@RequestBody会被当作是省略了 @ModelAttribute 注解的情况 new ServletModelAttributeMethodProcessor(true), // 省略了 @ModelAttribute new RequestParamMethodArgumentResolver(beanFactory, true) // 省略 @RequestParam
-
**组合器设计模式(composite设计模式)**在 Spring 中的体现(HandlerMethodArgumentResolverComposite 多个解析器组合)
-
@RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取
-
PathVariableMethodArgumentResolver 解析器需要将路径与参数对应关系放到 request 作用域,这这一步 HandlerMapping 会帮我们处理好
Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123"); System.out.println(map); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);
3.4 参数名解析(两种方法)
总结:
- 只使用 javac 进行编译不会保留变量名
- (javap -c -v 字节码) ==> 反编译查看类的信息
- 如果编译时添加了 -parameters( javac -parameters 类) 可以生成参数表(会将参数放在 MethodParameters 中), 反射时就可以拿到参数名m
- 如果编译时添加了 -g ( javac -g 类) 可以生成调试信息, 但分为两种情况
- 普通类, 会包含局部变量表(LocalVariableTable), 用 ASM 可以拿到参数名,例如通过ASM库 LocalVariableTableParameterNameDiscoverer
- 接口, 不会包含局部变量表, 无法获得参数名**(本地变量表的局限性)**
- 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
// 1. 反射获取参数名
Method foo = itheima.a22.Bean2.class.getMethod("foo", String.class, int.class);
for (Parameter parameter : foo.getParameters()) {
System.out.println(parameter.getName());
}
// 2. 基于 LocalVariableTable 本地变量表
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(foo);
System.out.println(Arrays.toString(parameterNames));
3.5 对象绑定与类型转换
3.5.1 底层第一套转换接口与实现
- Printer 把其它类型转为 String
- Parser 把 String 转为其它类型
- Formatter 综合 Printer 与 Parser 功能(spring提供)
- Converter 把类型 S 转为类型 T
- Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
- FormattingConversionService 利用其它们实现转换
3.5.2 底层第二套转换接口
- PropertyEditor 把 String 与其它类型相互转换(jdk提供,类似于Formatter)
- PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
- 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
3.5.3 高层接口与实现
-
它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverterDelegate 委派 ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)
- 首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
- 再看有没有 ConversionService 转换
- 再利用默认的 PropertyEditor 转换(DefaultFormattingConversionService)
- 最后有一些特殊处理
-
SimpleTypeConverter 仅做类型转换
-
BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property(属性的set方法)
-
DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field
-
ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能
3.5.4 类型转换与数据绑定
3.5.4.1 SimpleTypeConverter
// 仅有类型转换的功能
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
Integer number = typeConverter.convertIfNecessary("13", int.class);
Date date = typeConverter.convertIfNecessary("1999/03/04", Date.class);
System.out.println(number);
System.out.println(date);
3.5.4.2 BeanWrapperImpl
public static void main(String[] args) {
// 利用反射原理, 为 bean 的属性赋值
MyBean target = new MyBean(); // MyBean需要提供set方法
BeanWrapperImpl wrapper = new BeanWrapperImpl(target);
wrapper.setPropertyValue("a", "10");
wrapper.setPropertyValue("b", "hello");
wrapper.setPropertyValue("c", "1999/03/04");
System.out.println(target);
}
3.5.4.3 DirectFieldAccessor
public static void main(String[] args) {
// 利用反射原理, 为 bean 的属性赋值
MyBean target = new MyBean();
DirectFieldAccessor accessor = new DirectFieldAccessor(target);
accessor.setPropertyValue("a", "10");
accessor.setPropertyValue("b", "hello");
accessor.setPropertyValue("c", "1999/03/04");
System.out.println(target);
}
3.5.4.4 ServletRequestDataBinder
public static void main(String[] args) {
// web 环境下数据绑定,ServletRequestDataBinder 的核心职责就是把 HTTP 请求里的参数(ServletRequest.getParameterMap())映射到目标对象(Bean)的属性上
MyBean target = new MyBean(); // Bean休要提供set方法
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("a", "10");
request.setParameter("b", "hello");
request.setParameter("c", "1999/03/04");
// 从请求中进行 Bean的绑定的属性绑定和类型转换(request的参数全是string类型)
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
3.5.5 数据绑定工厂
public static void main(String[] args) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "西安");
User target = new User();
// "1. 用工厂, ServletRequestDataBinderFactory 无转换功能"
/*ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);*/
// "2. 用 @InitBinder 转换" PropertyEditorRegistry PropertyEditor
/*InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null);*/
// "3. 用 ConversionService 转换" ConversionService Formatter
/*FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);*/
// "4. 同时加了 @InitBinder 和 ConversionService"
/*InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), initializer);*/
// "5. 使用默认 ConversionService 转换"
DefaultFormattingConversionService service = new DefaultFormattingConversionService(); // 默认的ConversionService,需要搭配注解使用,如 @DateTimeFormat(Spring)
// ApplicationConversionService service = new ApplicationConversionService(); // 默认的ConversionService,需要搭配注解使用,如 @DateTimeFormat(springboot )
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(target);
}
ServletRequestDataBinderFactory 的用法和扩展点
- 可以解析控制器的 @InitBinder 标注方法作为扩展点,添加自定义转换器
- 控制器私有范围
- 可以通过 ConfigurableWebBindingInitializer 配置 ConversionService 作为扩展点,添加自定义转换器
- 公共范围
- 同时加了 @InitBinder 和 ConversionService 的转换优先级
- 优先采用 @InitBinder 的转换器
- 其次使用 ConversionService 的转换器
- 使用默认转换器
- 特殊处理(例如有参构造)
- @DateTimeFormat 注解解析
- spring 通过 DefaultFormattingConversionService 识别该注解,并针对该注解进行类型的转换
- springboot 通过 ApplicationConversionService 识别该注解…
- 两个类都是继承 FormattingConversionService (第一套底层实现)
3.5.6 获取泛型参数(两种方法)
3.5.6.1 java api 获取泛型参数
// 示例类
class TeacherDao extends BaseDao<Teacher> {
}
- 反射调用 getGenericSuperclass 才能获得父类的泛型信息,getSuperclass 并不会获得泛型信息。Type 是 Java 中所有类型的通用表示
- 要通过 instanceof 进行类型转化的判断,将 Type 转为他的子类 ParameterizedType (专门用于表示带有参数的泛型类型)
- parameterizedType.getActualTypeArguments()[0]
// 获取继承的父类泛型信息
Type type = TeacherDao.class.getGenericSuperclass();
System.out.println(type); // com.itheima.a23.sub.BaseDao<com.itheima.a23.sub.Teacher>
// 获取参数的泛型
if (type instanceof ParameterizedType parameterizedType) {
System.out.println(parameterizedType.getActualTypeArguments()[0]); // class com.itheima.a23.sub.Teacher
}
3.5.6.2 spring api 获取泛型参数
- GenericTypeResolver.resolveTypeArgument(子类型class,父类型class)
- GenericTypeResolver.resolveTypeArguments(子类型class,父类型class)
Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class);
System.out.println(t); // class com.itheima.a23.sub.Teacher
3.6 @ControllerAdvice 之 @InitBinder
ControllerAdvice 和 AOP 没有关系,这里取名Advice只是《功能增强》的形容,当 RequestMappingHandlerAdapter 进行 afterPropertiesSet 初始化时,会去扫描所有的 ControllerAdviceBean ,进行功能增强(例如:@InitBinder,@ModelAttribute,@ResponseBodyAdvice, @ExceptionHandler)
准备 @InitBinder 在整个 HandlerAdapter 调用过程中所处的位置
- RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
- HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
- HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers
总结:
- RequestMappingHandlerAdapter 不是直接调用 method的,他会先进行一些初始化,完成功能的扩展
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @InitBinder 方法,存放在 initBinderAdviceCache 中
- RequestMappingHandlerAdapter 会以类为单位,在该** 类首次使用时解析**此类的 @InitBinder 方法,存放在 initBinderCache 中
- 以上两种 @InitBinder 的解析结果都会缓存来避免重复解析,存放在各自的缓存 Map 中
- 控制器方法调用时,会综合利用本类的 @InitBinder 方法和 @ControllerAdvice 中的 @InitBinder 方法创建绑定工厂
3.7 控制器方法执行流程
3.7.1 ServletInvocableHandlerMethod
1.HandlerMethod 需要
- bean 就是对应的 Controller 对象
- method 是对应的方法对象
2.ServletInvocableHandlerMethod 继承 HandlerMethod, ServletInvocableHandlerMethod 需要
- WebDataBinderFactory 负责对象绑定、类型转换
- ParameterNameDiscoverer 负责参数名解析
- HandlerMethodArgumentResolverComposite 负责参数解析
- HandlerMethodReturnValueHandlerComposite 负责处理返回值
3.7.2 初始化@ControllerAdvice
3.7.3 HandlerAdapter整体调用流程
3.8 @ControllerAdvice 之 @ModelAttribute
参数中的 @ModelAttribute 注解是进行对象的创建,然后根据数据绑定,给空对象的参数进行绑定,最终得到的结作为模型数据放入到 ModelAndView 中,名字可以根据 @ModelAttribute指定,不指定的话就以对象的类型首字母小写。是通过参数解析器完成的
方法中的 @ModelAttribute 注解是进行 Model 的创建,然后添加到 ModelAndViewContainer 中。名字也是可以根据注解指定,不指定就采用方法名。是通过 RequestMappingHandlerAdapter 初始化的时候根据 modelFactory 进行解析的
紫色区域为 RequestMappingHandlerAdapter 初始化时,进行的部分功能增强(对象绑定,模型数据),然后再进行的方法调用
总结:
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
- RequestMappingHandlerAdapter 会以类为单位,在该**类首次使用时解析**此类的 @ModelAttribute 方法
- 以上两种 @ModelAttribute 的解析结果都会**缓存来避免重复解析**(modelAttributeCache / modelAttributeAdviceCache)
- 控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂
- @InitBinder 和 @ModelAttribute 都是扩展方法,加在@ControllerAdvice和@Controller中是不一样的结果
3.9 返回值处理器(组合器设计模式) \textcolor{green}{3.9\ 返回值处理器(组合器设计模式)} 3.9 返回值处理器(组合器设计模式)
总结:
- 常见的返回值处理器
- ModelAndView,分别获取其模型和视图名,放入 ModelAndViewContainer
- 返回值类型为 String 时,把它当做视图名,放入 ModelAndViewContainer
- 返回值添加了 @ModelAttribute 注解时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值省略 @ModelAttribute 注解且返回非简单类型时,将返回值作为模型,放入 ModelAndViewContainer
- 此时需找到默认视图名
- 返回值类型为 ResponseEntity 时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true,表示请求已经处理完毕,springmvc不需要在进行视图解析
- 返回值类型为 HttpHeaders 时
- 会设置 ModelAndViewContainer.requestHandled 为 true
- 返回值添加了 @ResponseBody 注解时
- 此时走 MessageConverter, 并设置 ModelAndViewContainer.requestHandled 为 true,表示请求已经处理完毕,springmvc不需要再进行视图解析
- **组合器设计模式(composite设计模式)**在 Spring 中的体现 + 1,在 Spring 中的体现(HandlerMethodReturnValueHandlerComposite 多个解析器组合)
- spring 常用的 java对象 和 json 数据的转换器:MappingJackson2HttpMessageConverter
3.10 MessageConverter(消息转换器)
总结:
- MessageConverter 的作用
- @ResponseBody 是返回值处理器解析的,但具体转换工作是 MessageConverter 做的
- MessageConverter 将输入或输出的对象转换为我们指定的 MediaType 类型
- 如何选择 MediaType
- 首先看 @RequestMapping 上有没有指定,服务端指定了返回类型(@RequestMapping(produces = “application/json”))
- 其次看 request 的 Accept 头有没有指定(客服端需要什么类型)
- 最后按返回值处理器中 MessageConverter 的添加顺序, 谁能谁先转换(都没有指定则按converter的顺序执行)
- 常见的消息转换器
- MappingJackson2HttpMessageConverter
- MappingJackson2XmlHttpMessageConverter
- FormHttpMessageConverter(HTML表单数据与 Java 对象之间进行转换)
3.11 @ControllerAdvice 之 @ResponseBodyAdvice
ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置
总结:
- 实现 ResponseBodyAdvice 接口,并且实现方法 supports 和 beforeBodyWrite
- supports 方法中不仅要查询该方法是否包含 @Responsebody 注解,还要去类上查找。利用 spring 提供的 AnnotationUtils 的 findAnnotation 可以查看注解及其包含的注解(@RestController = @ResponseBody + @Controller)
- ResponseBodyAdvice 返回响应体前包装(功能增强,统一包装返回数据的类型 ==> 统一响应结果Result)
3.12 异常解析器
ExceptionHandlerExceptionResolver总结:
- 它能够重用参数解析器、返回值处理器,实现组件重用
- 它能够支持嵌套异常
- @ExceptionHandler + @Controller 局部生效
- @ExceptionHandler + @ControllerAdvice 全局生效
3.13 @ControllerAdvice 之 @ExceptionHandler
总结:
- ExceptionHandlerExceptionResolver 初始化时会解析 @ControllerAdvice 中的 @ExceptionHandler 方法
- ExceptionHandlerExceptionResolver 会以类为单位,在该类首次处理异常时,解析此类的 @ExceptionHandler 方法
- 以上两种 @ExceptionHandler 的解析结果都会缓存来避免重复解析
- @ExceptionHandler 不同于 HandlerAdpter 的逻辑
3.14 Tomcat 异常处理
- 我们知道 @ExceptionHandler 只能处理发生在 mvc 流程中的异常,例如控制器内、拦截器内,那么如果是 Filter 出现了异常,如何进行处理呢?
- 在 Spring Boot 中,是这么实现的:
- 因为内嵌了 Tomcat 容器,因此可以配置 Tomcat 的错误页面,Filter 与 错误页面之间是通过请求转发跳转的,可以在这里做手脚
- 先通过 ErrorPageRegistrarBeanPostProcessor 这个后处理器配置错误页面地址,默认为
/error
也可以通过${server.error.path}
进行配置- 也可以通过 ErrorPageRegistrar 修改 Tomcat 服务器默认错误地址
- 当 Filter 发生异常时,不会走 Spring 流程,但会走 Tomcat 的错误处理,于是就希望转发至
/error
这个地址- 当然,如果没有 @ExceptionHandler,那么最终也会走到 Tomcat 的错误处理
- Spring Boot 又提供了一个 BasicErrorController,它就是一个标准 @Controller,@RequestMapping 配置为
/error
,所以处理异常的职责就又回到了 Spring- new BasicErrorController(new DefaultErrorAttributes(), new ErrorProperties());
- 具体异常信息会由 DefaultErrorAttributes 封装好,其中 DefaultErrorAttributes 默认返回四个有关于异常的信息。如果需要更多的信息,可以通过设置 ErrorProperties 读取配置文件,返回想要的异常信息
- 异常信息由于会被 Tomcat 放入 request 作用域,因此 BasicErrorController 里也能获取到
- BasicErrorController 通过 Accept 头判断需要生成哪种 MediaType 的响应
- 如果要的不是 text/html,走 MessageConverter 流程
- 如果需要 text/html,走 mvc 流程,此时又分两种情况
- 配置了 ErrorViewResolver,根据状态码去找 View
- 没配置或没找到,用 BeanNameViewResolver 根据一个固定为 error 的名字找到 View,即所谓的 WhitelabelErrorView
3.14.1 ErrorPageRegistrarBeanPostProcessor
@Bean // ⬅️修改了 Tomcat 服务器默认错误地址
public ErrorPageRegistrar errorPageRegistrar() { // 出现错误,会使用请求转发 forward 跳转到 error 地址
return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}
@Bean // ⬅️TomcatServletWebServerFactory 初始化前用它增强, 注册所有 ErrorPageRegistrar
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
return new ErrorPageRegistrarBeanPostProcessor();
}
总结:
- 先通过 ErrorPageRegistrarBeanPostProcessor 这个后处理器配置错误页面地址,默认为
/error
也可以通过${server.error.path}
进行配置 - 也可以通过 ErrorPageRegistrar 修改 Tomcat 服务器默认错误地址
3.14.2 BasicErrorController
@Bean // ⬅️ErrorProperties 封装环境键值, ErrorAttributes 控制有哪些错误信息
public BasicErrorController basicErrorController() {
ErrorProperties errorProperties = new ErrorProperties();
// 可以通过配置进行设置(返回的错误属性包含异常内容)。DefaultErrorAttributes,里面默认返回四个异常属性信息
errorProperties.setIncludeException(true);
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}
@Bean // ⬅️名称为 error 的视图, 作为 BasicErrorController 的 text/html 响应结果
public View error() {
return new View() {
@Override
public void render(
Map<String, ?> model,
HttpServletRequest request,
HttpServletResponse response
) throws Exception {
System.out.println(model);
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("""
<h3>服务器内部错误</h3>
""");
}
};
}
@Bean // ⬅️收集容器中所有 View 对象, bean 的名字作为视图名
public ViewResolver viewResolver() {
return new BeanNameViewResolver();
}
总结:
-
BasicErrorController就是采用 Tomcat 的请求转发 forward 跳转到 error 地址
-
Spring Boot 中 BasicErrorController 首先进行路径映射:KaTeX parse error: Expected '}', got 'EOF' at end of input: …ver.error.path:{error.path:/error}}
-
根据 MediaType 类型进行返回
-
① 是 text/html 形式(可以通过 errorProperties 添加属性展示内容)
-
② 是 json 形式返回(必须要添加一个名为 view 的视图。添加视图又必须添加一个视图名字解析器(收集 View 对象,bean 的名字作为视图名))
-
3.15 映射器和适配器(以前的实现方式)
3.15.1 BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
@Bean
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
@Bean("/c3")
public Controller controller3() {
return (request, response) -> {
response.getWriter().print("this is c3");
return null;
};
}
总结:
- BeanNameUrlHandlerMapping,以 ‘/’ 开头的 bean 的名字会被当作映射路径
- 这些 bean 本身当作 handler,要求实现 Controller 接口
- SimpleControllerHandlerAdapter,调用 handler
3.15.2 RouterFunctionMapping 与 HandlerFunctionAdapter
@Bean
public RouterFunctionMapping routerFunctionMapping() {
return new RouterFunctionMapping();
}
@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {
return new HandlerFunctionAdapter();
}
@Bean
public RouterFunction<ServerResponse> r1() {
// ⬇️映射条件 ⬇️handler(需要实现HandlerFunction接口)
return RouterFunctions.route(RequestPredicates.GET("/r1"), new HandlerFunction<ServerResponse>() {
@Override
public ServerResponse handle(ServerRequest request) throws Exception {
return ServerResponse.ok().body("this is r1");
}
});
}
总结:
- RouterFunctionMapping, 通过 RequestPredicate 条件映射
- handler 要实现 HandlerFunction 接口
- HandlerFunctionAdapter, 调用 handler
函数式控制器
a. RouterFunctionMapping, 通过 RequestPredicate 映射
b. handler 要实现 HandlerFunction 接口
c. HandlerFunctionAdapter, 调用 handler
对比
a. RequestMappingHandlerMapping, 以 @RequestMapping 作为映射路径
b. 控制器的具体方法会被当作 handler
c. RequestMappingHandlerAdapter, 调用 handler
3.15.3 SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
// SimpleUrlHandlerMapping 没有 afterProperties 初始化方法,所以需要手动建立映射关系 映射路径(String)和ResourceHttpRequestHandler的关系
Map<String, ResourceHttpRequestHandler> map
= context.getBeansOfType(ResourceHttpRequestHandler.class);
handlerMapping.setUrlMap(map);
return handlerMapping;
}
@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
return new HttpRequestHandlerAdapter();
}
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
return handler;
}
@Bean("/img/**")
public ResourceHttpRequestHandler handler2() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("images/")));
return handler;
}
总结:
- SimpleUrlHandlerMapping 不会在初始化时收集映射信息,需要手动收集,没有 afterProperties 初始化方法,所以需要手动建立映射关系
- 映射路径(String ) 和 ResourceHttpRequestHandler(控制器方法) 的关系
- ResourceHttpRequestHandler 作为静态资源 handler
- HttpRequestHandlerAdapter, 调用此 handler
3.15.3.1 静态资源解析优化(责任链模式) \textcolor{green}{3.15.3.1\ 静态资源解析优化(责任链模式)} 3.15.3.1 静态资源解析优化(责任链模式)
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
handler.setResourceResolvers(List.of(
// ⬇️缓存优化
new CachingResourceResolver(new ConcurrentMapCache("cache1")),
// ⬇️压缩优化
new EncodedResourceResolver(),
// ⬇️磁盘路径资源解析
new PathResourceResolver()
));
return handler;
}
总结:
- ResourceHttpRequestHandler 资源解析器默认只有 PathResourceResolver (磁盘路径解析),我们常用的资源解析器还有 CachingResoutceResolver(缓存文件) 和 EncodedRescourceResolver(压缩文件,压缩文件需要自己生成压缩文件)
- 静态资源解析就是责任链设计模式的一个体现
3.15.4 WelcomePageHandlerMapping(欢迎页)
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
return new WelcomePageHandlerMapping(null, context, resource, "/**");
// WelcomePageHandlerMapping 会生成 ==> ParameterizableViewController ==> 这个处理器会固定跳转到视图 forward:index.html
}
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter(); // SimpleControllerHandlerAdapter 识别Controller
}
总结:
- 欢迎页支持静态欢迎页与动态欢迎页
- WelcomePageHandlerMapping 映射欢迎页(即只映射 ‘/’,例如 localhost:8080/ )
- 它内置的 handler ParameterizableViewController 作用是不执行逻辑,仅根据视图名找视图
- 视图名固定为 forward:index.html
- WelcomePageHandlerMapping 会生成 ParameterizableViewController (实现了Controller接口)
- SimpleControllerHandlerAdapter, 调用 handler
- 转发至 /index.html
- 处理 /index.html 又会走上面的静态资源处理流程
3.16 映射器与适配器小结(五大映射器,四大适配器) \textcolor{red}{3.16\ 映射器与适配器小结(五大映射器,四大适配器)} 3.16 映射器与适配器小结(五大映射器,四大适配器)
- HandlerMapping 负责建立请求与控制器之间的映射关系
- RequestMappingHandlerMapping (与 @RequestMapping 匹配)
- WelcomePageHandlerMapping ( / )
- BeanNameUrlHandlerMapping (与 bean 的名字匹配 以 / 开头)
- RouterFunctionMapping (函数式 RequestPredicate, HandlerFunction)
- SimpleUrlHandlerMapping (静态资源 通配符 /** /img/**)
- 映射器之间也会有顺序问题, boot 中默认顺序如上
- HandlerAdapter 负责实现对各种各样的 handler 的适配调用
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
- 参数解析器、返回值处理器体现了组合模式
- SimpleControllerHandlerAdapter 处理:Controller 接口
- HandlerFunctionAdapter 处理:HandlerFunction 函数式接口
- HttpRequestHandlerAdapter 处理:HttpRequestHandler 接口 (静态资源处理)
- 这也是典型适配器模式体现
- ResourceHttpRequestHandler.setResourceResolvers 这是典型责任链模式体现(CachingResourceResolver, EncodedResourceResolver, PathResourceResolver)
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
3.17 M V C 处理流程 \textcolor{BLUE}{3.17\ MVC\ 处理流程} 3.17 MVC 处理流程
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
-
服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
- 路径:默认映射路径为
/
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器- jsp 不会匹配到 DispatcherServlet
- 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
- 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean
- 初始化:根据初始化时机进行初始化,DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
- HandlerMapping,初始化时记录映射关系
- HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
- HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
- ViewResolver,视图解析器
- 路径:默认映射路径为
-
DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
-
例如根据 /hello 路径找到 @RequestMapping(“/hello”) 对应的控制器方法
-
控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
-
HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
-
-
DispatcherServlet 接下来会:
- 调用拦截器的 preHandle 方法(true就是被拦截,false表示放行)
- RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
- @ControllerAdvice 全局增强点1️⃣:补充模型数据(@ModelAttribute)
- @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器(@InitBinder)
- 使用 HandlerMethodArgumentResolver 准备参数
- @ControllerAdvice 全局增强点3️⃣:RequestBody 增强(@RequestBody 请求体增强)
- 调用 ServletInvocableHandlerMethod
- 使用 HandlerMethodReturnValueHandler 处理返回值
- @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
- 根据 ModelAndViewContainer 获取 ModelAndView
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 例如,有的返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
- 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 调用拦截器的 postHandle 方法
- 处理异常或视图渲染
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
- 正常,走视图解析及渲染流程
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- 调用拦截器的 afterCompletion 方法