封面画师:T5-茨舞(微博)     封面ID:104944884

参考视频:黑马程序员Spring视频教程,全面深度讲解spring5底层原理

源码仓库:mofan212/advanced-spring (github.com)

20. RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter

20.1 DispatcherServlet 的初始化

选择支持内嵌Tomcat 服务器的 Spring 容器作为 ApplicationContext 的实现:

1
2
3
4
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂、DispatcherServletDispatcherServlet 注册对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @author mofan
* @date 2023/1/22 22:38
*/
@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, "/");
}
}

运行 main() 方法,控制台打印出:

Tomcat initialized with port(s): 8080 (http)
Root WebApplicationContext: initialization completed in 2132 ms

Tomcat 容器初始化成功,Spring 容器初始化成功,但 DispatcherServlet 还未被初始化。

当Tomcat 服务器 首次 使用到 DispatcherServlet 时,才会由Tomcat 服务器初始化 DispatcherServlet

清空控制台信息,使用浏览器访问 localhost:8080,控制台打印出:

信息: 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 

完成 DispatcherServlet 的初始化。

使用 DEBUG 查看 DispatcherServlet 的初始化时机

断点 DispatcherServletonRefresh() 方法中 this.initStrategies(context); 的所在行:

1
2
3
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}

以 DEBUG 方式重启程序,此时程序尚未执行到断点处。

再次在浏览器中访问 localhost:8080,程序执行到断点处。

查看调用栈可知,是从 GenericServletinit() 方法执行到 onRefresh() 方法的:

1
2
3
4
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}

因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。

使 DispatcherServlet 在Tomcat 服务器启动时被初始化

修改添加到 Spring 容器的 DispatcherServlet 注册 Bean:

1
2
3
4
5
6
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}

设置其 loadOnStartup 为一个正数。

当存在多个 DispatcherServlet 需要被注册时,设置的 loadOnStartup 越大,优先级越小,初始化顺序越靠后。

再次重启程序,根据控制台输出的内容可知,不仅完成 Tomcat 和 Spring 容器的初始化,DispatcherServlet 也初始化成功。

抽取配置信息到配置文件中

使用 @PropertySource 注解设置配置类需要读取的配置文件,以便后续读取配置文件中的内容。

要读取配置文件中的内容,可以使用 @Value 注解,但该注解一次仅仅能够读取一个值,现实是往往需要从配置文件中读取多个值。

可以使用 @EnableConfigurationProperties 注解完成配置文件信息与对象的绑定,后续使用时作为 @Bean 注解标记的方法的参数直接在方法中使用即可:

1
2
3
server.port=9090

spring.mvc.servlet.load-on-startup=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
@ComponentScan
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
/**
* 内嵌 Web 容器工厂
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties) {
return new TomcatServletWebServerFactory(serverProperties.getPort());
}

/**
* 创建 DispatcherServlet
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

/**
* 注册 DispatcherServlet,Spring MVC 的入口
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
return registrationBean;
}
}

再次重启程序,根据控制台输出的内容可知,Tomcat 此时监听的端口是 9090DispatcherServlet 也在 Tomcat 启动时被初始化。

DispatcherServlet 初始化时执行的操作

回到 DispatcherServletonRefresh() 方法,它又调用了 initStrategies() 方法:

1
2
3
4
5
6
7
8
9
10
11
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 不仅会在当前容器中获取目标组件,还会在其所有父级容器中寻找。

initHandlerMappings() 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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--
}

20.2 RequestMappingHandlerMapping

HandlerMapping,即处理器映射器,用于建立请求路径与控制器方法的映射关系。

RequestMappingHandlerMappingHandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。

当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。

在配置类中将 RequestMappingHandlerMapping 添加到 Spring 容器:

1
2
3
4
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}

定义一个控制器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
@Controller
public class Controller1 {
@GetMapping("/test1")
public ModelAndView test1() throws Exception {
log.debug("test1()");
return null;
}

@PostMapping("/test2")
public ModelAndView test2(@RequestParam("name") String name) {
log.debug("test2({})", name);
return null;
}

@PutMapping("/test3")
public ModelAndView test3(String token) {
log.debug("test3({})", token);
return null;
}

@RequestMapping("/test4")
public User test4() {
log.debug("test4");
return new User("张三", 18);
}
}

编写 main() 方法,从 Spring 容器中获取 RequestMappingHandlerMapping,再获取请求路径与映射器方法的映射关系,并根据给定请求获取控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
// 解析 @RequestMapping 以及派生注解,在初始化时生成路径与控制器方法的映射关系
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 获取映射结果
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k, v) -> System.out.println(k + " = " + v));
// 根据给定请求获取控制器方法,返回处理器执行链
HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
System.out.println(chain);
}
{GET [/test1]} = indi.mofan.a20.Controller1#test1()
{POST [/test2]} = indi.mofan.a20.Controller1#test2(String)
{PUT [/test3]} = indi.mofan.a20.Controller1#test3(String)
{ [/test4]} = indi.mofan.a20.Controller1#test4()
HandlerExecutionChain with [indi.mofan.a20.Controller1#test1()] and 0 interceptors

getHandler() 方法返回的对象时处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息。

MockHttpServletRequest 的使用

需要导入以下依赖:

1
2
3
4
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>

20.3 RequestMappingHandlerAdapter

RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。

同样需要在配置类中将 RequestMappingHandlerAdapter 添加到 Spring 容器,但该类中需要测试的方法被 protected 修饰,无法直接使用,因此创建一个子类,将子类添加到 Spring 容器中:

1
2
3
4
5
6
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
1
2
3
4
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}

main() 方法中测试 RequestMappingHandlerAdapterinvokeHandlerMethod() 方法:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test2");
request.setParameter("name", "mofan");
MockHttpServletResponse response = new MockHttpServletResponse();
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, ((HandlerMethod) handlerMapping.getHandler(request).getHandler()));
}
[DEBUG] indi.mofan.a20.Controller1          - test2(mofan) 

实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?

显然是需要解析 @RequestParam 注解。

Spring 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdaptergetArgumentResolvers() 方法获取所有参数解析器。

Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdaptergetReturnValueHandlers() 方法获取所有返回值处理器。

自定义参数解析器

假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记控制器方法的哪个参数来获取 Token 信息:

1
2
3
4
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}

使 test3() 控制器方法参数被 @Token 标记:

1
2
3
4
5
@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
log.debug("test3({})", token);
return null;
}

自定义参数解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 中:

1
2
3
4
5
6
7
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
adapter.setCustomArgumentResolvers(Collections.singletonList(tokenArgumentResolver));
return adapter;
}

测试执行 test3() 控制器方法:

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest tokenRequest = new MockHttpServletRequest("PUT", "/test3");
tokenRequest.addHeader("token", "token info");
handlerAdapter.invokeHandlerMethod(tokenRequest, response, ((HandlerMethod) handlerMapping.getHandler(tokenRequest).getHandler()));
}
[DEBUG] indi.mofan.a20.Controller1          - test3(token info) 

自定义返回值处理器

@ResponseBody 标记了控制器方法时,方法的返回值会转换成 JSON 写入响应体中。

自定义 @Yml 注解,被 @Yml 注解标记的控制器方法的返回值会转换成 YAML 写入响应体中。

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Yml {
}

使 test4() 控制器方法被 @Yml 注解标记:

1
2
3
4
5
6
@RequestMapping("/test4")
@Yml
public User test4() {
log.debug("test4");
return new User("张三", 18);
}

自定义返回值处理器将返回值转换成 YAML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return returnType.getMethodAnnotation(Yml.class) != null;
}

@Override
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 转换返回结果为 YAML
String str = new Yaml().dump(returnValue);
// 将 YAML 字符串写入响应体
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType("text/plain;charset=utf-8");
response.getWriter().print(str);
// 设置请求已经处理完毕
mavContainer.setRequestHandled(true);
}
}

将返回值处理器添加到 HandlerAdapter 中:

1
2
3
4
5
6
7
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();
adapter.setCustomReturnValueHandlers(Collections.singletonList(ymlReturnValueHandler));
return adapter;
}

测试执行 test4() 控制器方法:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
// --snip--

MockHttpServletRequest test4Req = new MockHttpServletRequest("GET", "/test4");
handlerAdapter.invokeHandlerMethod(test4Req, response, ((HandlerMethod) handlerMapping.getHandler(test4Req).getHandler()));
byte[] content = response.getContentAsByteArray();
System.out.println(new String(content, StandardCharsets.UTF_8));
}
[DEBUG] indi.mofan.a20.Controller1          - test4 
!!indi.mofan.a20.Controller1$User {age: 18, name: 张三}

21. 参数解析器

Spring 提供了许多种类的控制器方法参数解析器,定义一个包含多个不同种类参数的控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}

将控制器方法封装成 HandlerMethod 并打印方法中每个参数的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Exception {
// 控制器方法封装成 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);

// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
String annotations = Arrays.stream(parameter.getParameterAnnotations())
.map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining());
String appendAt = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
System.out.println("[" + parameter.getParameterIndex() + "] " + appendAt +
parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName());
}
}
[0] @RequestParam String name1
[1] String name2
[2] @RequestParam int age
[3] @RequestParam String home1
[4] @RequestParam MultipartFile file
[5] @PathVariable int id
[6] @RequestHeader String header
[7] @CookieValue String token
[8] @Value String home2
[9] HttpServletRequest request
[10] @ModelAttribute User user1
[11] User user2
[12] @RequestBody User user3

21.2 @RequestParam

@RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:

  • beanFactory:Bean 工厂对象。需要解析 ${} 时,就需要指定 Bean 工厂对象
  • useDefaultResolution:布尔类型参数。为 false 表示只解析添加了 @RequestParam 注解的参数,为 true 针对未添加 @RequestParam 注解的参数也使用该参数解析器进行解析。

RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:

  • parameter:参数对象
  • mavContainerModelAndView 容器,用来存储中间的 Model 结果
  • webRequest:由 ServletWebRequest 封装后的请求对象
  • binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 18 转换成整型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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);
}
}
}
class java.lang.String
[0] @RequestParam String name1 -> zhangsan
class java.lang.String
[1] String name2 -> lisi
class java.lang.Integer
[2] @RequestParam int age -> 18
class java.lang.String
[3] @RequestParam String home1 -> D:\environment\JDK1.8
class org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@f2ff811
Exception in thread "main" java.lang.IllegalStateException: Optional int parameter 'id' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type.

控制器方法 test() 的前 5 个参数解析成功,但在解析第 6 个参数时产生了异常。

这是因为在构造 RequestParamMethodArgumentResolver 对象时,将 useDefaultResolution 设置为 true,针对未添加 @RequestParam 注解的参数都使用该参数解析器进行解析。第 6 个参数需要的 id 信息使用该解析器解析得到的结果是 null,无法将 null 值赋值给基本类型 int,显然第 6 个及其以后的参数应该使用其他参数解析器进行解析。

多个参数解析器的组合 - 组合模式

不同种类的参数需要不同的参数解析器,当前使用的参数解析器不支持当前参数的解析时,就应该换一个参数解析器进行解析。

可以将所有参数解析器添加到一个集合中,然后遍历这个集合,实现上述需求。

Spring 提供了名为 HandlerMethodArgumentResolverComposite 的类,对上述逻辑进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
}
}

21.2 @PathVariable

@PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。

使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver()
);

// --snip--
}
}

修改 RequestParamMethodArgumentResolver 参数解析器的构造,将 useDefaultResolution 设置为 false,让程序 暂时 不抛出异常。

[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@11c9af63
[5] @PathVariable int id -> 123

21.3 @RequestHeader

@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory)
);

// --snip--
}
}
[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@3943a2be
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json

21.4 @CookieValue

@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory)
);

// --snip--
}
}
[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@1329eff
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456

21.5 @Value

@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory)
);

// --snip--
}
}
[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@46fa7c39
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8

21.6 HttpServletRequest

HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver()
);

// --snip--
}
}
[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@5f683daf
[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@152aa092

ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

21.7 @ModelAttribute

@ModelAttribute 注解的解析需要使用到 ServletModelAttributeMethodProcessor 参数解析器。构造时需要传入一个布尔类型的值。为 false 时,表示 @ModelAttribute 不是不必须的,即是必须的。

针对 @ModelAttribute("abc") User user1User user2 两种参数来说,尽管后者没有使用 @ModelAttribute 注解,但它们使用的是同一种解析器。

添加两个 ServletModelAttributeMethodProcessor 参数解析器,先解析带 @ModelAttribute 注解的参数,再解析不带 @ModelAttribute 注解的参数。

通过 ServletModelAttributeMethodProcessor 解析得到的数据还会被存入 ModelAndViewContainer 中。存储的数据结构是一个 Map,其 key 为 @ModelAttribute 注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。

1
2
3
4
5
6
7
8
9
static class Controller {
public void test(
// 指定 value
@ModelAttribute("abc") User user1, // name=zhang&age=18
User user2, // name=zhang&age=18
@RequestBody User user3 // json
) {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new ServletModelAttributeMethodProcessor(true)
);

// --snip--

if (composite.supportsParameter(parameter)) {
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(paramInfo + " -> " + v);
// 打印模型数据
ModelMap modelMap = container.getModel();
if (MapUtils.isNotEmpty(modelMap)) {
System.out.println("模型数据: " + modelMap);
}
} else {
System.out.println(paramInfo);
}
}
}
[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 数据转换而来,则需要使用另一个参数解析器。

21.8 @RequestBody

@RequestBody 注解的解析需要使用到 RequestResponseBodyMethodProcessor 参数解析器。构造时需要传入一个消息转换器列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)
);

// --snip--
}
}
[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@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)

@RequestBody User user3 参数数据通过 JSON 数据得到,与上一节的解析进行区分。

除此之外,添加的参数解析器顺序也影响着解析结果:

1
2
3
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)

先添加解析 @ModelAttribute 注解的解析器,再添加解析 @RequestBody 注解的解析器,最后添加解析省略了 @ModelAttribute 注解的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor

因此 String name2 参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(beanFactory, true)
);

// --snip--
}
}
[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)

22. 获取参数名

在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译:

1
2
3
4
5
6
7
package indi.mofan.a22;

public class Bean2 {
public void foo(String name, int age) {

}
}

将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容:

1
2
3
4
5
6
7
8
9
package indi.mofan.a22;

public class Bean2 {
public Bean2() {
}

public void foo(String var1, int var2) {
}
}

编译生成的 class 文件中的 foo() 方法的参数名称不再是 nameage,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。

执行 javac -parameters .\Bean2.java 再次编译 Bean2.java,并查看得到的 Bean2.class 文件内容:

1
2
3
4
5
6
7
8
9
package indi.mofan.a22;

public class Bean2 {
public Bean2() {
}

public void foo(String name, int age) {
}
}

foo() 方法的参数名称得以保留。

还可以使用 javap -c -v .\Bean2.class 命令反编译 Bean2.classfoo() 方法的反编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
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 中,可以使用 反射 获取:

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
// 反射获取参数名
Method foo = Bean2.class.getMethod("foo", String.class, int.class);
for (Parameter parameter : foo.getParameters()) {
System.out.println(parameter.getName());
}
}
name
age

使用 javac -g .\Bean2.java 命令进行编译也会保留方法的参数信息。再次使用 javap 反编译 Bean2.classfoo() 方法的反编译结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 封装的解析工具:

1
2
3
4
5
6
7
8
9
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));
}
[name, age]

在【21. 参数解析器】中并没有使用 LocalVariableTableParameterNameDiscoverer,而是使用的是 DefaultParameterNameDiscovererDefaultParameterNameDiscoverer 将两种实现进行了统一:

1
2
3
4
5
6
7
8
9
10
11
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {

public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}

}

javac -g 的局限性

假设有这样一个接口:

1
2
3
4
5
package indi.mofan.a22;

public interface Bean1 {
public void foo(String name, int age);
}

如果使用 javac -g .\Bean1.java 命令进行编译后,再利用 javap 查看 foo() 方法的反编译结果:

1
2
3
public abstract void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC, ACC_ABSTRACT

并没有记录抽象方法 foo() 的参数信息。

如果使用 javac -parameters .\Bean1.java 呢?

1
2
3
4
5
6
7
public abstract void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC, ACC_ABSTRACT
MethodParameters:
Name Flags
name
age

参数信息得以保留。

23. 对象绑定与类型转换

23.1 三种转换接口

底层第一套转换接口与实现

classDiagram

Formatter --|> Printer
Formatter --|> Parser

class Converters {
   Set~GenericConverter~
}
class Converter

class ConversionService
class FormattingConversionService

ConversionService <|-- FormattingConversionService
FormattingConversionService o-- Converters

Printer --> Adapter1
Adapter1 --> Converters
Parser --> Adapter2
Adapter2 --> Converters
Converter --> Adapter3
Adapter3 --> Converters

<<interface>> Formatter
<<interface>> Printer
<<interface>> Parser
<<interface>> Converter
<<interface>> ConversionService
  • Printer 把其它类型转为 String
  • ParserString 转为其它类型
  • Formatter 综合 PrinterParser 的功能
  • Converter 把类型 S 转为类型 T
  • PrinterParserConverter 经过适配转换成 GenericConverter 放入 Converters 集合
  • FormattingConversionService 利用其它接口实现转换

底层第二套转换接口

由 JDK 提供,而不是 Spring。

classDiagram

PropertyEditorRegistry o-- "多" PropertyEditor

<<interface>> PropertyEditorRegistry
<<interface>> PropertyEditor
  • PropertyEditorString 与其它类型相互转换
  • PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
  • 可以通过 FormatterPropertyEditorAdapter 与第一套接口进行适配

高层转换接口与实现

classDiagram
TypeConverter <|-- SimpleTypeConverter
TypeConverter <|-- BeanWrapperImpl
TypeConverter <|-- DirectFieldAccessor
TypeConverter <|-- ServletRequestDataBinder

SimpleTypeConverter --> TypeConverterDelegate
BeanWrapperImpl --> TypeConverterDelegate
DirectFieldAccessor --> TypeConverterDelegate
ServletRequestDataBinder --> TypeConverterDelegate

TypeConverterDelegate --> ConversionService
TypeConverterDelegate --> PropertyEditorRegistry

<<interface>> TypeConverter
<<interface>> ConversionService
<<interface>> PropertyEditorRegistry
  • 它们都实现了 TypeConverter 高层转换接口,在转换时会用到 TypeConverterDelegate 委派ConversionServicePropertyEditorRegistry 真正执行转换(使用 Facade 门面模式)
    • 首先查看是否存在实现了 PropertyEditorRegistry 的自定义转换器,@InitBinder 注解实现的就是自定义转换器(用了适配器模式把 Formatter 转为需要的 PropertyEditor
    • 再查看是否存在 ConversionService 实现
    • 再利用默认的 PropertyEditor 实现
    • 最后有一些特殊处理
  • SimpleTypeConverter 仅做类型转换
  • BeanWrapperImpl 利用 Property,即 Getter/Setter,为 Bean 的属性赋值,,必要时进行类型转换
  • DirectFieldAccessor 利用 Field,即字段,为 Bean 的字段赋值,必要时进行类型转换
  • ServletRequestDataBinder 为 Bean 的属性执行绑定,必要时进行类型转换,根据布尔类型成员变量 directFieldAccess 选择利用 Property 还是 Field,还具备校验与获取校验结果功能

23.2 使用示例

SimpleTypeConverter

1
2
3
4
5
6
7
public static void main(String[] args) {
SimpleTypeConverter converter = new SimpleTypeConverter();
Integer number = converter.convertIfNecessary("13", int.class);
System.out.println(number);
Date date = converter.convertIfNecessary("1999/03/04", Date.class);
System.out.println(date);
}
13
Thu Mar 04 00:00:00 CST 1999

BeanWrapperImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
// 利用反射为 bean 的属性赋值
MyBean bean = new MyBean();
BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
wrapper.setPropertyValue("a", "10");
wrapper.setPropertyValue("b", "hello");
wrapper.setPropertyValue("c", "1999/03/04");
System.out.println(bean);
}

@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestBeanWrapper.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)

DirectFieldAccessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// 利用反射为 bean 的字段赋值
MyBean bean = new MyBean();
DirectFieldAccessor accessor = new DirectFieldAccessor(bean);
accessor.setPropertyValue("a", "10");
accessor.setPropertyValue("b", "hello");
accessor.setPropertyValue("c", "1999/03/04");
System.out.println(bean);
}

@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestFieldAccessor.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)

DataBinder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 执行数据绑定
MyBean bean = new MyBean();
DataBinder binder = new DataBinder(bean);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("a", "10");
pvs.add("b", "hello");
pvs.add("c", "1999/03/04");
binder.bind(pvs);
System.out.println(bean);
}

@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)

如果 MyBean 没有提供 Getter/Setter 方法,可以调用 DataBinderinitDirectFieldAccess() 方法使数据绑定逻辑走字段赋值,而不是属性赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
// 执行数据绑定
MyBean bean = new MyBean();
DataBinder binder = new DataBinder(bean);
binder.initDirectFieldAccess();
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("a", "10");
pvs.add("b", "hello");
pvs.add("c", "1999/03/04");
binder.bind(pvs);
System.out.println(bean);
}

@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)

Web 环境下的数据绑定

Web 环境下的数据绑定需要使用 DataBinder 的子类 ServletRequestDataBinder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
// web 环境下的数据绑定
MyBean bean = new MyBean();
DataBinder dataBinder = new ServletRequestDataBinder(bean);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("a", "10");
request.setParameter("b", "hello");
request.setParameter("c", "1999/03/04");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));

System.out.println(bean);
}

@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestServletDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)

23.3 绑定器工厂

现有如下两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
@Setter
@ToString
public static class User {
private Date birthday;
private Address address;
}

@Getter
@Setter
@ToString
public static class Address {
private String name;
}

在 Web 环境下进行数据绑定:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "成都");

User user = new User();
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(user);

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}

birthdayaddress.name 都能绑定成功吗?

TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=成都))

birthday 绑定失败,要想使其绑定成功,需要自定义转换器,有两种方式:

  • 使用 Spring 提供的 ConversionService
  • 使用 JDK 提供的 PropertyEditorRegistry

创建 DataBinder 的职责交由 DataBinderFactory 完成,以便添加各种选项,拓展不同的自定义转换器。

1
2
3
4
5
6
7
8
9
10
11
12
13
@SneakyThrows
public static void main(String[] args) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("birthday", "1999|01|02");
request.setParameter("address.name", "成都");

User user = new User();
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), user, "user");

dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);
}

运行 main() 方法后,控制台输出的结果不变。

利用 @InitBinder 自定义转换器

声明一个 Controller 类,其中包含一个被 @InitBinder 注解标记的方法:

1
2
3
4
5
6
7
static class MyController {
@InitBinder
public void myMethod(WebDataBinder dataBinder) {
// 拓展 dataBinder 的转换器
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 进行拓展"));
}
}

WebDataBinder 作为方法参数,在方法体类添加自定义转换器 MyDateFormatter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
public class MyDateFormatter implements Formatter<Date> {
private final String desc;

public MyDateFormatter(String desc) {
// 仅做测试
this.desc = desc;
}

@Override
public String print(Date date, Locale locale) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.format(date);
}

@Override
public Date parse(String text, Locale locale) throws ParseException {
log.debug(">>>>>> 进入了: {}", desc);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
return sdf.parse(text);
}
}

在构造 DataBinderFactory 时传入 InvocableHandlerMethod 列表,列表中包含根据 Controller 对象、Controller 类中被 @InitBinder 注解标记的方法对象构造的 InvocableHandlerMethod 对象:

1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
public static void main(String[] args) {
// --snip--

InvocableHandlerMethod handlerMethod =
new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));

ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(handlerMethod), null);

// --snip--
}

再次执行 main()birthday 被成功绑定:

[DEBUG] indi.mofan.a23.MyDateFormatter      - >>>>>> 进入了: 用 @InitBinder 进行拓展 
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

这种方式使用了 JDK 提供的 PropertyEditorRegistry,证据就在 WebDataBinderaddCustomFormatter() 方法中:

1
2
3
4
public void addCustomFormatter(Formatter<?> formatter) {
FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter);
getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter);
}

ConversionService 拓展

选择 FormattingConversionService 作为 ConversionService 的实现,向其中添加自定义转换器 MyDateFormatter

构造 DataBinderFactory 时传入 WebBindingInitializer 的实现,因此将 FormattingConversionService 封装成 ConfigurableWebBindingInitializer 传入 DataBinderFactory 的构造方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SneakyThrows
public static void main(String[] args) {
// --snip--

// 使用 ConversionService 转换
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

// --snip--
}
[DEBUG] indi.mofan.a23.MyDateFormatter      - >>>>>> 进入了: 用 ConversionService 方式拓展转换功能 
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

如果同时存在 @InitBinderConversionService,将以 @InitBinder 为主,@InitBinder 实现的转换器属于自定义转换器,自定义转换器的优先级更高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void main(String[] args) {
// --snip--

FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
InvocableHandlerMethod handlerMethod =
new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(handlerMethod), initializer);

// --snip--
}
[DEBUG] indi.mofan.a23.MyDateFormatter      - >>>>>> 进入了: 用 @InitBinder 进行拓展 
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

默认的 ConversionService

ConversionService 有一个默认实现 DefaultFormattingConversionService,它还是 FormattingConversionService 的子类:

1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
public static void main(String[] args) {
// --snip--

DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(conversionService);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

// --snip--
}

运行 main() 方法后,控制台打印出:

TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=成都))

birthday 绑定失败,默认的 ConversionService 需要搭配注解 @DateTimeFormat 使用。在目标类的字段上使用该注解标记,并指定被转换的日期格式:

1
2
3
4
5
6
7
8
@Getter
@Setter
@ToString
public static class User {
@DateTimeFormat(pattern = "yyyy|MM|dd")
private Date birthday;
private Address address;
}

再次运行 main() 方法:

TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))

在 SpringBoot 中还提供了 ApplicationConversionService,它也是 FormattingConversionService 的子类,上述代码将 DefaultFormattingConversionService 换成 ApplicationConversionService 也能达到相同效果。

23.4 Spring 的泛型操作技巧

有一基类 BaseDao,接收一个泛型参数:

1
2
3
4
5
public class BaseDao<T> {
T findOne() {
return null;
}
}

围绕 BaseDao 有如下五个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EmployeeDao extends BaseDao {
}

public class Student {
}

public class StudentDao extends BaseDao<Student> {
}

public class Teacher {
}

public class TeacherDao extends BaseDao<Teacher> {
}

尝试获取 BaseDao 子类泛型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) {
// 1. java api
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
// 带有泛型信息的父类信息
Type teacherDaoType = TeacherDao.class.getGenericSuperclass();
System.out.println("TeacherDao type: " + teacherDaoType);
System.out.println("TeacherDao type class: " + teacherDaoType.getClass());

Type employeeDaoType = EmployeeDao.class.getGenericSuperclass();
System.out.println("EmployeeDao type: " + employeeDaoType);
System.out.println("EmployeeDao type class: " + employeeDaoType.getClass());

// 有泛型参数的 Type 对象才是 ParameterizedType 类型
if (teacherDaoType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) teacherDaoType;
System.out.println(parameterizedType.getActualTypeArguments()[0]);
}

// 2. spring api 1
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class);
System.out.println(t);

// 3. spring api 2
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(ResolvableType.forClass(StudentDao.class).getSuperType().getGeneric().resolve());
}
>>>>>>>>>>>>>>>>>>>>>>>
TeacherDao type: indi.mofan.a23.sub.BaseDao
TeacherDao type class: class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
EmployeeDao type: class indi.mofan.a23.sub.BaseDao
EmployeeDao type class: class java.lang.Class
class indi.mofan.a23.sub.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class indi.mofan.a23.sub.Teacher
>>>>>>>>>>>>>>>>>>>>>>>
class indi.mofan.a23.sub.Student

24. ControllerAdvice 之 @InitBinder

准备 @InitBinder 在整个 HandlerAdapter 调用过程中所处的位置:

sequenceDiagram
participant adapter as HandlerAdapter
participant bf as WebDataBinderFactory
participant mf as ModelFactory
participant ihm as ServletInvocableHandlerMethod
participant ar as ArgumentResolvers 
participant rh as ReturnValueHandlers
participant container as ModelAndViewContainer
rect rgb(200, 150, 255)
adapter ->> +bf: 准备 @InitBinder
bf -->> -adapter: Void
end
adapter ->> +mf: 准备 @ModelAttribute
mf ->> +container: 添加 Model 数据
container -->> -mf: Void
mf -->> -adapter: Void
adapter ->> +ihm: invokeAndHandle
ihm ->> +ar: 获取 args
ar ->> ar: 有的解析器涉及 RequestBodyAdvice
ar ->> container: 有的解析器涉及数据绑定生成 Model 数据
ar -->> -ihm: args
ihm ->> ihm: method.invoke(bean,args) 得到 returnValue
ihm ->> +rh: 处理 returnValue
rh ->> rh: 有的处理器涉及 ResponseBodyAdvice
rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等
container -->> -rh: Void
rh -->> -ihm: Void
ihm -->> -adapter: Void
adapter ->> +container: 获取 ModelAndView
container -->> -adapter: Void

备注:

  • RequestMappingHandlerAdapter 在图中缩写为 HandlerAdapter
  • HandlerMethodArgumentResolverComposite 在图中缩写为 ArgumentResolvers
  • HandlerMethodReturnValueHandlerComposite 在图中缩写为 ReturnValueHandlers

功能与使用

@InitBinder 注解只能作用在方法上,通常搭配 @ControllerAdvice@Controller 以及他们的衍生注解使用。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@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 的来源有两个:

  1. @ControllerAdvice 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
  2. @Controller 标记的类中 @InitBinder 标记的方法,由 RequestMappingHandlerAdapter 在控制器方法首次执行时解析并记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();

handlerAdapter.setApplicationContext(context);
handlerAdapter.afterPropertiesSet();

log.debug("1. 刚开始...");
showBindMethods(handlerAdapter);

context.close();
}

@SuppressWarnings("all")
private static void showBindMethods(RequestMappingHandlerAdapter handlerAdapter) throws NoSuchFieldException, IllegalAccessException {
Field initBinderAdviceCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderAdviceCache");
initBinderAdviceCache.setAccessible(true);
Map<ControllerAdviceBean, Set<Method>> globalMap = (Map<ControllerAdviceBean, Set<Method>>) initBinderAdviceCache.get(handlerAdapter);
log.debug("全局的 @InitBinder 方法 {}",
globalMap.values().stream()
.flatMap(ms -> ms.stream().map(m -> m.getName()))
.collect(Collectors.toList())
);

Field initBinderCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderCache");
initBinderCache.setAccessible(true);
Map<Class<?>, Set<Method>> controllerMap = (Map<Class<?>, Set<Method>>) initBinderCache.get(handlerAdapter);
log.debug("控制器的 @InitBinder 方法 {}",
controllerMap.entrySet().stream()
.flatMap(e -> e.getValue().stream().map(v -> e.getKey().getSimpleName() + "." + v.getName()))
.collect(Collectors.toList())
);
}

运行 main() 方法后,控制台打印出:

indi.mofan.a24.A24                  - 1. 刚开始... 
indi.mofan.a24.A24                  - 全局的 @InitBinder 方法 [binder3] 
indi.mofan.a24.A24                  - 控制器的 @InitBinder 方法 [] 

全局的 @InitBinder 方法被解析并记录,但控制器中被 @InitBinder 标记的方法并没有被解析记录。

模拟调用控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
// --snip--

log.debug("1. 刚开始...");
showBindMethods(handlerAdapter);

Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
getDataBinderFactory.setAccessible(true);
log.debug("2. 模拟调用 Controller1 的 foo 方法...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
showBindMethods(handlerAdapter);

log.debug("3. 模拟调用 Controller2 的 bar 方法时...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller2(), WebConfig.Controller2.class.getMethod("bar")));
showBindMethods(handlerAdapter);

context.close();
}
1. 刚开始... 
全局的 @InitBinder 方法 [binder3] 
控制器的 @InitBinder 方法 [] 
2. 模拟调用 Controller1 的 foo 方法... 
全局的 @InitBinder 方法 [binder3] 
控制器的 @InitBinder 方法 [Controller1.binder1] 
3. 模拟调用 Controller2 的 bar 方法时... 
全局的 @InitBinder 方法 [binder3] 
控制器的 @InitBinder 方法 [Controller1.binder1, Controller2.binder22, Controller2.binder21] 

首次调用控制器中的方法时,控制器中被 @InitBinder 标记方法被解析记录。

25. 控制器方法执行流程

ServletInvocableHandlerMethod 的组成

classDiagram
class ServletInvocableHandlerMethod {
	+invokeAndHandle(ServletWebRequest,ModelAndViewContainer)
}
HandlerMethod <|-- ServletInvocableHandlerMethod
HandlerMethod o-- bean
HandlerMethod o-- method
ServletInvocableHandlerMethod o-- WebDataBinderFactory
ServletInvocableHandlerMethod o-- ParameterNameDiscoverer
ServletInvocableHandlerMethod o-- HandlerMethodArgumentResolverComposite
ServletInvocableHandlerMethod o-- HandlerMethodReturnValueHandlerComposite

HandlerMethod 需要:

  • bean,即哪个 Controller
  • method,即 Controller 中的哪个方法

ServletInvocableHandlerMethod 需要:

  • WebDataBinderFactory,用于对象绑定、类型转换
  • ParameterNameDiscoverer,用于参数名解析
  • HandlerMethodArgumentResolverComposite,用于解析参数
  • HandlerMethodReturnValueHandlerComposite,用于处理返回值

控制器方法执行流程

RequestMappingHandlerAdapter 为起点,创建 WebDataBinderFactory,添加自定义类型转换器,再创建 ModelFactory,添加 Model 数据:

sequenceDiagram
participant adapter as RequestMappingHandlerAdapter
participant bf as WebDataBinderFactory
participant mf as ModelFactory
participant container as ModelAndViewContainer
adapter ->> +bf: 初始化 advice:解析 @InitBinder
bf -->> -adapter: 添加自定义类型转换器
adapter ->> +mf: 初始化 advice:解析 @ModelAttribute
mf ->> +container: 添加 Model 数据
container -->> -mf: Void
mf -->> -adapter: Void

接下来调用 ServletInvocableHandlerMethod,主要完成三件事:

  1. 准备参数
  2. 反射调用控制器方法
  3. 处理返回值
sequenceDiagram
participant adapter as RequestMappingHandlerAdapter
participant ihm as ServletInvocableHandlerMethod
participant ar as HandlerMethodArgumentResolverComposite
participant rh as HandlerMethodReturnValueHandlerComposite
participant container as ModelAndViewContainer

adapter ->> +ihm: invokeAndHandle
ihm ->> +ar: 获取 args
ar ->> ar: 有的解析器涉及 RequestBodyAdvice
ar ->> container: 有的解析器涉及数据绑定生成模型数据
container -->> ar: Void
ar -->> -ihm: args
ihm ->> ihm: method.invoke(bean,args) 得到 returnValue
ihm ->> +rh: 处理 returnValue
rh ->> rh: 有的处理器涉及 ResponseBodyAdvice
rh ->> +container: 添加Model数据,处理视图名,是否渲染等
container -->> -rh: Void
rh -->> -ihm: Void
ihm -->> -adapter: Void 
adapter ->> +container: 获取 ModelAndView
container -->> -adapter: Void

代码演示

提供配置类 WebConfig,其中包含一个控制器 Controller1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class WebConfig {
@Controller
static class Controller1 {
@ResponseStatus(HttpStatus.OK)
public ModelAndView foo(User user) {
// 使用 @ResponseStatus 注解,咋不考虑返回值的处理
System.out.println("foo");
return null;
}
}

@Getter
@Setter
@ToString
static class User {
private String name;
}
}

创建 Spring 容器,Mock 请求,创建 HandlerMethod 对象指定需要执行的控制器方法,创建 DataBinderFactory 数据绑定工厂。向创建的 HandlerMethod 对象中添加数据绑定工厂、参数名称解析器、参数解析器(暂不考虑返回值的处理),最后创建模型视图容器,调用 HandlerMethodinvokeAndHandle 方法执行控制器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SneakyThrows
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);

MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "mofan");

ServletInvocableHandlerMethod handlerMethod =
new ServletInvocableHandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo", WebConfig.User.class));
ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);

handlerMethod.setDataBinderFactory(binderFactory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
// getArgumentResolvers() 组装了一系列参数解析器,省略具体实现
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
// 暂不考虑返回值的处理

ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());

context.close();
}
foo
{user=WebConfig.User(name=mofan), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

26. ControllerAdvice 之 @ModelAttribute

准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置:

sequenceDiagram
participant adapter as HandlerAdapter
participant bf as WebDataBinderFactory
participant mf as ModelFactory
participant ihm as ServletInvocableHandlerMethod
participant ar as ArgumentResolvers 
participant rh as ReturnValueHandlers
participant container as ModelAndViewContainer

adapter ->> +bf: 准备 @InitBinder
bf -->> -adapter: Void
rect rgb(200, 150, 255)
adapter ->> +mf: 准备 @ModelAttribute
mf ->> +container: 添加 Model 数据
container -->> -mf: Void
mf -->> -adapter: Void
end
adapter ->> +ihm: invokeAndHandle
ihm ->> +ar: 获取 args
ar ->> ar: 有的解析器涉及 RequestBodyAdvice
ar ->> container: 有的解析器涉及数据绑定生成 Model 数据
ar -->> -ihm: args
ihm ->> ihm: method.invoke(bean,args) 得到 returnValue
ihm ->> +rh: 处理 returnValue
rh ->> rh: 有的处理器涉及 ResponseBodyAdvice
rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等
container -->> -rh: Void
rh -->> -ihm: Void
ihm -->> -adapter: Void
adapter ->> +container: 获取 ModelAndView
container -->> -adapter: Void

功能与使用

@ModelAttribute 可以作用在参数上和方法上。

当其作用在参数上时,会将请求中的参数信息 按名称 注入到指定对象中,并将这个对象信息自动添加到 ModelMap 中。当未指定 @ModelAttributevalue 时,添加到 ModelMap 中的 key 是对象类型首字母小写对应的字符串。此时的 @ModelAttribute 注解由 ServletModelAttributeMethodProcessor 解析。

当其作用在方法上时:

  • 如果该方法在被 @Controller 注解标记的类中,会在当前控制器中每个控制器方法执行前执行被 @ModelAttribute 标记的方法,如果该方法有返回值,自动将返回值添加到 ModelMap 中。当未指定 @ModelAttributevalue 时,添加到 ModelMap 中的 key 是返回值类型首字母小写对应的字符串。
  • 如果该方法在被 @ControllerAdvice 注解标记的类中,会在所有控制器方法执行前执行该方法。

作用在方法上的 @ModelAttribute 注解由 RequestMappingHandlerAdapter 解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
static class Controller1 {
@ResponseStatus(HttpStatus.OK)
public ModelAndView foo(@ModelAttribute("u") User user) {
// 使用 @ResponseStatus 注解,咋不考虑返回值的处理
System.out.println("foo");
return null;
}
}

@ControllerAdvice
static class MyControllerAdvice {
@ModelAttribute("a")
public String aa() {
return "aa";
}
}

先不使用 RequestMappingHandlerAdapter 对作用在方法上的 @ModelAttribute 注解进行解析,沿用【25. 控制器方法执行流程】中的 main() 方法:

foo
{u=WebConfig.User(name=mofan), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

再解析方法上的 @ModelAttribute 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SneakyThrows
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);

RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();

MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "mofan");

// --snip--

// 获取模型工厂
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, binderFactory);

// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

// --snip--
}
foo
{a=aa, u=WebConfig.User(name=mofan), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}

{a=aa} 也被放入到 ModelAndViewContainer 中。

27. 返回值处理器

含有多种返回值的控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@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;
}

测试渲染视图需要用到的配置

为测试对视图的渲染,采用 Freemarker 进行测试,先导入 Freemarker 依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

Freemarker 配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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;
}
}

渲染视图使用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
@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));
}

提供构造 HandlerMethodReturnValueHandlerComposite 对象的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public static HandlerMethodReturnValueHandlerComposite getReturnValueHandler() {
HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
composite.addHandlers(Arrays.asList(
new ModelAndViewMethodReturnValueHandler(),
new ViewNameMethodReturnValueHandler(),
new ServletModelAttributeMethodProcessor(false),
new HttpEntityMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new HttpHeadersReturnValueHandler(),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)
));
return composite;
}

测试返回值处理器的方法 testReturnValueProcessor()

利用两个函数式接口 Comsumer,对 Mock 的请求进行补充,或者在请求处理完毕后,输出 Mock 的响应信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@SneakyThrows
private static void testReturnValueProcessor(ApplicationContext context, String methodName,
Consumer<MockHttpServletRequest> requestConsumer,
Consumer<MockHttpServletResponse> responseConsumer) {
Method method = Controller.class.getMethod(methodName);
Controller controller = new Controller();
Object returnValue = method.invoke(controller);

HandlerMethod handlerMethod = new HandlerMethod(context, method);
ModelAndViewContainer container = new ModelAndViewContainer();

HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
MethodParameter returnType = handlerMethod.getReturnType();
MockHttpServletRequest request = new MockHttpServletRequest();
Optional.ofNullable(requestConsumer).ifPresent(i -> i.accept(request));
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);
if (composite.supportsReturnType(returnType)) {
composite.handleReturnValue(returnValue, returnType, container, webRequest);
System.out.println(container.getModel());
System.out.println(container.getViewName());
if (!container.isRequestHandled()) {
// 渲染视图
renderView(context, container, webRequest);
} else {
Optional.ofNullable(responseConsumer).ifPresent(i -> i.accept(response));
}
}
}

27.1 ModelAndView

ModelAndView 类型的返回值由 ModelAndViewMethodReturnValueHandler 处理,构造时无需传入任何参数。

解析 ModelAndView 时,将其中的视图和模型数据分别提取出来,放入 ModelAndViewContainer 中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
test1(context);
}

private static void test1(ApplicationContext context) {
testReturnValueProcessor(context, "test1", null, null);
}

对应的模板页面 view1.ftl

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>view1</title>
</head>
<body>
<h1>Hello! ${name}</h1>
</body>
</html>
indi.mofan.a27.A27$Controller       - test1() 
{name=张三}
view1
indi.mofan.a27.A27                  - >>>>>> 渲染视图 
indi.mofan.a27.A27                  - 没有获取到视图名, 采用默认视图名: view1 
indi.mofan.a27.WebConfig$1$1        - View name 'view1', model {name=张三} 
indi.mofan.a27.WebConfig$1$1        - Rendering [/view1.ftl] 
<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>view1</title>
</head>
<body>
    <h1>Hello! 张三</h1>
</body>
</html>

27.2 字符串类型

控制器方法的返回值是字符串类型时,返回的字符串即为视图的名称。与 ModelAndView 类型的返回值相比,不包含模型数据。

此种类型的返回值由 ViewNameMethodReturnValueHandler 处理,构造时无需传入任何参数。

1
2
3
private static void test2(ApplicationContext context) {
testReturnValueProcessor(context, "test2", null, null);
}

对应的模板页面 view2.ftl

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>view2</title>
</head>
<body>
<h1>Hello!</h1>
</body>
</html>
indi.mofan.a27.A27$Controller       - test2() 
{}
view2
indi.mofan.a27.A27                  - >>>>>> 渲染视图 
indi.mofan.a27.A27                  - 没有获取到视图名, 采用默认视图名: view2 
indi.mofan.a27.WebConfig$1$1        - View name 'view2', model {} 
indi.mofan.a27.WebConfig$1$1        - Rendering [/view2.ftl] 
<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>view2</title>
</head>
<body>
    <h1>Hello!</h1>
</body>
</html>

27.3 @ModelAttribute

@ModelAttribute 的用法在【26. ControllerAdvice 之 @ModelAttribute】中已经介绍过,简单来说,当 @ModelAttribute 注解作用在方法上时,会将方法的返回值作为模型数据添加到 ModelAndViewContainer 中。

@ModelAttribute 标记的方法的返回值由 ServletModelAttributeMethodProcessor 解析,构造时需要传入一个布尔类型数据 annotationNotRequired,表示 @ModelAttribute 注解是否不是必须的。

模型数据已经有了,但视图名称又是什么呢?

在实际开发场景中,控制器方法需要被 @RequestMapping 标记,并指定请求地址,比如:

1
2
3
4
5
6
@ModelAttribute
@RequestMapping("/test3")
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}

当未找到视图名称时,默认以请求路径作为视图名称。

但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

对应的模板页面 test3.ftl

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>test3</title>
</head>
<body>
<h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>
indi.mofan.a27.A27$Controller       - test3() 
{user=A27.User(name=李四, age=20)}
null
indi.mofan.a27.A27                  - >>>>>> 渲染视图 
indi.mofan.a27.A27                  - 没有获取到视图名, 采用默认视图名: test3 
indi.mofan.a27.WebConfig$1$1        - View name 'test3', model {user=A27.User(name=李四, age=20)} 
indi.mofan.a27.WebConfig$1$1        - Rendering [/test3.ftl] 
<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>test3</title>
</head>
<body>
    <h1>Hello! 李四 20</h1>
</body>
</html>

针对控制器方法 test4() 也可以按照相同方式测试:

1
2
3
4
private static void test4(ApplicationContext context) {
String methodName = "test4";
testReturnValueProcessor(context, methodName, mockHttpServletRequestConsumer(methodName), null);
}

对应的模板页面 test4.ftl

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>test4</title>
</head>
<body>
<h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>
indi.mofan.a27.A27$Controller       - test4() 
{user=A27.User(name=王五, age=30)}
null
indi.mofan.a27.A27                  - >>>>>> 渲染视图 
indi.mofan.a27.A27                  - 没有获取到视图名, 采用默认视图名: test4 
indi.mofan.a27.WebConfig$1$1        - View name 'test4', model {user=A27.User(name=王五, age=30)} 
indi.mofan.a27.WebConfig$1$1        - Rendering [/test4.ftl] 
<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>test4</title>
</head>
<body>
    <h1>Hello! 王五 30</h1>
</body>
</html>

与解析参数类似,返回值处理器的执行顺序也有严格要求。

27.4 HttpEntity

HttpEntity 类型的返回值由 HttpEntityMethodProcessor 处理,构造时需要传入一个消息转换器列表。

这种类型的返回值表示响应完成,无需经过视图的解析、渲染流程再生成响应。可在处理器的 handleReturnValue() 方法中得以论证:

1
2
3
4
5
6
7
8
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 一进入方法就设置请求处理完毕
mavContainer.setRequestHandled(true);

// --snip--
}

HttpEntity 中包含了状态码、响应体信息和响应头信息。

尝试在请求处理完毕后,输出响应体信息:

1
2
3
4
5
6
7
8
9
10
private static final Consumer<MockHttpServletResponse> RESPONSE_CONSUMER = resp -> {
for (String name : resp.getHeaderNames()) {
System.out.println(name + " = " + resp.getHeader(name));
}
System.out.println(new String(resp.getContentAsByteArray(), StandardCharsets.UTF_8));
};

private static void test5(ApplicationContext context) {
testReturnValueProcessor(context, "test5", null, RESPONSE_CONSUMER);
}
indi.mofan.a27.A27$Controller       - test5() 
{}
null
Content-Type = application/json
{"name":"赵六","age":40}

27.5 HttpHeaders

HttpEntity 相比,HttpHeaders 只包含响应头信息,HttpHeaders 类型的返回值由 HttpHeadersReturnValueHandler 处理,构造时无需传入任何参数。

HttpEntity 一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。

1
2
3
private static void test6(ApplicationContext context) {
testReturnValueProcessor(context, "test6", null, RESPONSE_CONSUMER);
}
indi.mofan.a27.A27$Controller       - test6() 
{}
null
Content-Type = text/html

27.6 @ResponseBody

@ResponseBody 标记的方法的返回值由 RequestResponseBodyMethodProcessor 处理,构造时需要传入一个消息转换器列表。

这样的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。

1
2
3
private static void test7(ApplicationContext context) {
testReturnValueProcessor(context, "test7", null, RESPONSE_CONSUMER);
}
indi.mofan.a27.A27$Controller       - test7() 
{}
null
Content-Type = application/json
{"name":"钱七","age":50}

28. 消息转换器

在构造参数解析器 RequestResponseBodyMethodProcessor、返回值解析器 HttpEntityMethodProcessorHttpEntityMethodProcessor 时,都需要传入消息转换器列表。

消息转换器的基类是 HttpMessageConverter

介绍两个常见的消息转换器的实现:

  • MappingJackson2XmlHttpMessageConverter

  • MappingJackson2XmlHttpMessageConverter

一个 User 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Getter
@Setter
@ToString
public static class User {
private String name;
private int age;

@JsonCreator
public User(@JsonProperty("name") String name, @JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
}

User 对象转换成 JSON 格式的数据:

1
2
3
4
5
6
7
8
9
10
@SneakyThrows
public static void test1() {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 判断能否将对象转换成目标消息格式
if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)) {
converter.write(new User("张三", 18), MediaType.APPLICATION_JSON, message);
System.out.println(message.getBodyAsString());
}
}
1
{"name":"张三","age":18}

User 对象转换成 XML 格式的数据:

1
2
3
4
5
6
7
8
9
@SneakyThrows
public static void test2() {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_XML)) {
converter.write(new User("李四", 20), MediaType.APPLICATION_XML, message);
System.out.println(message.getBodyAsString());
}
}

使用 MappingJackson2XmlHttpMessageConverter 时,需要额外导入依赖:

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
1
<User><name>李四</name><age>20</age></User>

将 JSON 格式的数据转换成 User 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void test3() {
//language=JSON
String json = "{\n" +
" \"name\": \"李四\",\n" +
" \"age\": 20\n" +
"}";
MockHttpInputMessage message = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) {
Object read = converter.read(User.class, message);
System.out.println(read);
}
}
A28.User(name=李四, age=20)

如果存在多个消息转换器呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SneakyThrows
public static void test4() {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest webRequest = new ServletWebRequest(request, response);

request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);

RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));
processor.handleReturnValue(
new User("张三", 18),
new MethodParameter(A28.class.getMethod("user"), -1),
new ModelAndViewContainer(),
webRequest
);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

@ResponseBody
public User user () {
return null;
}

将以添加的消息转换器顺序为主,比如此处会将 User 对象转换成 JSON 格式的数据:

1
{"name":"张三","age":18}

调换添加的消息转换器顺序:

1
2
3
4
5
6
7
8
9
10
11
@SneakyThrows
public static void test4() {
// --snip--

RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2XmlHttpMessageConverter(),
new MappingJackson2HttpMessageConverter()
));

// --snip--
}

这下会将 User 对象转换成 XML 格式的数据:

1
<User><name>张三</name><age>18</age></User>

再将添加的消息转换器顺序还原,在请求头中添加 Accept 信息,指定数据格式为 XML:

1
2
3
4
5
6
7
8
9
10
11
12
@SneakyThrows
public static void test4() {
// --snip--

request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));

// --snip--
}

尽管转换成 JSON 的转换器在前,但会以请求头中指定的 Accept 信息为主:

1
<User><name>张三</name><age>18</age></User>

在上文基础上,在指定响应的 Content-Typeapplication/json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void test4() {
// --snip--

request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);

RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));

// --snip--
}

此时又会以 Content-Type 的信息为主:

1
{"name":"张三","age":18}

总结

@ResponseBody 注解由 RequestResponseBodyMethodProcessor 解析,但涉及到的数据格式转换由消息转换器完成。

当存在多个消息转换器时,如果选择 MediaType

  • 首先看 @RequestMapping 注解的 produces 为主,相当于设置了响应的 Content-Type,比如:
1
2
3
4
5
@ResponseBody
@RequestMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
public User user () {
return null;
}
  • 再看请求头中的 Accept 是否指定了目标格式
  • 最后按照消息转换器的添加顺序进行转换

29. ControllerAdvice 之 ResponseBodyAdvice

ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置:

sequenceDiagram
participant adapter as HandlerAdapter
participant bf as WebDataBinderFactory
participant mf as ModelFactory
participant ihm as ServletInvocableHandlerMethod
participant ar as ArgumentResolvers 
participant rh as ReturnValueHandlers
participant container as ModelAndViewContainer

adapter ->> +bf: 准备 @InitBinder
bf -->> -adapter: Void
adapter ->> +mf: 准备 @ModelAttribute
mf ->> +container: 添加 Model 数据
container -->> -mf: Void
mf -->> -adapter: Void
adapter ->> +ihm: invokeAndHandle
ihm ->> +ar: 获取 args
ar ->> ar: 有的解析器涉及 RequestBodyAdvice
ar ->> container: 有的解析器涉及数据绑定生成 Model 数据
ar -->> -ihm: args
ihm ->> ihm: method.invoke(bean,args) 得到 returnValue
ihm ->> +rh: 处理 returnValue
rect rgb(200, 150, 255)
rh ->> rh: 有的处理器涉及 ResponseBodyAdvice
end
rh ->> +container: 添加 Model 数据,处理视图名,是否渲染等
container -->> -rh: Void
rh -->> -ihm: Void
ihm -->> -adapter: Void
adapter ->> +container: 获取 ModelAndView
container -->> -adapter: Void

ResponseBodyAdvice 是一个接口,对于实现了这个接口并被 @ControllerAdvice 标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice 接口中的 beforeBodyWrite() 方法对返回值进行增强。

现有一个控制器类与内部使用到的 User 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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;
}

调用控制器方法,并输出响应数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
}

运行 main() 方法后,控制台输出:

{"name":"王五","age":18}

在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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 进行增强:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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);
}
}

进行上述增强后,再运行 main() 方法, 输出结果不变, 这是因为没有将实现的 ResponseBodyAdvice 添加到返回值处理器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers(AnnotationConfigApplicationContext context) {
// 添加 advice
List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context);
List<Object> responseBodyAdviceList = annotatedBeans.stream()
.filter(b -> b.getBeanType() != null
&& ResponseBodyAdvice.class.isAssignableFrom(b.getBeanType()))
.collect(Collectors.toList());

HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
// 省略其他返回值处理器的添加
composite.addHandler(new RequestResponseBodyMethodProcessor(
Collections.singletonList(new MappingJackson2HttpMessageConverter()),
responseBodyAdviceList
));
composite.addHandler(new ServletModelAttributeMethodProcessor(true));
return composite;
}

再次运行 main() 方法,控制台输出:

{"code":200,"data":{"name":"王五","age":18}}

如果将控制器方法修改成以下形式,也能输出相同的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
@ResponseBody
public static class MyController {
public User user() {
return new User("王五", 18);
}
}
// 或者
@RestController
public static class MyController {
public User user() {
return new User("王五", 18);
}
}

30. 异常处理

DispatcherServlet 中对异常处理的核心方法是 processHandlerException(),在这个方法中会对所有异常解析器进行遍历,然后使用每个异常解析器对异常信息进行处理。

存放异常解析器的是 DispatcherServlet 中泛型为 HandlerExceptionResolver、名为 handlerExceptionResolvers 的列表成员变量。

HandlerExceptionResolver 是一个接口,本节讲解解析 @ExceptionHandler 注解的异常解析器 ExceptionHandlerExceptionResolver

四个控制器类,测试异常处理方法被 @ResponseBody 注解标记、异常处理方法返回 ModelAndView、嵌套异常和对异常处理方法的参数处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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));
}

运行 main() 方法后,控制台打印出:

{"error":"除以零"}
{error=除以零}
test2
{"error":"除以零"}{"error":"e3"}
org.springframework.mock.web.MockHttpServletRequest@7c1e2a9e
{"error":"除以零"}{"error":"e3"}{"error":"e4"}

31. ControllerAdvice 之 @ExceptionHandler

控制器中被 @ExceptionHandler 标记的异常处理方法只会在当前控制器中生效,如果想要某个异常处理方法全局生效,则需要将异常处理方法编写在被 @ControllerAdvice 注解标记的类中。

一个“朴素”的控制器类:

1
2
3
4
static class Controller1 {
public void foo() {
}
}

当不存在任何异常处理方法时,调用控制器中的 foo() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SneakyThrows
public static void main(String[] args) {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();

ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2XmlHttpMessageConverter()));
resolver.afterPropertiesSet();

HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception exception = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, exception);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

main() 方法运行后,控制台不输出任何信息。

编写配置类,向 Spring 容器中添加 ExceptionHandlerExceptionResolver,并声明全局异常处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(Exception e) {
return Collections.singletonMap("error", e.getMessage());
}
}

@Bean
public ExceptionHandlerExceptionResolver resolver() {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
// 无需调用 resolver.afterPropertiesSet(); 方法,这是 Spring 的提供的内置拓展,会在 Spring 生命周期中自动执行
return resolver;
}
}

ExceptionHandlerExceptionResolver 不再直接通过 new 关键词构造,而是从 Spring 容器中获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SneakyThrows
public static void main(String[] args) {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ExceptionHandlerExceptionResolver resolver = context.getBean(ExceptionHandlerExceptionResolver.class);

HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception exception = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, exception);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

32. Tomcat 异常处理

可以利用 @ExceptionHandler@ControllerAdvice 注解全局对控制器方法中抛出的异常进行处理,但针对诸如 filter 中不在控制器方法中的异常就变得无能为力了。

因此需要一个更上层的“异常处理者”,这个“异常处理者”就是 Tomcat 服务器。

32.1 Tomcat 的错误页处理

首先将“老三样”利用配置类添加到 Spring 容器中,还要将 RequestMappingHandlerMappingRequestMappingHandlerAdapter 也添加到 Spring 容器中。

必要的控制器也不能少,控制器方法手动制造异常,但不提供使用 @ExceptionHandler 实现的异常处理方法,将产生的异常交由 Tomcat 处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@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;
}
}
}

利用 AnnotationConfigServletWebServerApplicationContext 创建 Spring Web 容器,并输出所有的路径映射信息:

1
2
3
4
5
6
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
handlerMapping.getHandlerMethods().forEach((k, v) -> System.out.println("映射路径: " + k + "\t方法信息: " + v));
}

运行 main() 方法后,控制台只输出一条路径映射信息:

映射路径: { [/test]}	方法信息: indi.mofan.a32.WebConfig$MyController#test()

在浏览器中访问 http://localhost:8080/test 地址:

Tomcat错误处理页

显示 Tomcat 的错误处理页,并在页面中输出了错误信息。

Tomcat 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?

修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 修改了 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 格式的数据:

1
2
3
4
5
6
7
@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

自定义Tomcat错误处理页

32.2 BasicErrorController

BasicErrorController 是由 SpringBoot 提供的类,它也是一个控制器:

1
2
3
4
5
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// --snip--
}

它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error

向容器中添加 BasicErrorController,构造 BasicErrorController 时需要传递两个参数:

  • errorAttributes:错误属性,可以理解成封装的错误信息对象
  • errorProperties:也可以翻译成错误属性,用于对输出的错误信息进行配置
1
2
3
4
@Bean
public BasicErrorController basicErrorController() {
return new BasicErrorController(new DefaultErrorAttributes(), new 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"
}

timestampstatus 等响应内容就是错误属性 errorAttributes 的中包含的内容。

返回的数据中并没有显示异常信息,可以通过配置文件进行配置:

1
server.error.include-exception=true

也可以在添加 BasicErrorController 到 Spring 容器中时,设置错误属性 errorProperties

1
2
3
4
5
6
@Bean
public BasicErrorController basicErrorController() {
ErrorProperties errorProperties = new ErrorProperties();
errorProperties.setIncludeException(true);
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}

重启程序,再次使用接口测试工具访问 http://localhost:8080/test

{
  "timestamp": 1674736991768,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "java.lang.ArithmeticException",
  "path": "/test"
}

使用浏览器访问

如果使用浏览器访问 http://localhost:8080/test,又会回到“解放前”,显示与 Tomcat 的默认错误处理页相同的内容。

这是因为使用浏览器访问时,将调用 BasicErrorController 中的 errorHtml() 控制器方法:

1
2
3
4
5
6
7
8
9
10
@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

1
2
3
4
5
6
7
8
@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 的名称进行匹配,还需要添加一个解析器:

1
2
3
4
5
@Bean
public ViewResolver viewResolver() {
// View 类型的 Bean 的名称即为视图名称
return new BeanNameViewResolver();
}

重启程序,使用浏览器访问 http://localhost:8080/test

使用BasicErrorController自定义error视图

控制台还打印出:

{timestamp=Thu Jan 26 21:01:50 CST 2023, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, path=/test}

33. BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter

33.1 功能与使用

BeanNameUrlHandlerMappingRequestMappingHandlerMapping 类似,也是用于解析请求路径,只不过 BeanNameUrlHandlerMapping 将根据请求路径在 Spring 容器中寻找同名的 Bean,对请求进行处理,这个 Bean 必须/ 开头。比如:请求路径为 /c1,寻找的 Bean 的名称也是 /c1

SimpleControllerHandlerAdapterRequestMappingHandlerAdapter 也类似,也是用于调用控制器方法,但要求控制器类必须实现 org.springframework.web.servlet.mvc.Controller 接口。

现有三个控制器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component("/c1")
public static class Controller1 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c1");
return null;
}
}

@Component("/c2")
public static class Controller2 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c2");
return null;
}
}

@Bean("/c3")
public Controller controller3() {
return (request, response) -> {
response.getWriter().print("this is c3");
return null;
};
}

提供配置类 WebConfig,添加 Web 换件下必要的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}

@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

@Bean
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}

@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
}
1
2
3
4
5
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

}

运行 main() 方法后,在浏览器中访问 http://localhost:8080/c1,页面上显示 this is c1。更换请求路径为 c2c3 后,也会出现类似的信息。

33.2 自定义实现

在配置类 WebConfig 中移除 Spring 提供的 BeanNameUrlHandlerMappingSimpleControllerHandlerAdapter,手动编码实现它们的功能。

为了与前文的测试形成对比,将 Controller2 的 Bean 名称设置为 c2,而不是 /c2,使其不能被解析到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Component
static class MyHandlerMapping implements HandlerMapping {

@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
String key = request.getRequestURI();
Controller controller = controllerMap.get(key);
if (controller == null) {
return null;
}
return new HandlerExecutionChain(controller);
}

@Autowired
private ApplicationContext context;

private Map<String, Controller> controllerMap;

@PostConstruct
public void init() {
controllerMap = context.getBeansOfType(Controller.class).entrySet().stream()
.filter(i -> i.getKey().startsWith("/"))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}

@Component
static class MyHandlerAdapter implements HandlerAdapter {

@Override
public boolean supports(Object handler) {
return handler instanceof Controller;
}

@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof Controller) {
((Controller) handler).handleRequest(request, response);
}
return null;
}

@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}

运行 main() 方法后,在浏览器中访问:

  • http://localhost:8080/c1,页面上显示 this is c1
  • http://localhost:8080/c2,页面上显示 404
  • http://localhost:8080/c3,页面上显示 this is c3

34. RouterFunctionMapping 与 HandlerFunctionAdapter

RouterFunctionMapping 在初始化时,在 Spring 容器中收集所有 RouterFunctionRouterFunction 包括两部分:

  1. RequestPredicate:设置映射条件
  2. HandlerFunction:处理逻辑

当请求到达时,根据映射条件找到 HandlerFunction,即 handler,然后使用 HandlerFunctionAdapter 调用 handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}

@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

@Bean
public RouterFunctionMapping routerFunctionMapping() {
return new RouterFunctionMapping();
}

@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {
return new HandlerFunctionAdapter();
}

@Bean
public RouterFunction<ServerResponse> r1() {
return route(GET("/r1"), req -> ok().body("this is r1"));
}

@Bean
public RouterFunction<ServerResponse> r2() {
return route(GET("/r2"), req -> ok().body("this is r2"));
}
}
1
2
3
4
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

运行 main() 方法后,在浏览器中访问 http://localhost:8080/r1,页面上显示 this is r1,访问 r2 时也类似。

35. SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter

35.1 功能与使用

概括一下,这两个主要用于静态资源处理,SimpleUrlHandlerMapping 用于静态资源映射,而静态资源处理器是 ResourceHttpRequestHandlerHttpRequestHandlerAdapter 用于处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}

@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}

@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
// 设置静态资源处理器,得到所有映射关系
Map<String, ResourceHttpRequestHandler> map = context.getBeansOfType(ResourceHttpRequestHandler.class);
mapping.setUrlMap(map);
return mapping;
}

@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
return new HttpRequestHandlerAdapter();
}

@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
// 以 / 结尾表示目录,否则认为是文件
handler.setLocations(Collections.singletonList(new ClassPathResource("static/")));
return handler;
}

@Bean("/img/**")
public ResourceHttpRequestHandler handler2() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(Collections.singletonList(new ClassPathResource("images/")));
return handler;
}
}

添加的两个 ResourceHttpRequestHandler 类型的 Bean,分别设置了它们处理 ClassPath 路径下哪个目录下的静态资源,那如何将请求路径与静态资源访问路径进行映射呢?

也就是说,当要访问 ClassPath 路径下的 static 目录下的静态资源时,应该通过哪个请求路径呢?

可以利用通配符设置添加的 ResourceHttpRequestHandler 类型的 Bean 的名称。

比如设置 Bean 的名称为 /**,那么在访问 localhost:8080/r1.html 时,就会尝试访问 ClassPath 路径下 static 目录中名为 r1.html 的静态资源;又比如设置 Bean 的名称为 /img/**,那么在访问 localhost:8080/img/1.jpg 时, 就会尝试访问 ClassPath 路径下 images 目录中名为 1.jpg 的静态资源。

35.2 资源解析器

ResourceHttpRequestHandler 用于对静态资源进行处理,但静态资源解析的功能是由 ResourceResolver 完成的。

ResourceHttpRequestHandler 实现了 InitializingBean 接口,查看重写的 afterPropertiesSet()

1
2
3
4
5
6
7
8
9
@Override
public void afterPropertiesSet() throws Exception {
resolveResourceLocations();

if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
// --snip--
}

当使用的资源解析器列表为空时,默认添加最基本的资源解析器 PathResourceResolver

尝试添加额外的资源解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
// 以 / 结尾表示目录,否则认为是文件
handler.setLocations(Collections.singletonList(new ClassPathResource("static/")));
// 不使用默认的资源解析器,而是使用自行添加的
handler.setResourceResolvers(Arrays.asList(
// 读取资源时使用缓存
new CachingResourceResolver(new ConcurrentMapCache("cache1")),
// 读取压缩资源
new EncodedResourceResolver(),
// 最基本的:从磁盘上读取静态资源
new PathResourceResolver()
));
return handler;
}

添加了三个资源解析器:

  1. CachingResourceResolver:对静态资源进行缓存
  2. EncodedResourceResolver:对静态资源进行压缩
  3. PathResourceResolver:最基本的资源处理器

还要注意添加的顺序,先尝试从缓存中获取,再尝试获取压缩文件,最后才是直接从磁盘上读取。

针对 EncodedResourceResolver 来说,Spring 不会自行对静态资源进行压缩,需要在配置类中提供压缩方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostConstruct
@SuppressWarnings("all")
public void initGzip() throws IOException {
Resource resource = new ClassPathResource("static");
File dir = resource.getFile();
for (File file : dir.listFiles(pathname -> pathname.getName().endsWith(".html"))) {
System.out.println(file);
try (FileInputStream fis = new FileInputStream(file);
GZIPOutputStream fos = new GZIPOutputStream(new FileOutputStream(file.getAbsoluteFile() + ".gz"))) {
byte[] bytes = new byte[8 * 1024];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
}
}
}

配置类对应的 Bean 初始化阶段时,将 ClassPath 路径下 static 目录中的静态资源进行压缩。

比如 static 目录下的 r1.html 会被压缩成 r1.html.gz,在访问 r1.html 时,会访问压缩文件 r1.html.gz,由浏览器识别并解压成 r1.html 进行访问,减少网络传输数据量。

35.3 欢迎页处理

将访问 根路径 的请求,映射到某一欢迎页。这个功能由 WelcomePageHandlerMapping 完成。

设置静态资源欢迎页为 ClassPath 下 static 目录中的 index.html 文件:

1
2
3
4
5
6
7
8
9
10
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
return new WelcomePageHandlerMapping(
null,
context,
resource,
"/**"
);
}

程序会根据配置的欢迎页映射器生成一个实现了 Controller 接口的处理器,使用 SimpleControllerHandlerAdapter 执行生成的处理器:

1
2
3
4
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}

重启程序,控制台会输出一条如下的日志,表示欢迎页配置成功:

o.s.b.a.w.s.WelcomePageHandlerMapping - Adding welcome page: class path resource [static/index.html] 

在浏览器上访问 localhost:8080 时,会直接访问静态资源 static/index.html 的内容。

注意: 如果重启程序后访问 localhost:8080 并没有跳转到配置的欢迎页,可以重新编译项目后在运行。

总结

WelcomePageHandlerMapping 作为欢迎页映射器,只将根路径,即 / 映射到配置的欢迎页。

它内置了一个处理器,名为 ParameterizableViewController,该处理器不执行逻辑,仅根据固定的视图名 forward:index.html 去寻找视图。

SimpleControllerHandlerAdapter 用于调用处理器,根据重定向到根路径的 index.html 页面,执行静态资源处理器,访问 static 目录下的 index.html 文件(在配置类中自行配置的)。

35.4 映射器与适配器总结

HandlerMapping 用于建立请求路径与控制器之间的映射关系:

  • RequestMappingHandlerMapping:解析 @RequestMapping 及其派生注解,建立请求路径与控制器方法之间的映射关系
  • WelcomePageHandlerMapping:映射 / 根路径,寻找欢迎页
  • BeanNameUrlHandlerMapping:与 Bean 的名称进行匹配,要求名称必须以 / 开头
  • RouterFunctionMapping:将 RequestPredicate 映射到 HandlerFunction
  • SimpleUrlHandlerMapping:静态资源映射

映射器之间的顺序也是有要求的,SpringBoot 中的映射器按上述顺序排序。

HandlerAdapter 用于对各种处理器进行适配调用(适配器 模式):

  • RequestMappingHandlerAdapter:执行被 @RequestMapping 标记的控制器方法,内部还会使用参数解析器、返回值处理器对控制器方法的参数、返回值进行处理(组合 模式)
  • SimpleControllerHandlerAdapter:执行实现了 Controller 接口的处理器
  • HandlerFunctionAdapter:处理 HandlerFunction 函数式接口
  • HttpRequestHandlerAdapter:处理 HttpRequestHandler 接口,用于静态资源处理

ResourceHttpRequestHandler 中的 setResourceResolvers() 方法是 责任链 模式体现。

36. MVC 处理流程

当浏览器发送一个请求 http://localhost:8080/hello 后,请求到达服务器,其处理流程是:

服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术

  • 路径:默认映射路径为 /,即会匹配到所有请求 URL,可作为请求的统一入口,DispatcherServlet 也被称之为 前控制器。但也有例外:
    • JSP 不会匹配到 DispatcherServlet
    • 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
  • 创建:在 SpringBoot 中,由自动配置类 DispatcherServletAutoConfiguration 提供 DispatcherServlet 的 Bean
  • 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
    • HandlerMapping,初始化时记录映射关系
    • HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
    • HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
    • ViewResolver

DispatcherServlet 利用 RequestMappingHandlerMapping 查找控制器方法

例如根据 /hello 路径找到被 @RequestMapping("/hello") 标记的控制器方法,控制器方法会被封装成 HandlerMethod 对象,并结合 匹配到的拦截器 一起返回给 DispatcherServlet

HandlerMethod拦截器 合称为 HandlerExecutionChain(调用链)对象。

DispatcherServlet 接下来会

  1. 调用拦截器的 preHandle() 方法,返回一个布尔类型的值。若返回 true,则放行,进行后续调用,反之拦截请求,不进行后续调用;
  2. RequestMappingHandlerAdapter 调用处理器方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
    • @ControllerAdvice 全局增强点 1️⃣:利用 @ModelAttribute 补充模型数据
    • @ControllerAdvice 全局增强点 2️⃣:利用 @InitBinder 补充自定义类型转换器
    • 使用 HandlerMethodArgumentResolver 准备参数
      • @ControllerAdvice 全局增强点 3️⃣:利用 RequestBodyAdvice 接口对请求体增强
    • 调用 ServletInvocableHandlerMethod
    • 使用 HandlerMethodReturnValueHandler 处理返回值
      • @ControllerAdvice 全局增强点 4️⃣:利用 RequestBodyAdvice 对响应体增强
    • 根据 ModelAndViewContainer 获取 ModelAndView
      • 如果返回的 ModelAndViewnull,不走第 4 步视图解析及渲染流程。例如返回值处理器调用了 HttpMessageConverter 将结果转换为 JSON,这时 ModelAndView 就为 null
      • 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
  3. 调用拦截器的 postHandle() 方法
  4. 处理异常或视图渲染
    • 如果 1~3 步中出现异常,使用 ExceptionHandlerExceptionResolver 处理异常流程
      • @ControllerAdvice 全局增强点 5️⃣:利用 @ExceptionHandler 进行统一异常处理
    • 未出现异常时,进行视图解析及渲染流程
  5. 调用拦截器的 afterCompletion() 方法