RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
DispatcherServlet 的初始化
选择支持内嵌Tomcat 服务器的 Spring 容器作为 ApplicationContext 的实现:
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂、DispatcherServlet 和 DispatcherServlet 注册对象。
@Configuration
@ComponentScan
public class WebConfig {
/**
* 内嵌 Web 容器工厂
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
/**
* 创建 DispatcherServlet
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
/**
* 注册 DispatcherServlet,Spring MVC 的入口
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
}
控制台输出:
Tomcat initialized with port(s): 8080 (http)
Root WebApplicationContext: initialization completed in 2132 ms
从输出中可以发现,此时DispatcherServlet 还未被加载。
当Tomcat 服务器 首次 使用到 DispatcherServlet 时,才会由Tomcat 服务器初始化 DispatcherServlet。
断点 DispatcherServlet 的 onRefresh() 方法中 this.initStrategies(context); 的所在行:
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
查看调用栈可知,是从 GenericServlet 的 init() 方法执行到 onRefresh() 方法的。
因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。
使 DispatcherServlet 在Tomcat 服务器启动时被初始化
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
当存在多个 DispatcherServlet 需要被注册时,设置的 loadOnStartup 越大,优先级越小,初始化顺序越靠后。
再次重启程序,根据控制台输出的内容可知,不仅完成 Tomcat 和 Spring 容器的初始化,DispatcherServlet 也初始化成功。
抽取配置信息到配置文件中
使用 @PropertySource 注解设置配置类需要读取的配置文件,以便后续读取配置文件中的内容。
要读取配置文件中的内容,可以使用 @Value 注解,但该注解一次仅仅能够读取一个值,现实是往往需要从配置文件中读取多个值。
可以使用 @EnableConfigurationProperties 注解完成配置文件信息与对象的绑定,后续使用时作为 @Bean 注解标记的方法的参数直接在方法中使用即可:
@Configuration
@ComponentScan
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
......
/**
* 注册 DispatcherServlet,Spring MVC 的入口
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
return registrationBean;
}
}
DispatcherServlet 初始化时执行的操作
回到 DispatcherServlet 的 onRefresh() 方法,它又调用了 initStrategies() 方法:
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);
}
在这个方法中初始化了一系列组件,见名识意即可,重点介绍:
- initHandlerMappings():初始化处理器映射器
- initHandlerAdapters():初始化处理器适配器
- initHandlerExceptionResolvers():初始化异常处理器
在所有的初始化方法中都有一个相似的逻辑,首先使用一个布尔值判断是否检测 所有 目标组件。
Spring 支持父子容器嵌套,如果判断的布尔值为 true,那么 Spring 不仅会在当前容器中获取目标组件,还会在其所有父级容器中寻找(这个特性仅在 Spring MVC 中有效,Boot 中已被合并)。
以 initHandlerMappings() 为例:
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) { // 是否需要检测所有处理器映射器
// --snip--
} else {
// 无需检测所有处理器映射器时,获取当前容器中的处理器映射器
// --snip--
}
if (this.handlerMappings == null) {
// 当前容器中没有处理器映射器时,设置默认的处理器映射器
this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
// --snip--
}
// --snip--
}
RequestMappingHandlerMapping
HandlerMapping,即处理器映射器,用于建立请求路径与控制器方法的映射关系。
RequestMappingHandlerMapping 是 HandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。
当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。
信息: Initializing Spring DispatcherServlet 'dispatcherServlet'
[INFO ] Initializing Servlet 'dispatcherServlet'
[TRACE] No MultipartResolver 'multipartResolver' declared
[TRACE] No LocaleResolver 'localeResolver': using default [AcceptHeaderLocaleResolver]
[TRACE] No ThemeResolver 'themeResolver': using default [FixedThemeResolver]
[TRACE] No HandlerMappings declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No HandlerAdapters declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No HandlerExceptionResolvers declared in servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No RequestToViewNameTranslator 'viewNameTranslator': using default [DefaultRequestToViewNameTranslator]
[TRACE] No ViewResolvers declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties
[TRACE] No FlashMapManager 'flashMapManager': using default [SessionFlashMapManager]
[INFO] Completed initialization in 482 ms
在配置类中将 RequestMappingHandlerMapping 添加到 Spring 容器:
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
接下来用 Spring 提供的测试类来测试:
HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
[DEBUG] 16:51:35.349 [main] com.itheima.a20.Controller1 - test1()
getHandler() 方法返回的对象时处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息。
:::info
MockHttpServletRequest 的使用需要导入以下依赖:
:::
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
RequestMappingHandlerAdapter
RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, ((HandlerMethod) handlerMapping.getHandler(request).getHandler()));
自定义参数解析器
假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记控制器方法的哪个参数来获取 Token 信息:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}
自定义参数解析器:
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
@Override//判断是否包含注解
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(Token.class) != null;
}
@Override//解析内容
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return webRequest.getHeader("token");
}
}
将参数解析器添加到 HandlerAdapter 中:
adapter.setCustomArgumentResolvers(Collections.singletonList(tokenArgumentResolver));
参数解析器
测试数据:
static class Controller {
public void test(
@RequestParam("name1") String name1, // name1=张三
String name2, // name2=李四
@RequestParam("age") int age, // age=18
@RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
@RequestParam("file") MultipartFile file, // 上传文件
@PathVariable("id") int id, // /test/124 /test/{id}
@RequestHeader("Content-Type") String header,
@CookieValue("token") String token,
@Value("${JAVA_HOME}") String home2, // spring 获取数据 ${} #{}
HttpServletRequest request, // request, response, session ...
@ModelAttribute("abc") User user1, // name=zhang&age=18
User user2, // name=zhang&age=18
@RequestBody User user3 // json
) {
}
}
@Getter
@Setter
@ToString
static class User {
private String name;
private int age;
}
@RequestParam
@RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:
- beanFactory:Bean 工厂对象。需要解析 ${} 时,就需要指定 Bean 工厂对象
- useDefaultResolution:布尔类型参数。为 false 表示只解析添加了 @RequestParam 注解的参数,为 true 针对未添加 @RequestParam 注解的参数也使用该参数解析器进行解析。
RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:
- parameter:参数对象
- mavContainer:ModelAndView 容器,用来存储中间的 Model 结果
- webRequest:由 ServletWebRequest 封装后的请求对象
- binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 18 转换成整型
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
HttpServletRequest request = mockRequest();
// 控制器方法封装成 HandlerMethod
Method method = Controller.class.getMethod("test", String.class, String.class,
int.class, String.class, MultipartFile.class,
int.class, String.class, String.class,
String.class, HttpServletRequest.class, User.class,
User.class, User.class);
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), method);
// 准备对象绑定与类型转换
ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);
// 准备 ModelAndViewContainer 用来存储中间的 Model 结果
ModelAndViewContainer container = new ModelAndViewContainer();
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, true);
String annotations = Arrays.stream(parameter.getParameterAnnotations())
.map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining());
String appendAt = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
String paramInfo = "[" + parameter.getParameterIndex() + "] " + appendAt +
parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName();
if (resolver.supportsParameter(parameter)) {
Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(Objects.requireNonNull(v).getClass());
System.out.println(paramInfo + " -> " + v);
} else {
System.out.println(paramInfo);
}
}
}
多个参数解析器的组合 - 组合模式
不同种类的参数需要不同的参数解析器,当前使用的参数解析器不支持当前参数的解析时,就应该换一个参数解析器进行解析。
可以将所有参数解析器添加到一个集合中,然后遍历这个集合,实现上述需求。
Spring 提供了名为 HandlerMethodArgumentResolverComposite 的类,对上述逻辑进行封装。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, true)
);
// --snip--
if (composite.supportsParameter(parameter)) {
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(paramInfo + " -> " + v);
} else {
System.out.println(paramInfo);
}
}
}
@PathVariable
@PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。
@RequestHeader
@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
@CookieValue
@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
@Value
@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。
HttpServletRequest
HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:
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);
}
@ModelAttribute
@ModelAttribute 注解的解析需要使用到 ServletModelAttributeMethodProcessor 参数解析器。构造时需要传入一个布尔类型的值。为 false 时,表示 @ModelAttribute 不是不必须的,即是必须的。
针对 @ModelAttribute("abc") User user1 和 User user2 两种参数来说,尽管后者没有使用 @ModelAttribute 注解,但它们使用的是同一种解析器。
添加两个 ServletModelAttributeMethodProcessor 参数解析器,先解析带 @ModelAttribute 注解的参数,再解析不带 @ModelAttribute 注解的参数。
通过 ServletModelAttributeMethodProcessor 解析得到的数据还会被存入 ModelAndViewContainer 中。存储的数据结构是一个 Map,其 key 为 @ModelAttribute 注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。
[0] @RequestParam String name1 -> zhangsan
[1] String name2
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@2beee7ff
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5fa07e12
[10] @ModelAttribute User user1 -> A21.User(name=张三, age=18)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
[11] User user2 -> A21.User(name=张三, age=18)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
@RequestBody User user3 参数也被 ServletModelAttributeMethodProcessor 解析了,如果想使其数据通过 JSON 数据转换而来,则需要使用另一个参数解析器。
@RequestBody
@RequestBody 注解的解析需要使用到 RequestResponseBodyMethodProcessor 参数解析器。构造时需要传入一个消息转换器列表。
@RequestBody User user3 参数数据通过 JSON 数据得到,与上一节的解析进行区分。
除此之外,添加的参数解析器顺序也影响着解析结果:
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)
先添加解析 @ModelAttribute 注解的解析器,再添加解析 @RequestBody 注解的解析器,最后添加解析省略了 @ModelAttribute 注解的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor。
因此 String name2 参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:
[0] @RequestParam String name1 -> zhangsan
[1] String name2 -> lisi
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645
[10] @ModelAttribute User user1 -> A21.User(name=张三, age=18)
[11] User user2 -> A21.User(name=张三, age=18)
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)
获取参数名
在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译:
package indi.mofan.a22;
public class Bean2 {
public void foo(String name, int age) {
}
}
将命令行切换到 Bean2.java
文件所在目录的位置,执行 javac .\Bean2.java
命令手动编译 Bean2.java
。查看 Bean2.class
文件的内容:
package indi.mofan.a22;
public class Bean2 {
public Bean2() {
}
public void foo(String var1, int var2) {
}
}
编译生成的 class
文件中的 foo()
方法的参数名称不再是 name
和 age
,也就是说直接使用 javac
命令进行编译得到的字节码文件不会保存方法的参数名称。
执行 javac -parameters .\Bean2.java
再次编译 Bean2.java
,并查看得到的 Bean2.class
文件内容:
package indi.mofan.a22;
public class Bean2 {
public Bean2() {
}
public void foo(String name, int age) {
}
}
foo()
方法的参数名称得以保留。
还可以使用 javap -c -v .\Bean2.class
命令反编译 Bean2.class
,foo()
方法的反编译结果如下:
public void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 6: 0
MethodParameters:
Name Flags
name
age
foo()
方法的参数信息被保存在 MethodParameters
中,可以使用 反射 获取.
使用 javac -g .\Bean2.java
命令进行编译也会保留方法的参数信息。再次使用 javap
反编译 Bean2.class
,foo()
方法的反编译结果如下:
public void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lindi/mofan/a22/Bean2;
0 1 1 name Ljava/lang/String;
0 1 2 age I
foo()
方法的参数信息被保存在 LocalVariableTable
中,不能使用反射获取,但可以使用 ASM 获取,使用 Spring 封装的解析工具:
public static void main(String[] args) throws Exception {
// 反射获取参数名
Method foo = Bean2.class.getMethod("foo", String.class, int.class);
// 基于 LocalVariableTable 本地变量表获取
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(foo);
System.out.println(Arrays.toString(parameterNames));
}
DefaultParameterNameDiscoverer
将两种实现进行了统一:
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
:::infojavac -g
的局限性:只限类使用,接口无法使用
:::
对象绑定与类型转换
底层第一套转换接口与实现(Spring)
Printer
把其它类型转为String
Parser
把String
转为其它类型Formatter
综合Printer
与Parser
的功能Converter
把类型S
转为类型T
Printer
、Parser
、Converter
经过适配转换成GenericConverter
放入Converters
集合FormattingConversionService
利用其它接口实现转换
底层第二套转换接口(JDK)
PropertyEditor
将String
与其它类型相互转换PropertyEditorRegistry
可以注册多个PropertyEditor
对象- 可以通过
FormatterPropertyEditorAdapter
与第一套接口进行适配
高层转换接口与实现
- 它们都实现了
TypeConverter
高层转换接口,在转换时会用到TypeConverterDelegate
委派ConversionService
与PropertyEditorRegistry
真正执行转换(使用 Facade 门面模式) - 首先查看是否存在实现了
PropertyEditorRegistry
的自定义转换器,@InitBinder
注解实现的就是自定义转换器(用了适配器模式把Formatter
转为需要的PropertyEditor
) - 再查看是否存在
ConversionService
实现 - 再利用默认的
PropertyEditor
实现 - 最后有一些特殊处理
SimpleTypeConverter
仅做类型转换BeanWrapperImpl
利用Property
,即Getter/Setter
,为 Bean 的属性赋值,,必要时进行类型转换DirectFieldAccessor
利用Field
,即字段,为 Bean 的字段赋值,必要时进行类型转换ServletRequestDataBinder
为 Bean 的属性执行绑定,必要时进行类型转换,根据布尔类型成员变量directFieldAccess
选择利用Property
还是Field
,还具备校验与获取校验结果功能
ControllerAdvice 之 @InitBinder
@InitBinder
注解只能作用在方法上,通常搭配 @ControllerAdvice
和 @Controller
以及他们的衍生注解使用。比如:
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@InitBinder
public void binder3(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
}
}
@Controller
static class Controller1 {
@InitBinder
public void binder1(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
}
public void foo() {
}
}
@Controller
static class Controller2 {
@InitBinder
public void binder21(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder21 转换器"));
}
@InitBinder
public void binder22(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder22 转换器"));
}
public void foo() {
}
}
}
当 @InitBinder
作用的方法存在于被 @ControllerAdvice
标记的类里面时,是对 所有 控制器都生效的自定义类型转换器。当 @InitBinder
作用的方法存在于被 @Controller
标记的类里面时,是 只对当前 控制器生效的自定义类型转换器。@InitBinder
的来源有两个:
@ControllerAdvice
标记的类中@InitBinder
标记的方法,由RequestMappingHandlerAdapter
在初始化时解析并记录@Controller
标记的类中@InitBinder
标记的方法,由RequestMappingHandlerAdapter
在控制器方法首次执行时解析并记录
控制器方法执行流程
ServletInvocableHandlerMethod
的组成:HandlerMethod
需要:
bean
,即哪个 Controllermethod
,即 Controller 中的哪个方法
ServletInvocableHandlerMethod
需要:
WebDataBinderFactory
,用于对象绑定、类型转换ParameterNameDiscoverer
,用于参数名解析HandlerMethodArgumentResolverComposite
,用于解析参数HandlerMethodReturnValueHandlerComposite
,用于处理返回值
控制器方法执行流程:
以 RequestMappingHandlerAdapter
为起点,创建 WebDataBinderFactory
,添加自定义类型转换器,再创建 ModelFactory
,添加 Model 数据。
接下来调用 ServletInvocableHandlerMethod
,主要完成三件事:
- 准备参数
- 反射调用控制器方法
- 处理返回值
ControllerAdvice 之 @ModelAttribute
准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置:@ModelAttribute
可以作用在参数上和方法上。
当其作用在参数上时,会将请求中的参数信息 按名称 注入到指定对象中,并将这个对象信息自动添加到 ModelMap 中。当未指定 @ModelAttribute
的 value
时,添加到 ModelMap 中的 key 是对象类型首字母小写对应的字符串。此时的 @ModelAttribute
注解由 ServletModelAttributeMethodProcessor
解析。
当其作用在方法上时:
- 如果该方法在被
@Controller
注解标记的类中,会在当前控制器中每个控制器方法执行前执行被@ModelAttribute
标记的方法,如果该方法有返回值,自动将返回值添加到 ModelMap 中。当未指定@ModelAttribute
的value
时,添加到 ModelMap 中的 key 是返回值类型首字母小写对应的字符串。 - 如果该方法在被
@ControllerAdvice
注解标记的类中,会在所有控制器方法执行前执行该方法。
作用在方法上的 @ModelAttribute
注解由 RequestMappingHandlerAdapter
解析。
返回值处理器
测试类:
@Slf4j
static class Controller {
public ModelAndView test1() {
log.debug("test1()");
ModelAndView mav = new ModelAndView("view1");
mav.addObject("name", "张三");
return mav;
}
public String test2() {
log.debug("test2()");
return "view2";
}
@ModelAttribute
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}
public User test4() {
log.debug("test4()");
return new User("王五", 30);
}
public HttpEntity<User> test5() {
log.debug("test5()");
return new HttpEntity<>(new User("赵六", 40));
}
public HttpHeaders test6() {
log.debug("test6()");
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/html");
return headers;
}
@ResponseBody
public User test7() {
log.debug("test7()");
return new User("钱七", 50);
}
}
@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
private String name;
private int age;
}
为测试渲染视图(MVC 早期使用)需要额外引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
相关配置:
@Configuration
public class WebConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setDefaultEncoding("utf-8");
configurer.setTemplateLoaderPath("classpath:templates");
return configurer;
}
/**
* FreeMarkerView 在借助 Spring 初始化时,会要求在 web 环境才会走 setConfiguration, 这里想办法去掉了 web 环境的约束
*/
@Bean
public FreeMarkerViewResolver viewResolver(FreeMarkerConfigurer configurer) {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver() {
@Override
protected AbstractUrlBasedView instantiateView() {
FreeMarkerView view = new FreeMarkerView() {
@Override
protected boolean isContextRequired() {
return false;
}
};
view.setConfiguration(configurer.getConfiguration());
return view;
}
};
resolver.setContentType("text/html;charset=utf-8");
resolver.setPrefix("/");
resolver.setSuffix(".ftl");
resolver.setExposeSpringMacroHelpers(false);
return resolver;
}
}
FreeMaker 使用方法:
@SuppressWarnings("all")
private static void renderView(ApplicationContext context, ModelAndViewContainer container,
ServletWebRequest webRequest) throws Exception {
log.debug(">>>>>> 渲染视图");
FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class);
String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator().getViewName(webRequest.getRequest());
log.debug("没有获取到视图名, 采用默认视图名: {}", viewName);
// 每次渲染时, 会产生新的视图对象, 它并非被 Spring 所管理, 但确实借助了 Spring 容器来执行初始化
View view = resolver.resolveViewName(viewName, Locale.getDefault());
view.render(container.getModel(), webRequest.getRequest(), webRequest.getResponse());
System.out.println(new String(((MockHttpServletResponse) webRequest.getResponse()).getContentAsByteArray(), StandardCharsets.UTF_8));
}
ModelAndView
ModelAndView
类型的返回值由 ModelAndViewMethodReturnValueHandler
处理,构造时无需传入任何参数。
解析 ModelAndView
时,将其中的视图和模型数据分别提取出来,放入 ModelAndViewContainer
中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。
字符串类型
控制器方法的返回值是字符串类型时,返回的字符串即为视图的名称。与 ModelAndView
类型的返回值相比,不包含模型数据。
此种类型的返回值由 ViewNameMethodReturnValueHandler
处理,构造时无需传入任何参数。
@ModelAttribute
当 @ModelAttribute
注解作用在方法上时,会将方法的返回值作为模型数据添加到 ModelAndViewContainer
中。@ModelAttribute
标记的方法的返回值由 ServletModelAttributeMethodProcessor
解析,构造时需要传入一个布尔类型数据 annotationNotRequired
,表示 @ModelAttribute
注解是否不是必须的。
模型数据已经有了,但视图名称又是什么呢?
在实际开发场景中,控制器方法需要被 @RequestMapping
标记,并指定请求地址,比如:
@ModelAttribute
@RequestMapping("/test3")
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}
当未找到视图名称时,默认以请求路径作为视图名称。
但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。
private static Consumer<MockHttpServletRequest> mockHttpServletRequestConsumer(String methodName) {
return req -> {
req.setRequestURI("/" + methodName);
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(req);
};
}
private static void test3(ApplicationContext context) {
String methodName = "test3";
testReturnValueProcessor(context, methodName,
mockHttpServletRequestConsumer(methodName), null);
}
HttpEntity
HttpEntity
类型的返回值由 HttpEntityMethodProcessor
处理,构造时需要传入一个消息转换器列表。
这种类型的返回值表示响应完成,无需经过视图的解析、渲染流程再生成响应。可在处理器的 handleReturnValue()
方法中得以论证:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 一进入方法就设置请求处理完毕
mavContainer.setRequestHandled(true);
// --snip--
}
HttpEntity
中包含了状态码、响应体信息和响应头信息。
HttpHeaders
与 HttpEntity
相比,HttpHeaders
只包含响应头信息,HttpHeaders
类型的返回值由 HttpHeadersReturnValueHandler
处理,构造时无需传入任何参数。
与 HttpEntity
一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue()
方法中得以论证(省略源码)。
@ResponseBody
@ResponseBody
标记的方法的返回值由 RequestResponseBodyMethodProcessor
处理,构造时需要传入一个消息转换器列表。
这样的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue()
方法中得以论证(省略源码)。
ControllerAdvice 之 ResponseBodyAdvice
ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置:ResponseBodyAdvice
是一个接口,对于实现了这个接口并被 @ControllerAdvice
标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice
接口中的 beforeBodyWrite()
方法对返回值进行增强。
现有一个控制器类与内部使用到的 User
类:
@Controller
public static class MyController {
@ResponseBody
public User user() {
return new User("王五", 18);
}
}
@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
private String name;
private int age;
}
调用控制器方法,并输出响应数据:
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
context.getBean(WebConfig.MyController.class),
WebConfig.MyController.class.getMethod("user")
);
handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(Collections.emptyList(), null));
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
// 设置参数解析器(省略源码)
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
// 设置返回值处理器(省略源码)
handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandlers(context));
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request, response), container);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
context.close();
}
输出:
{"name":"王五","age":18}
在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result
类型:
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
private int code;
private String msg;
private Object data;
@JsonCreator
private Result(@JsonProperty("code") int code, @JsonProperty("data") Object data) {
this.code = code;
this.data = data;
}
private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
public static Result ok() {
return new Result(200, null);
}
public static Result ok(Object data) {
return new Result(200, data);
}
public static Result error(String msg) {
return new Result(500, "服务器内部错误:" + msg);
}
}
除了直接让控制器方法返回 Result
外,还可以使用 ResponseBodyAdvice
进行增强:
@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
/*
* 满足条件才转换
* 1. 控制器方法被 @ResponseBody 注解标记
* 2. 控制器方法所在类被 @ResponseBody 注解或包含 @ResponseBody 注解的注解标记
*/
return returnType.getMethodAnnotation(ResponseBody.class) != null
|| AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.ok(body);
}
}
{"code":200,"data":{"name":"王五","age":18}}
异常处理
DispatcherServlet
中对异常处理的核心方法是 processHandlerException()
,在这个方法中会对所有异常解析器进行遍历,然后使用每个异常解析器对异常信息进行处理。
存放异常解析器的是 DispatcherServlet
中泛型为 HandlerExceptionResolver
、名为 handlerExceptionResolvers
的列表成员变量。HandlerExceptionResolver
是一个接口,本节讲解解析 @ExceptionHandler
注解的异常解析器 ExceptionHandlerExceptionResolver
。
四个控制器类,测试异常处理方法被 @ResponseBody
注解标记、异常处理方法返回 ModelAndView
、嵌套异常和对异常处理方法的参数处理:
static class Controller1 {
public void foo() {
}
@ResponseBody
@ExceptionHandler
public Map<String, Object> handle(ArithmeticException e) {
return Collections.singletonMap("error", e.getMessage());
}
}
static class Controller2 {
public void foo() {
}
@ExceptionHandler
public ModelAndView handler(ArithmeticException e) {
return new ModelAndView("test2", Collections.singletonMap("error", e.getMessage()));
}
}
static class Controller3 {
public void foo() {
}
@ResponseBody
@ExceptionHandler
public Map<String, Object> handle(IOException e) {
return Collections.singletonMap("error", e.getMessage());
}
}
static class Controller4 {
public void foo() {}
@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(Exception e, HttpServletRequest request) {
System.out.println(request);
return Collections.singletonMap("error", e.getMessage());
}
}
@SneakyThrows
public static void main(String[] args) {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
// 调用该方法,添加默认的参数解析器和返回值处理器
resolver.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception e = new ArithmeticException("除以零");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
handlerMethod = new HandlerMethod(new Controller2(), Controller2.class.getMethod("foo"));
ModelAndView modelAndView = resolver.resolveException(request, response, handlerMethod, e);
System.out.println(modelAndView.getModel());
System.out.println(modelAndView.getViewName());
// 嵌套异常
handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
// 异常处理方法参数处理
handlerMethod = new HandlerMethod(new Controller4(), Controller4.class.getMethod("foo"));
e = new Exception("e4");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
TomCat 异常处理
可以利用 @ExceptionHandler
和 @ControllerAdvice
注解全局对控制器方法中抛出的异常进行处理,但针对诸如 filter
中不在控制器方法中的异常就变得无能为力了。
因此需要一个更上层的“异常处理者”,这个“异常处理者”就是 Tomcat 服务器。
错误页处理
首先将“老三样”利用配置类添加到 Spring 容器中,还要将 RequestMappingHandlerMapping
和 RequestMappingHandlerAdapter
也添加到 Spring 容器中。
必要的控制器也不能少,控制器方法手动制造异常,但不提供使用 @ExceptionHandler
实现的异常处理方法,将产生的异常交由 Tomcat 处理:
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
// 解析 @RequestMapping
return new RequestMappingHandlerMapping();
}
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
// 注意默认的 RequestMappingHandlerAdapter 不会带 jackson 转换器
handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}
@Controller
public static class MyController {
@RequestMapping("test")
public ModelAndView test() {
int i = 1 / 0;
return null;
}
}
}
在浏览器中访问 http://localhost:8080/test 地址:
显示 Tomcat 的错误处理页,并在页面中输出了错误信息。
Tomcat 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?
修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:
/**
* 修改了 Tomcat 服务器默认错误地址
*/
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
/*
* ErrorPageRegistrar 由 SpringBoot 提供,TomcatServletWebServerFactory 也实现了该接口
* 出现错误,会使用请求转发 forward 跳转到 error 地址
*/
return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}
@Bean
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
/*
* 在 TomcatServletWebServerFactory 初始化完成前,获取容器中所有的 ErrorPageRegistrar
* 并将这些 ErrorPageRegistrar 进行注册
*/
return new ErrorPageRegistrarBeanPostProcessor();
}
重启程序,再次在浏览器中访问 http://localhost:8080/test
,此时页面上不再显示 Tomcat 的默认错误处理页,而是产生了 404
错误。
这是因为整个程序中并没有名称为 error
的页面,或者为 /error
的请求路径。在控制器中添加请求路径为 /error
的控制器方法,该方法被 @ResponseBody
标记,最终返回 JSON 格式的数据:
@RequestMapping("/error")
@ResponseBody
public Map<String, Object> error(HttpServletRequest request) {
// tomcat 会将异常对象存储到 request 作用域中,可以直接获取
Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
return Collections.singletonMap("error", e.getMessage());
}
再次重启程序,控制台输出的路径映射信息多了一条:
映射路径: { [/error]} 方法信息: indi.mofan.a32.WebConfig$MyController#error(HttpServletRequest)
映射路径: { [/test]} 方法信息: indi.mofan.a32.WebConfig$MyController#test()
在浏览器中访问 http://localhost:8080/test
:
BasicErrorController
BasicErrorController
是由 SpringBoot 提供的类,它也是一个控制器:
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// --snip--
}
它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error
。
向容器中添加 BasicErrorController
,构造 BasicErrorController
时需要传递两个参数:
errorAttributes
:错误属性,可以理解成封装的错误信息对象errorProperties
:也可以翻译成错误属性,用于对输出的错误信息进行配置
移除前文添加的 error()
控制器方法。
再次重启程序,控制台输出的路径映射信息为:
映射路径: { [/error]} 方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
映射路径: { [/test]} 方法信息: indi.mofan.a32.WebConfig$MyController#test()
映射路径: { [/error], produces [text/html]} 方法信息: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
路径映射信息多了两条,它们的请求路径一样,但根据不同的请求来源返回不同格式的数据。
使用接口测试工具访问
如果采用 Postman 等接口测试工具访问 http://localhost:8080/test
路径时,将返回 JSON 格式的数据,比如:
{
"timestamp": 1674736682248,
"status": 500,
"error": "Internal Server Error",
"path": "/test"
}
timestamp
、status
等响应内容就是错误属性 errorAttributes
的中包含的内容。
返回的数据中并没有显示异常信息,可以通过配置文件进行配置:
server.error.include-exception=true
也可以在添加 BasicErrorController
到 Spring 容器中时,设置错误属性 errorProperties
:
@Bean
public BasicErrorController basicErrorController() {
ErrorProperties errorProperties = new ErrorProperties();
errorProperties.setIncludeException(true);
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}
使用浏览器访问
如果使用浏览器访问 http://localhost:8080/test
,又会回到“解放前”,显示与 Tomcat 的默认错误处理页相同的内容。
这是因为使用浏览器访问时,将调用 BasicErrorController
中的 errorHtml()
控制器方法:
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
该方法返回 ModelAndView
,并且在没有添加新的错误视图的情况下,尝试寻找视图名称为 error
的视图。
这里既没有添加新的错误视图,也没有名称为 error
的视图,因此最终又会交由 Tomcat 进行处理。
尝试向 Spring 容器中添加一个 View
视图,Bean 的名字 必须 是 error
:
@Bean
public View error() {
return (model, request, response) -> {
System.out.println(model);
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("<h3>服务器内部错误</h3>");
};
}
为了能够在查找指定名称的视图时按照 View
类型的 Bean 的名称进行匹配,还需要添加一个解析器:
@Bean
public ViewResolver viewResolver() {
// View 类型的 Bean 的名称即为视图名称
return new BeanNameViewResolver();
}
MVC 处理流程
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
- 服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
- 路径:默认映射路径为
/
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器- jsp 不会匹配到 DispatcherServlet
- 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
- 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean 及其注册有关 Bean:
- 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
- HandlerMapping,初始化时记录映射关系
- HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
- HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
- ViewResolver
- DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
- 例如根据 /hello 路径找到 @RequestMapping("/hello") 对应的控制器方法
- 控制器方法(连同控制器本身)会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
- HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
- DispatcherServlet 接下来会:
- 调用拦截器的 preHandle 方法
- RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
- @ControllerAdvice 全局增强点1️⃣:补充模型数据 到 ModelAndViewContainer
- @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器(@InitBinder)
- 使用 HandlerMethodArgumentResolver 准备参数
- @ControllerAdvice 全局增强点3️⃣:RequestBody 增强
- 调用 ServletInvocableHandlerMethod
- 使用 HandlerMethodReturnValueHandler 处理返回值
- @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
- 根据 ModelAndViewContainer 获取 ModelAndView
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 例如,有的返回值处理器(@ResponseBody)调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
- 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
- 调用拦截器的 postHandle 方法
- 处理异常或视图渲染
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
- 正常,走视图解析及渲染流程
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- 调用拦截器的 afterCompletion 方法
Comments NOTHING