封面画师: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 容器工厂、DispatcherServlet
和 DispatcherServlet
注册对象。
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 @Configuration @ComponentScan public class WebConfig { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory () { return new TomcatServletWebServerFactory (); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @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
的初始化时机
断点 DispatcherServlet
的 onRefresh()
方法中 this.initStrategies(context);
的所在行:
1 2 3 protected void onRefresh (ApplicationContext context) { this .initStrategies(context); }
以 DEBUG 方式重启程序,此时程序尚未执行到断点处。
再次在浏览器中访问 localhost:8080
,程序执行到断点处。
查看调用栈可知,是从 GenericServlet
的 init()
方法执行到 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 { @Bean public TomcatServletWebServerFactory tomcatServletWebServerFactory (ServerProperties serverProperties) { return new TomcatServletWebServerFactory (serverProperties.getPort()); } @Bean public DispatcherServlet dispatcherServlet () { return new DispatcherServlet (); } @Bean public DispatcherServletRegistrationBean dispatcherServletRegistrationBean (DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties) { DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean (dispatcherServlet, "/" ); registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); return registrationBean; } }
再次重启程序,根据控制台输出的内容可知,Tomcat 此时监听的端口是 9090
,DispatcherServlet
也在 Tomcat 启动时被初始化。
DispatcherServlet
初始化时执行的操作
回到 DispatcherServlet
的 onRefresh()
方法,它又调用了 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) { } else { } if (this .handlerMappings == null ) { this .handlerMappings = this .getDefaultStrategies(context, HandlerMapping.class); } }
20.2 RequestMappingHandlerMapping
HandlerMapping
,即处理器映射器,用于建立请求路径与控制器方法的映射关系。
RequestMappingHandlerMapping
是 HandlerMapping
的一种实现,根据类名可知,它是通过 @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); 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()
方法中测试 RequestMappingHandlerAdapter
的 invokeHandlerMethod()
方法:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) throws Exception { 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 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdapter
的 getArgumentResolvers()
方法获取所有参数解析器。
Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdapter
的 getReturnValueHandlers()
方法获取所有返回值处理器。
自定义参数解析器
假如经常需要使用到请求头中的 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 { 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 { String str = new Yaml ().dump(returnValue); 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 { 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 { 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
:参数对象
mavContainer
:ModelAndView
容器,用来存储中间的 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(); 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 container = new ModelAndViewContainer (); for (MethodParameter parameter : handlerMethod.getMethodParameters()) { 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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, true ) ); 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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver () ); } }
修改 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
@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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory) ); } }
[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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory) ); } }
[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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory) ); } }
[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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver () ); } }
[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 user1
和 User 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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new ServletModelAttributeMethodProcessor (true ) ); 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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new RequestResponseBodyMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())), new ServletModelAttributeMethodProcessor (true ) ); } }
[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 { for (MethodParameter parameter : handlerMethod.getMethodParameters()) { HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite (); composite.addResolvers( new RequestParamMethodArgumentResolver (beanFactory, false ), new PathVariableMethodArgumentResolver (), new RequestHeaderMethodArgumentResolver (beanFactory), new ServletCookieValueMethodArgumentResolver (beanFactory), new ExpressionValueMethodArgumentResolver (beanFactory), new ServletRequestMethodArgumentResolver (), new ServletModelAttributeMethodProcessor (false ), new RequestResponseBodyMethodProcessor (Collections.singletonList(new MappingJackson2HttpMessageConverter ())), new ServletModelAttributeMethodProcessor (true ), new RequestParamMethodArgumentResolver (beanFactory, true ) ); } }
[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()
方法的参数名称不再是 name
和 age
,也就是说直接使用 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.class
,foo()
方法的反编译结果如下:
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.class
,foo()
方法的反编译结果如下:
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); LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer (); String[] parameterNames = discoverer.getParameterNames(foo); System.out.println(Arrays.toString(parameterNames)); }
[name, age]
在【21. 参数解析器】中并没有使用 LocalVariableTableParameterNameDiscoverer
,而是使用的是 DefaultParameterNameDiscoverer
。DefaultParameterNameDiscoverer
将两种实现进行了统一:
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
Parser
把 String
转为其它类型
Formatter
综合 Printer
与 Parser
的功能
Converter
把类型 S
转为类型 T
Printer
、Parser
、Converter
经过适配转换成 GenericConverter
放入 Converters
集合
FormattingConversionService
利用其它接口实现转换
底层第二套转换接口
由 JDK 提供,而不是 Spring。
classDiagram
PropertyEditorRegistry o-- "多" PropertyEditor
<<interface>> PropertyEditorRegistry
<<interface>> PropertyEditor
PropertyEditor
将 String
与其它类型相互转换
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
委派ConversionService
与 PropertyEditorRegistry
真正执行转换(使用 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) { 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) { 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
方法,可以调用 DataBinder
的 initDirectFieldAccess()
方法使数据绑定逻辑走字段赋值,而不是属性赋值:
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) { 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); }
birthday
和 address.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.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) { InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod (new MyController (), MyController.class.getMethod("myMethod" , WebDataBinder.class)); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (Collections.singletonList(handlerMethod), null ); }
再次执行 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
,证据就在 WebDataBinder
的 addCustomFormatter()
方法中:
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) { FormattingConversionService service = new FormattingConversionService (); service.addFormatter(new MyDateFormatter ("用 ConversionService 方式拓展转换功能" )); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , initializer); }
[DEBUG] indi.mofan.a23.MyDateFormatter - >>>>>> 进入了: 用 ConversionService 方式拓展转换功能
TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))
如果同时存在 @InitBinder
和 ConversionService
,将以 @InitBinder
为主,@InitBinder
实现的转换器属于自定义转换器,自定义转换器的优先级更高:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SneakyThrows public static void main (String[] args) { 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); }
[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) { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService (); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer (); initializer.setConversionService(conversionService); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory (null , initializer); }
运行 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) { 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()); if (teacherDaoType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) teacherDaoType; System.out.println(parameterizedType.getActualTypeArguments()[0 ]); } System.out.println(">>>>>>>>>>>>>>>>>>>>>>>" ); Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class); System.out.println(t); 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
的来源有两个:
@ControllerAdvice
标记的类中 @InitBinder
标记的方法,由 RequestMappingHandlerAdapter
在初始化时解析并记录
@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 { 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
,主要完成三件事:
准备参数
反射调用控制器方法
处理返回值
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) { System.out.println("foo" ); return null ; } } @Getter @Setter @ToString static class User { private String name; } }
创建 Spring 容器,Mock 请求,创建 HandlerMethod
对象指定需要执行的控制器方法,创建 DataBinderFactory
数据绑定工厂。向创建的 HandlerMethod
对象中添加数据绑定工厂、参数名称解析器、参数解析器(暂不考虑返回值的处理),最后创建模型视图容器,调用 HandlerMethod
的 invokeAndHandle
方法执行控制器方法:
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 ()); 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 中。当未指定 @ModelAttribute
的 value
时,添加到 ModelMap 中的 key 是对象类型首字母小写对应的字符串。此时的 @ModelAttribute
注解由 ServletModelAttributeMethodProcessor
解析。
当其作用在方法上时:
如果该方法在被 @Controller
注解标记的类中,会在当前控制器中每个控制器方法执行前执行被 @ModelAttribute
标记的方法,如果该方法有返回值,自动将返回值添加到 ModelMap 中。当未指定 @ModelAttribute
的 value
时,添加到 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) { 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" ); 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); }
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; } @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); 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 ); }
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}
与 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
、返回值解析器 HttpEntityMethodProcessor
和 HttpEntityMethodProcessor
时,都需要传入消息转换器列表。
消息转换器的基类是 HttpMessageConverter
。
介绍两个常见的消息转换器的实现:
一个 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()); } }
将 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 () { 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 2 3 4 5 6 7 8 9 10 11 @SneakyThrows public static void test4 () { RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2XmlHttpMessageConverter (), new MappingJackson2HttpMessageConverter () )); }
这下会将 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 () { request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2HttpMessageConverter (), new MappingJackson2XmlHttpMessageConverter () )); }
尽管转换成 JSON 的转换器在前,但会以请求头中指定的 Accept
信息为主:
1 <User > <name > 张三</name > <age > 18</age > </User >
在上文基础上,在指定响应的 Content-Type
为 application/json
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SneakyThrows public static void test4 () { request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE); response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE); RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor (Arrays.asList( new MappingJackson2HttpMessageConverter (), new MappingJackson2XmlHttpMessageConverter () )); }
此时又会以 Content-Type
的信息为主:
总结
@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) { 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) { 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 ())); 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 容器中,还要将 RequestMappingHandlerMapping
和 RequestMappingHandlerAdapter
也添加到 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 () { return new RequestMappingHandlerMapping (); } @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter () { RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter (); 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 默认提供的错误处理方式返回的是 HTML 格式的数据,但需要返回 JSON 格式的数据又该怎么自定义呢?
修改 Tomcat 默认的错误处理路径,并添加后置处理器进行注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public ErrorPageRegistrar errorPageRegistrar () { return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage ("/error" )); } @Bean public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor () { 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) { 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
:
32.2 BasicErrorController
BasicErrorController
是由 SpringBoot 提供的类,它也是一个控制器:
1 2 3 4 5 @Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { }
它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /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"
}
timestamp
、status
等响应内容就是错误属性 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 () { return new BeanNameViewResolver (); }
重启程序,使用浏览器访问 http://localhost:8080/test
:
控制台还打印出:
{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 功能与使用
BeanNameUrlHandlerMapping
与 RequestMappingHandlerMapping
类似,也是用于解析请求路径,只不过 BeanNameUrlHandlerMapping
将根据请求路径在 Spring 容器中寻找同名的 Bean,对请求进行处理,这个 Bean 必须 以 /
开头。比如:请求路径为 /c1
,寻找的 Bean 的名称也是 /c1
。
SimpleControllerHandlerAdapter
与 RequestMappingHandlerAdapter
也类似,也是用于调用控制器方法,但要求控制器类必须实现 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
。更换请求路径为 c2
、c3
后,也会出现类似的信息。
33.2 自定义实现
在配置类 WebConfig
中移除 Spring 提供的 BeanNameUrlHandlerMapping
与 SimpleControllerHandlerAdapter
,手动编码实现它们的功能。
为了与前文的测试形成对比,将 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 容器中收集所有 RouterFunction
,RouterFunction
包括两部分:
RequestPredicate
:设置映射条件
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
用于静态资源映射,而静态资源处理器是 ResourceHttpRequestHandler
,HttpRequestHandlerAdapter
用于处理器。
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 ()); } }
当使用的资源解析器列表为空时,默认添加最基本的资源解析器 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; }
添加了三个资源解析器:
CachingResourceResolver
:对静态资源进行缓存
EncodedResourceResolver
:对静态资源进行压缩
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 接下来会
调用拦截器的 preHandle()
方法,返回一个布尔类型的值。若返回 true
,则放行,进行后续调用,反之拦截请求,不进行后续调用;
RequestMappingHandlerAdapter
调用处理器方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer
、将 HandlerMethod
完善为 ServletInvocableHandlerMethod
@ControllerAdvice 全局增强点 1️⃣:利用 @ModelAttribute
补充模型数据
@ControllerAdvice 全局增强点 2️⃣:利用 @InitBinder
补充自定义类型转换器
使用 HandlerMethodArgumentResolver
准备参数
@ControllerAdvice 全局增强点 3️⃣:利用 RequestBodyAdvice
接口对请求体增强
调用 ServletInvocableHandlerMethod
使用 HandlerMethodReturnValueHandler
处理返回值
@ControllerAdvice 全局增强点 4️⃣:利用 RequestBodyAdvice
对响应体增强
根据 ModelAndViewContainer
获取 ModelAndView
如果返回的 ModelAndView
为 null
,不走第 4 步视图解析及渲染流程。例如返回值处理器调用了 HttpMessageConverter
将结果转换为 JSON,这时 ModelAndView
就为 null
如果返回的 ModelAndView
不为 null
,会在第 4 步走视图解析及渲染流程
调用拦截器的 postHandle()
方法
处理异常或视图渲染
如果 1~3 步中出现异常,使用 ExceptionHandlerExceptionResolver
处理异常流程
@ControllerAdvice 全局增强点 5️⃣:利用 @ExceptionHandler
进行统一异常处理
未出现异常时,进行视图解析及渲染流程
调用拦截器的 afterCompletion()
方法