封面画师:T5-茨舞(微博) 封面ID:104944884
参考视频:黑马程序员Spring视频教程,全面深度讲解spring5底层原理
源码仓库:mofan212/advanced-spring (github.com)
20. RequestMappingHandlerMapping 与 RequestMappingHandlerAdapter
20.1 DispatcherServlet 的初始化
选择支持内嵌 Tomcat 服务器的 Spring 容器作为 ApplicationContext 的实现:
public static void main(String[] args) {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}
WebConfig 作为配置类,向 Spring 容器中添加内嵌 Web 容器工厂、DispatcherServlet 和 DispatcherServlet 注册对象。
/**
* @author mofan
* @date 2023/1/22 22:38
*/
@Configuration
@ComponentScan
public class WebConfig {
/**
* 内嵌 Web 容器工厂
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
/**
* 创建 DispatcherServlet
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
/**
* 注册 DispatcherServlet,Spring MVC 的入口
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
}
运行 main() 方法,控制台打印出:
Tomcat initialized with port(s): 8080 (http) Root WebApplicationContext: initialization completed in 2132 ms
Tomcat 容器初始化成功,Spring 容器初始化成功,但 DispatcherServlet 还未被初始化。
当 Tomcat 服务器 首次 使用到 DispatcherServlet 时,才会由 Tomcat 服务器初始化 DispatcherServlet。
清空控制台信息,使用浏览器访问 localhost:8080,控制台打印出:
信息: Initializing Spring DispatcherServlet 'dispatcherServlet' [INFO ] Initializing Servlet 'dispatcherServlet' [TRACE] No MultipartResolver 'multipartResolver' declared [TRACE] No LocaleResolver 'localeResolver': using default [AcceptHeaderLocaleResolver] [TRACE] No ThemeResolver 'themeResolver': using default [FixedThemeResolver] [TRACE] No HandlerMappings declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties [TRACE] No HandlerAdapters declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties [TRACE] No HandlerExceptionResolvers declared in servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties [TRACE] No RequestToViewNameTranslator 'viewNameTranslator': using default [DefaultRequestToViewNameTranslator] [TRACE] No ViewResolvers declared for servlet 'dispatcherServlet': using default strategies from DispatcherServlet.properties [TRACE] No FlashMapManager 'flashMapManager': using default [SessionFlashMapManager] [INFO] Completed initialization in 482 ms
完成 DispatcherServlet 的初始化。
使用 DEBUG 查看
DispatcherServlet的初始化时机
断点 DispatcherServlet 的 onRefresh() 方法中 this.initStrategies(context); 的所在行:
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
以 DEBUG 方式重启程序,此时程序尚未执行到断点处。
再次在浏览器中访问 localhost:8080,程序执行到断点处。
查看调用栈可知,是从 GenericServlet 的 init() 方法执行到 onRefresh() 方法的:
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
因此 DispatcherServlet 的初始化流程走的是 Servlet 的初始化流程。
使
DispatcherServlet在 Tomcat 服务器启动时被初始化
修改添加到 Spring 容器的 DispatcherServlet 注册 Bean:
@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 注解标记的方法的参数直接在方法中使用即可:
server.port=9090
spring.mvc.servlet.load-on-startup=1
@Configuration
@ComponentScan
@PropertySource("classpath:application.properties")
@EnableConfigurationProperties({WebMvcProperties.class, ServerProperties.class})
public class WebConfig {
/**
* 内嵌 Web 容器工厂
*/
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties) {
return new TomcatServletWebServerFactory(serverProperties.getPort());
}
/**
* 创建 DispatcherServlet
*/
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
/**
* 注册 DispatcherServlet,Spring MVC 的入口
*/
@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
return registrationBean;
}
}
再次重启程序,根据控制台输出的内容可知,Tomcat 此时监听的端口是 9090,DispatcherServlet 也在 Tomcat 启动时被初始化。
DispatcherServlet初始化时执行的操作
回到 DispatcherServlet 的 onRefresh() 方法,它又调用了 initStrategies() 方法:
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
在这个方法中初始化了一系列组件,见名识意即可,重点介绍:
initHandlerMappings():初始化处理器映射器initHandlerAdapters():初始化处理器适配器initHandlerExceptionResolvers():初始化异常处理器
在所有的初始化方法中都有一个相似的逻辑,首先使用一个布尔值判断是否检测 所有 目标组件。
Spring 支持父子容器嵌套,如果判断的布尔值为 true,那么 Spring 不仅会在当前容器中获取目标组件,还会在其所有父级容器中寻找。
以 initHandlerMappings() 为例:
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) { // 是否需要检测所有处理器映射器
// --snip--
} else {
// 无需检测所有处理器映射器时,获取当前容器中的处理器映射器
// --snip--
}
if (this.handlerMappings == null) {
// 当前容器中没有处理器映射器时,设置默认的处理器映射器
this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
// --snip--
}
// --snip--
}
20.2 RequestMappingHandlerMapping
HandlerMapping,即处理器映射器,用于建立请求路径与控制器方法的映射关系。
RequestMappingHandlerMapping 是 HandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。
当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。
在配置类中将 RequestMappingHandlerMapping 添加到 Spring 容器:
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
定义一个控制器类:
@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,再获取请求路径与映射器方法的映射关系,并根据给定请求获取控制器方法:
public static void main(String[] args) throws Exception {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
// 解析 @RequestMapping 以及派生注解,在初始化时生成路径与控制器方法的映射关系
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 获取映射结果
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
handlerMethods.forEach((k, v) -> System.out.println(k + " = " + v));
// 根据给定请求获取控制器方法,返回处理器执行链
HandlerExecutionChain chain = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/test1"));
System.out.println(chain);
}
{GET [/test1]} = indi.mofan.a20.Controller1#test1()
{POST [/test2]} = indi.mofan.a20.Controller1#test2(String)
{PUT [/test3]} = indi.mofan.a20.Controller1#test3(String)
{ [/test4]} = indi.mofan.a20.Controller1#test4()
HandlerExecutionChain with [indi.mofan.a20.Controller1#test1()] and 0 interceptors
getHandler() 方法返回的对象时处理器执行链,不仅包含映射器方法,还包含需要执行的拦截器信息。
MockHttpServletRequest的使用
需要导入以下依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
20.3 RequestMappingHandlerAdapter
RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。
同样需要在配置类中将 RequestMappingHandlerAdapter 添加到 Spring 容器,但该类中需要测试的方法被 protected 修饰,无法直接使用,因此创建一个子类,将子类添加到 Spring 容器中:
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
return super.invokeHandlerMethod(request, response, handlerMethod);
}
}
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}
在 main() 方法中测试 RequestMappingHandlerAdapter 的 invokeHandlerMethod() 方法:
public static void main(String[] args) throws Exception {
// --snip--
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test2");
request.setParameter("name", "mofan");
MockHttpServletResponse response = new MockHttpServletResponse();
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, ((HandlerMethod) handlerMapping.getHandler(request).getHandler()));
}
[DEBUG] indi.mofan.a20.Controller1 - test2(mofan)
实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?
显然是需要解析 @RequestParam 注解。
Spring 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 MyRequestMappingHandlerAdapter 的 getArgumentResolvers() 方法获取所有参数解析器。
Spring 也支持许多种类的控制器方法返回值类型,使用 MyRequestMappingHandlerAdapter 的 getReturnValueHandlers() 方法获取所有返回值处理器。
自定义参数解析器
假如经常需要使用到请求头中的 Token 信息,自定义 @Token 注解,使用该注解标记控制器方法的哪个参数来获取 Token 信息:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Token {
}
使 test3() 控制器方法参数被 @Token 标记:
@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
log.debug("test3({})", token);
return null;
}
自定义参数解析器:
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 中:
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
adapter.setCustomArgumentResolvers(Collections.singletonList(tokenArgumentResolver));
return adapter;
}
测试执行 test3() 控制器方法:
public static void main(String[] args) throws Exception {
// --snip--
MockHttpServletRequest tokenRequest = new MockHttpServletRequest("PUT", "/test3");
tokenRequest.addHeader("token", "token info");
handlerAdapter.invokeHandlerMethod(tokenRequest, response, ((HandlerMethod) handlerMapping.getHandler(tokenRequest).getHandler()));
}
[DEBUG] indi.mofan.a20.Controller1 - test3(token info)
自定义返回值处理器
当 @ResponseBody 标记了控制器方法时,方法的返回值会转换成 JSON 写入响应体中。
自定义 @Yml 注解,被 @Yml 注解标记的控制器方法的返回值会转换成 YAML 写入响应体中。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Yml {
}
使 test4() 控制器方法被 @Yml 注解标记:
@RequestMapping("/test4")
@Yml
public User test4() {
log.debug("test4");
return new User("张三", 18);
}
自定义返回值处理器将返回值转换成 YAML:
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return returnType.getMethodAnnotation(Yml.class) != null;
}
@Override
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 转换返回结果为 YAML
String str = new Yaml().dump(returnValue);
// 将 YAML 字符串写入响应体
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType("text/plain;charset=utf-8");
response.getWriter().print(str);
// 设置请求已经处理完毕
mavContainer.setRequestHandled(true);
}
}
将返回值处理器添加到 HandlerAdapter 中:
@Bean
public MyRequestMappingHandlerAdapter myRequestMappingHandlerAdapter() {
MyRequestMappingHandlerAdapter adapter = new MyRequestMappingHandlerAdapter();
YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();
adapter.setCustomReturnValueHandlers(Collections.singletonList(ymlReturnValueHandler));
return adapter;
}
测试执行 test4() 控制器方法:
public static void main(String[] args) throws Exception {
// --snip--
MockHttpServletRequest test4Req = new MockHttpServletRequest("GET", "/test4");
handlerAdapter.invokeHandlerMethod(test4Req, response, ((HandlerMethod) handlerMapping.getHandler(test4Req).getHandler()));
byte[] content = response.getContentAsByteArray();
System.out.println(new String(content, StandardCharsets.UTF_8));
}
[DEBUG] indi.mofan.a20.Controller1 - test4
!!indi.mofan.a20.Controller1$User {age: 18, name: 张三}
21. 参数解析器
Spring 提供了许多种类的控制器方法参数解析器,定义一个包含多个不同种类参数的控制器方法:
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 并打印方法中每个参数的信息:
public static void main(String[] args) throws Exception {
// 控制器方法封装成 HandlerMethod
Method method = Controller.class.getMethod("test", String.class, String.class,
int.class, String.class, MultipartFile.class,
int.class, String.class, String.class,
String.class, HttpServletRequest.class, User.class,
User.class, User.class);
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), method);
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
String annotations = Arrays.stream(parameter.getParameterAnnotations())
.map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining());
String appendAt = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
System.out.println("[" + parameter.getParameterIndex() + "] " + appendAt +
parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName());
}
}
[0] @RequestParam String name1 [1] String name2 [2] @RequestParam int age [3] @RequestParam String home1 [4] @RequestParam MultipartFile file [5] @PathVariable int id [6] @RequestHeader String header [7] @CookieValue String token [8] @Value String home2 [9] HttpServletRequest request [10] @ModelAttribute User user1 [11] User user2 [12] @RequestBody User user3
21.2 @RequestParam
@RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:
beanFactory:Bean 工厂对象。需要解析${}时,就需要指定 Bean 工厂对象useDefaultResolution:布尔类型参数。为false表示只解析添加了@RequestParam注解的参数,为true针对未添加@RequestParam注解的参数也使用该参数解析器进行解析
RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:
parameter:参数对象mavContainer:ModelAndView容器,用来存储中间的 Model 结果webRequest:由ServletWebRequest封装后的请求对象binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的18转换成整型
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
HttpServletRequest request = mockRequest();
// 控制器方法封装成 HandlerMethod
Method method = Controller.class.getMethod("test", String.class, String.class,
int.class, String.class, MultipartFile.class,
int.class, String.class, String.class,
String.class, HttpServletRequest.class, User.class,
User.class, User.class);
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), method);
// 准备对象绑定与类型转换
ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);
// 准备 ModelAndViewContainer 用来存储中间的 Model 结果
ModelAndViewContainer container = new ModelAndViewContainer();
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, true);
String annotations = Arrays.stream(parameter.getParameterAnnotations())
.map(i -> i.annotationType().getSimpleName()).collect(Collectors.joining());
String appendAt = annotations.length() > 0 ? "@" + annotations + " " : "";
// 设置参数名解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
String paramInfo = "[" + parameter.getParameterIndex() + "] " + appendAt +
parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName();
if (resolver.supportsParameter(parameter)) {
Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(Objects.requireNonNull(v).getClass());
System.out.println(paramInfo + " -> " + v);
} else {
System.out.println(paramInfo);
}
}
}
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 的类,对上述逻辑进行封装。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, true)
);
// --snip--
if (composite.supportsParameter(parameter)) {
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(paramInfo + " -> " + v);
} else {
System.out.println(paramInfo);
}
}
}
21.2 @PathVariable
@PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver()
);
// --snip--
}
}
修改 RequestParamMethodArgumentResolver 参数解析器的构造,将 useDefaultResolution 设置为 false,让程序 暂时 不抛出异常。
[0] @RequestParam String name1 -> zhangsan [1] String name2 [2] @RequestParam int age -> 18 [3] @RequestParam String home1 -> D:\environment\JDK1.8 [4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@11c9af63 [5] @PathVariable int id -> 123
21.3 @RequestHeader
@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个 Bean 工厂对象。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory)
);
// --snip--
}
}
[0] @RequestParam String name1 -> zhangsan [1] String name2 [2] @RequestParam int age -> 18 [3] @RequestParam String home1 -> D:\environment\JDK1.8 [4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@3943a2be [5] @PathVariable int id -> 123 [6] @RequestHeader String header -> application/json
21.4 @CookieValue
@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个 Bean 工厂对象。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory)
);
// --snip--
}
}
[0] @RequestParam String name1 -> zhangsan [1] String name2 [2] @RequestParam int age -> 18 [3] @RequestParam String home1 -> D:\environment\JDK1.8 [4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@1329eff [5] @PathVariable int id -> 123 [6] @RequestHeader String header -> application/json [7] @CookieValue String token -> 123456
21.5 @Value
@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个 Bean 工厂对象。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory)
);
// --snip--
}
}
[0] @RequestParam String name1 -> zhangsan [1] String name2 [2] @RequestParam int age -> 18 [3] @RequestParam String home1 -> D:\environment\JDK1.8 [4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@46fa7c39 [5] @PathVariable int id -> 123 [6] @RequestHeader String header -> application/json [7] @CookieValue String token -> 123456 [8] @Value String home2 -> D:\environment\JDK1.8
21.6 HttpServletRequest
HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver()
);
// --snip--
}
}
[0] @RequestParam String name1 -> zhangsan [1] String name2 [2] @RequestParam int age -> 18 [3] @RequestParam String home1 -> D:\environment\JDK1.8 [4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5f683daf [5] @PathVariable int id -> 123 [6] @RequestHeader String header -> application/json [7] @CookieValue String token -> 123456 [8] @Value String home2 -> D:\environment\JDK1.8 [9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@152aa092
ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:
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 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。
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
) {
}
}
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new ServletModelAttributeMethodProcessor(true)
);
// --snip--
if (composite.supportsParameter(parameter)) {
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), binderFactory);
System.out.println(paramInfo + " -> " + v);
// 打印模型数据
ModelMap modelMap = container.getModel();
if (MapUtils.isNotEmpty(modelMap)) {
System.out.println("模型数据: " + modelMap);
}
} else {
System.out.println(paramInfo);
}
}
}
[0] @RequestParam String name1 -> zhangsan
[1] String name2
[2] @RequestParam int age -> 18
[3] @RequestParam String home1 -> D:\environment\JDK1.8
[4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@2beee7ff
[5] @PathVariable int id -> 123
[6] @RequestHeader String header -> application/json
[7] @CookieValue String token -> 123456
[8] @Value String home2 -> D:\environment\JDK1.8
[9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5fa07e12
[10] @ModelAttribute User user1 -> A21.User(name=张三, age=18)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
[11] User user2 -> A21.User(name=张三, age=18)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
[12] @RequestBody User user3 -> A21.User(name=李四, age=20)
模型数据: {abc=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.abc=org.springframework.validation.BeanPropertyBindingResult: 0 errors, user=A21.User(name=张三, age=18), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
@RequestBody User user3 参数也被 ServletModelAttributeMethodProcessor 解析了,如果想使其数据通过 JSON 数据转换而来,则需要使用另一个参数解析器。
21.8 @RequestBody
@RequestBody 注解的解析需要使用到 RequestResponseBodyMethodProcessor 参数解析器。构造时需要传入一个消息转换器列表。
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)
);
// --snip--
}
}
[0] @RequestParam String name1 -> zhangsan [1] String name2 [2] @RequestParam int age -> 18 [3] @RequestParam String home1 -> D:\environment\JDK1.8 [4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a [5] @PathVariable int id -> 123 [6] @RequestHeader String header -> application/json [7] @CookieValue String token -> 123456 [8] @Value String home2 -> D:\environment\JDK1.8 [9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645 [10] @ModelAttribute User user1 -> A21.User(name=张三, age=18) [11] User user2 -> A21.User(name=张三, age=18) [12] @RequestBody User user3 -> A21.User(name=李四, age=20)
@RequestBody User user3 参数数据通过 JSON 数据得到,与上一节的解析进行区分。
除此之外,添加的参数解析器顺序也影响着解析结果:
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true)
先添加解析 @ModelAttribute 注解的解析器,再添加解析 @RequestBody 注解的解析器,最后添加解析省略了 @ModelAttribute 注解的解析器。如果更换最后两个解析器的顺序,那么 @RequestBody User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor。
因此 String name2 参数也能通过添加同种参数但不同构造参数的解析器进行解析,注意添加的解析器的顺序,先处理对象,再处理单个参数:
public static void main(String[] args) throws Exception {
// --snip--
// 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
// 多个参数解析器的组合
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
// useDefaultResolution 为 false 表示必须添加 @RequestParam 注解
new RequestParamMethodArgumentResolver(beanFactory, false),
// 解析 @PathVariable
new PathVariableMethodArgumentResolver(),
// 解析 @RequestHeader
new RequestHeaderMethodArgumentResolver(beanFactory),
// 解析 @CookieValue
new ServletCookieValueMethodArgumentResolver(beanFactory),
// 解析 @Value
new ExpressionValueMethodArgumentResolver(beanFactory),
// 解析 HttpServletRequest
new ServletRequestMethodArgumentResolver(),
// 解析 @ModelAttribute,且不能省略
new ServletModelAttributeMethodProcessor(false),
new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true),
new RequestParamMethodArgumentResolver(beanFactory, true)
);
// --snip--
}
}
[0] @RequestParam String name1 -> zhangsan [1] String name2 -> lisi [2] @RequestParam int age -> 18 [3] @RequestParam String home1 -> D:\environment\JDK1.8 [4] @RequestParam MultipartFile file -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@5e17553a [5] @PathVariable int id -> 123 [6] @RequestHeader String header -> application/json [7] @CookieValue String token -> 123456 [8] @Value String home2 -> D:\environment\JDK1.8 [9] HttpServletRequest request -> org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@13bc8645 [10] @ModelAttribute User user1 -> A21.User(name=张三, age=18) [11] User user2 -> A21.User(name=张三, age=18) [12] @RequestBody User user3 -> A21.User(name=李四, age=20)
22. 获取参数名
在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译:
package indi.mofan.a22;
public class Bean2 {
public void foo(String name, int age) {
}
}
将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容:
package indi.mofan.a22;
public class Bean2 {
public Bean2() {
}
public void foo(String var1, int var2) {
}
}
编译生成的 class 文件中的 foo() 方法的参数名称不再是 name 和 age,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。
执行 javac -parameters .\Bean2.java 再次编译 Bean2.java,并查看得到的 Bean2.class 文件内容:
package indi.mofan.a22;
public class Bean2 {
public Bean2() {
}
public void foo(String name, int age) {
}
}
foo() 方法的参数名称得以保留。
还可以使用 javap -c -v .\Bean2.class 命令反编译 Bean2.class,foo() 方法的反编译结果如下:
public void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 6: 0
MethodParameters:
Name Flags
name
age
foo() 方法的参数信息被保存在 MethodParameters 中,可以使用 反射 获取:
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() 方法的反编译结果如下:
public void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lindi/mofan/a22/Bean2;
0 1 1 name Ljava/lang/String;
0 1 2 age I
foo() 方法的参数信息被保存在 LocalVariableTable 中,不能使用反射获取,但可以使用 ASM 获取,使用 Spring 封装的解析工具:
public static void main(String[] args) throws Exception {
// 反射获取参数名
Method foo = Bean2.class.getMethod("foo", String.class, int.class);
// 基于 LocalVariableTable 本地变量表获取
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(foo);
System.out.println(Arrays.toString(parameterNames));
}
[name, age]
在【21. 参数解析器】中并没有使用 LocalVariableTableParameterNameDiscoverer,而是使用的是 DefaultParameterNameDiscoverer。DefaultParameterNameDiscoverer 将两种实现进行了统一:
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
javac -g的局限性
假设有这样一个接口:
package indi.mofan.a22;
public interface Bean1 {
public void foo(String name, int age);
}
如果使用 javac -g .\Bean1.java 命令进行编译后,再利用 javap 查看 foo() 方法的反编译结果:
public abstract void foo(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC, ACC_ABSTRACT
并没有记录抽象方法 foo() 的参数信息。
如果使用 javac -parameters .\Bean1.java 呢?
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 三种转换接口
底层第一套转换接口与实现
Printer把其它类型转为StringParser把String转为其它类型Formatter综合Printer与Parser的功能Converter把类型S转为类型TPrinter、Parser、Converter经过适配转换成GenericConverter放入Converters集合FormattingConversionService利用其它接口实现转换
底层第二套转换接口
由 JDK 提供,而不是 Spring。
PropertyEditor将String与其它类型相互转换PropertyEditorRegistry可以注册多个PropertyEditor对象- 可以通过
FormatterPropertyEditorAdapter与第一套接口进行适配
高层转换接口与实现
- 它们都实现了
TypeConverter高层转换接口,在转换时会用到TypeConverterDelegate委派ConversionService与PropertyEditorRegistry真正执行转换(使用 Facade 门面模式)- 首先查看是否存在实现了
PropertyEditorRegistry的自定义转换器,@InitBinder注解实现的就是自定义转换器(用了适配器模式把Formatter转为需要的PropertyEditor) - 再查看是否存在
ConversionService实现 - 再利用默认的
PropertyEditor实现 - 最后有一些特殊处理
- 首先查看是否存在实现了
SimpleTypeConverter仅做类型转换BeanWrapperImpl利用Property,即Getter/Setter,为 Bean 的属性赋值,,必要时进行类型转换DirectFieldAccessor利用Field,即字段,为 Bean 的字段赋值,必要时进行类型转换ServletRequestDataBinder为 Bean 的属性执行绑定,必要时进行类型转换,根据布尔类型成员变量directFieldAccess选择利用Property还是Field,还具备校验与获取校验结果功能
23.2 使用示例
SimpleTypeConverter
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
public static void main(String[] args) {
// 利用反射为 bean 的属性赋值
MyBean bean = new MyBean();
BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
wrapper.setPropertyValue("a", "10");
wrapper.setPropertyValue("b", "hello");
wrapper.setPropertyValue("c", "1999/03/04");
System.out.println(bean);
}
@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestBeanWrapper.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
DirectFieldAccessor
public static void main(String[] args) {
// 利用反射为 bean 的字段赋值
MyBean bean = new MyBean();
DirectFieldAccessor accessor = new DirectFieldAccessor(bean);
accessor.setPropertyValue("a", "10");
accessor.setPropertyValue("b", "hello");
accessor.setPropertyValue("c", "1999/03/04");
System.out.println(bean);
}
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestFieldAccessor.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
DataBinder
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() 方法使数据绑定逻辑走字段赋值,而不是属性赋值:
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。
public static void main(String[] args) {
// web 环境下的数据绑定
MyBean bean = new MyBean();
DataBinder dataBinder = new ServletRequestDataBinder(bean);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("a", "10");
request.setParameter("b", "hello");
request.setParameter("c", "1999/03/04");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(bean);
}
@Getter
@Setter
@ToString
static class MyBean {
private int a;
private String b;
private Date c;
}
TestServletDataBinder.MyBean(a=10, b=hello, c=Thu Mar 04 00:00:00 CST 1999)
23.3 绑定器工厂
现有如下两个类:
@Getter
@Setter
@ToString
public static class User {
private Date birthday;
private Address address;
}
@Getter
@Setter
@ToString
public static class Address {
private String name;
}
在 Web 环境下进行数据绑定:
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 完成,以便添加各种选项,拓展不同的自定义转换器。
@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 注解标记的方法:
static class MyController {
@InitBinder
public void myMethod(WebDataBinder dataBinder) {
// 拓展 dataBinder 的转换器
dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 进行拓展"));
}
}
以 WebDataBinder 作为方法参数,在方法体类添加自定义转换器 MyDateFormatter:
@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 对象:
@SneakyThrows
public static void main(String[] args) {
// --snip--
InvocableHandlerMethod handlerMethod =
new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(handlerMethod), null);
// --snip--
}
再次执行 main(),birthday 被成功绑定:
[DEBUG] indi.mofan.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展 TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))
这种方式使用了 JDK 提供的 PropertyEditorRegistry,证据就在 WebDataBinder 的 addCustomFormatter() 方法中:
public void addCustomFormatter(Formatter<?> formatter) {
FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter);
getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter);
}
ConversionService拓展
选择 FormattingConversionService 作为 ConversionService 的实现,向其中添加自定义转换器 MyDateFormatter。
构造 DataBinderFactory 时传入 WebBindingInitializer 的实现,因此将 FormattingConversionService 封装成 ConfigurableWebBindingInitializer 传入 DataBinderFactory 的构造方法中:
@SneakyThrows
public static void main(String[] args) {
// --snip--
// 使用 ConversionService 转换
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
// --snip--
}
[DEBUG] indi.mofan.a23.MyDateFormatter - >>>>>> 进入了: 用 ConversionService 方式拓展转换功能 TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))
如果同时存在 @InitBinder 和 ConversionService,将以 @InitBinder 为主,@InitBinder 实现的转换器属于自定义转换器,自定义转换器的优先级更高:
@SneakyThrows
public static void main(String[] args) {
// --snip--
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式拓展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
InvocableHandlerMethod handlerMethod =
new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("myMethod", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(handlerMethod), initializer);
// --snip--
}
[DEBUG] indi.mofan.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 进行拓展 TestServletDataBinderFactory.User(birthday=Sat Jan 02 00:00:00 CST 1999, address=TestServletDataBinderFactory.Address(name=成都))
默认的
ConversionService
ConversionService 有一个默认实现 DefaultFormattingConversionService,它还是 FormattingConversionService 的子类:
@SneakyThrows
public static void main(String[] args) {
// --snip--
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(conversionService);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
// --snip--
}
运行 main() 方法后,控制台打印出:
TestServletDataBinderFactory.User(birthday=null, address=TestServletDataBinderFactory.Address(name=成都))
birthday 绑定失败,默认的 ConversionService 需要搭配注解 @DateTimeFormat 使用。在目标类的字段上使用该注解标记,并指定被转换的日期格式:
@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,接收一个泛型参数:
public class BaseDao<T> {
T findOne() {
return null;
}
}
围绕 BaseDao 有如下五个类:
public class EmployeeDao extends BaseDao {
}
public class Student {
}
public class StudentDao extends BaseDao<Student> {
}
public class Teacher {
}
public class TeacherDao extends BaseDao<Teacher> {
}
尝试获取 BaseDao 子类泛型参数:
public static void main(String[] args) {
// 1. java api
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
// 带有泛型信息的父类信息
Type teacherDaoType = TeacherDao.class.getGenericSuperclass();
System.out.println("TeacherDao type: " + teacherDaoType);
System.out.println("TeacherDao type class: " + teacherDaoType.getClass());
Type employeeDaoType = EmployeeDao.class.getGenericSuperclass();
System.out.println("EmployeeDao type: " + employeeDaoType);
System.out.println("EmployeeDao type class: " + employeeDaoType.getClass());
// 有泛型参数的 Type 对象才是 ParameterizedType 类型
if (teacherDaoType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) teacherDaoType;
System.out.println(parameterizedType.getActualTypeArguments()[0]);
}
// 2. spring api 1
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class);
System.out.println(t);
// 3. spring api 2
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(ResolvableType.forClass(StudentDao.class).getSuperType().getGeneric().resolve());
}
>>>>>>>>>>>>>>>>>>>>>>> TeacherDao type: indi.mofan.a23.sub.BaseDaoTeacherDao 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 调用过程中所处的位置:
备注:
RequestMappingHandlerAdapter在图中缩写为HandlerAdapterHandlerMethodArgumentResolverComposite在图中缩写为ArgumentResolversHandlerMethodReturnValueHandlerComposite在图中缩写为ReturnValueHandlers
功能与使用
@InitBinder 注解只能作用在方法上,通常搭配 @ControllerAdvice 和 @Controller 以及他们的衍生注解使用。比如:
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@InitBinder
public void binder3(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
}
}
@Controller
static class Controller1 {
@InitBinder
public void binder1(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
}
public void foo() {
}
}
@Controller
static class Controller2 {
@InitBinder
public void binder21(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder21 转换器"));
}
@InitBinder
public void binder22(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new MyDateFormatter("binder22 转换器"));
}
public void foo() {
}
}
}
当 @InitBinder 作用的方法存在于被 @ControllerAdvice 标记的类里面时,是对 所有 控制器都生效的自定义类型转换器。当 @InitBinder 作用的方法存在于被 @Controller 标记的类里面时,是 只对当前 控制器生效的自定义类型转换器。
@InitBinder 的来源有两个:
@ControllerAdvice标记的类中@InitBinder标记的方法,由RequestMappingHandlerAdapter在初始化时解析并记录@Controller标记的类中@InitBinder标记的方法,由RequestMappingHandlerAdapter在控制器方法首次执行时解析并记录
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 标记的方法并没有被解析记录。
模拟调用控制器方法:
public static void main(String[] args) throws Exception {
// --snip--
log.debug("1. 刚开始...");
showBindMethods(handlerAdapter);
Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
getDataBinderFactory.setAccessible(true);
log.debug("2. 模拟调用 Controller1 的 foo 方法...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
showBindMethods(handlerAdapter);
log.debug("3. 模拟调用 Controller2 的 bar 方法时...");
getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller2(), WebConfig.Controller2.class.getMethod("bar")));
showBindMethods(handlerAdapter);
context.close();
}
1. 刚开始... 全局的 @InitBinder 方法 [binder3] 控制器的 @InitBinder 方法 [] 2. 模拟调用 Controller1 的 foo 方法... 全局的 @InitBinder 方法 [binder3] 控制器的 @InitBinder 方法 [Controller1.binder1] 3. 模拟调用 Controller2 的 bar 方法时... 全局的 @InitBinder 方法 [binder3] 控制器的 @InitBinder 方法 [Controller1.binder1, Controller2.binder22, Controller2.binder21]
首次调用控制器中的方法时,控制器中被 @InitBinder 标记方法被解析记录。
25. 控制器方法执行流程
ServletInvocableHandlerMethod的组成
HandlerMethod 需要:
bean,即哪个 Controllermethod,即 Controller 中的哪个方法
ServletInvocableHandlerMethod 需要:
WebDataBinderFactory,用于对象绑定、类型转换ParameterNameDiscoverer,用于参数名解析HandlerMethodArgumentResolverComposite,用于解析参数HandlerMethodReturnValueHandlerComposite,用于处理返回值
控制器方法执行流程
以 RequestMappingHandlerAdapter 为起点,创建 WebDataBinderFactory,添加自定义类型转换器,再创建 ModelFactory,添加 Model 数据:
接下来调用 ServletInvocableHandlerMethod,主要完成三件事:
- 准备参数
- 反射调用控制器方法
- 处理返回值
代码演示
提供配置类 WebConfig,其中包含一个控制器 Controller1:
@Configuration
public class WebConfig {
@Controller
static class Controller1 {
@ResponseStatus(HttpStatus.OK)
public ModelAndView foo(User user) {
// 使用 @ResponseStatus 注解,咋不考虑返回值的处理
System.out.println("foo");
return null;
}
}
@Getter
@Setter
@ToString
static class User {
private String name;
}
}
创建 Spring 容器,Mock 请求,创建 HandlerMethod 对象指定需要执行的控制器方法,创建 DataBinderFactory 数据绑定工厂。向创建的 HandlerMethod 对象中添加数据绑定工厂、参数名称解析器、参数解析器(暂不考虑返回值的处理),最后创建模型视图容器,调用 HandlerMethod 的 invokeAndHandle 方法执行控制器方法:
@SneakyThrows
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "mofan");
ServletInvocableHandlerMethod handlerMethod =
new ServletInvocableHandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo", WebConfig.User.class));
ServletRequestDataBinderFactory binderFactory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(binderFactory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
// getArgumentResolvers() 组装了一系列参数解析器,省略具体实现
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
// 暂不考虑返回值的处理
ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());
context.close();
}
foo
{user=WebConfig.User(name=mofan), org.springframework.validation.BindingResult.user=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
26. ControllerAdvice 之 @ModelAttribute
准备 @ModelAttribute 在整个 HandlerAdapter 调用过程中所处的位置:
功能与使用
@ModelAttribute 可以作用在参数上和方法上。
当其作用在参数上时,会将请求中的参数信息 按名称 注入到指定对象中,并将这个对象信息自动添加到 ModelMap 中。当未指定 @ModelAttribute 的 value 时,添加到 ModelMap 中的 key 是对象类型首字母小写对应的字符串。此时的 @ModelAttribute 注解由 ServletModelAttributeMethodProcessor 解析。
当其作用在方法上时:
- 如果该方法在被
@Controller注解标记的类中,会在当前控制器中每个控制器方法执行前执行被@ModelAttribute标记的方法,如果该方法有返回值,自动将返回值添加到 ModelMap 中。当未指定@ModelAttribute的value时,添加到 ModelMap 中的 key 是返回值类型首字母小写对应的字符串 - 如果该方法在被
@ControllerAdvice注解标记的类中,会在所有控制器方法执行前执行该方法
作用在方法上的 @ModelAttribute 注解由 RequestMappingHandlerAdapter 解析。
@Controller
static class Controller1 {
@ResponseStatus(HttpStatus.OK)
public ModelAndView foo(@ModelAttribute("u") User user) {
// 使用 @ResponseStatus 注解,咋不考虑返回值的处理
System.out.println("foo");
return null;
}
}
@ControllerAdvice
static class MyControllerAdvice {
@ModelAttribute("a")
public String aa() {
return "aa";
}
}
先不使用 RequestMappingHandlerAdapter 对作用在方法上的 @ModelAttribute 注解进行解析,沿用【25. 控制器方法执行流程】中的 main() 方法:
foo
{u=WebConfig.User(name=mofan), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
再解析方法上的 @ModelAttribute 注解:
@SneakyThrows
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "mofan");
// --snip--
// 获取模型工厂
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, binderFactory);
// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);
// --snip--
}
foo
{a=aa, u=WebConfig.User(name=mofan), org.springframework.validation.BindingResult.u=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
{a=aa} 也被放入到 ModelAndViewContainer 中。
27. 返回值处理器
含有多种返回值的控制器
@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 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
Freemarker 配置类:
@Configuration
public class WebConfig {
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setDefaultEncoding("utf-8");
configurer.setTemplateLoaderPath("classpath:templates");
return configurer;
}
/**
* FreeMarkerView 在借助 Spring 初始化时,会要求在 web 环境才会走 setConfiguration, 这里想办法去掉了 web 环境的约束
*/
@Bean
public FreeMarkerViewResolver viewResolver(FreeMarkerConfigurer configurer) {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver() {
@Override
protected AbstractUrlBasedView instantiateView() {
FreeMarkerView view = new FreeMarkerView() {
@Override
protected boolean isContextRequired() {
return false;
}
};
view.setConfiguration(configurer.getConfiguration());
return view;
}
};
resolver.setContentType("text/html;charset=utf-8");
resolver.setPrefix("/");
resolver.setSuffix(".ftl");
resolver.setExposeSpringMacroHelpers(false);
return resolver;
}
}
渲染视图使用的方法:
@SuppressWarnings("all")
private static void renderView(ApplicationContext context, ModelAndViewContainer container,
ServletWebRequest webRequest) throws Exception {
log.debug(">>>>>> 渲染视图");
FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class);
String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator().getViewName(webRequest.getRequest());
log.debug("没有获取到视图名, 采用默认视图名: {}", viewName);
// 每次渲染时, 会产生新的视图对象, 它并非被 Spring 所管理, 但确实借助了 Spring 容器来执行初始化
View view = resolver.resolveViewName(viewName, Locale.getDefault());
view.render(container.getModel(), webRequest.getRequest(), webRequest.getResponse());
System.out.println(new String(((MockHttpServletResponse) webRequest.getResponse()).getContentAsByteArray(), StandardCharsets.UTF_8));
}
提供构造
HandlerMethodReturnValueHandlerComposite对象的方法
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 的响应信息。
@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 中,之后根据视图信息找到对应的模板页面,再将模型数据填充到模板页面中,完成视图的渲染。
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:
<!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 处理,构造时无需传入任何参数。
private static void test2(ApplicationContext context) {
testReturnValueProcessor(context, "test2", null, null);
}
对应的模板页面 view2.ftl:
<!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 标记,并指定请求地址,比如:
@ModelAttribute
@RequestMapping("/test3")
public User test3() {
log.debug("test3()");
return new User("李四", 20);
}
当未找到视图名称时,默认以请求路径作为视图名称。
但在本节测试中省略了路径映射这一步,因此需要通过编程的方式将请求路径解析后的结果放入 request 作用域中。
private static Consumer<MockHttpServletRequest> mockHttpServletRequestConsumer(String methodName) {
return req -> {
req.setRequestURI("/" + methodName);
UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(req);
};
}
private static void test3(ApplicationContext context) {
String methodName = "test3";
testReturnValueProcessor(context, methodName,
mockHttpServletRequestConsumer(methodName), null);
}
对应的模板页面 test3.ftl:
<!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() 也可以按照相同方式测试:
private static void test4(ApplicationContext context) {
String methodName = "test4";
testReturnValueProcessor(context, methodName, mockHttpServletRequestConsumer(methodName), null);
}
对应的模板页面 test4.ftl:
<!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() 方法中得以论证:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) throws Exception {
// 一进入方法就设置请求处理完毕
mavContainer.setRequestHandled(true);
// --snip--
}
HttpEntity 中包含了状态码、响应体信息和响应头信息。
尝试在请求处理完毕后,输出响应体信息:
private static final Consumer<MockHttpServletResponse> RESPONSE_CONSUMER = resp -> {
for (String name : resp.getHeaderNames()) {
System.out.println(name + " = " + resp.getHeader(name));
}
System.out.println(new String(resp.getContentAsByteArray(), StandardCharsets.UTF_8));
};
private static void test5(ApplicationContext context) {
testReturnValueProcessor(context, "test5", null, RESPONSE_CONSUMER);
}
indi.mofan.a27.A27$Controller - test5()
{}
null
Content-Type = application/json
{"name":"赵六","age":40}
27.5 HttpHeaders
与 HttpEntity 相比,HttpHeaders 只包含响应头信息,HttpHeaders 类型的返回值由 HttpHeadersReturnValueHandler 处理,构造时无需传入任何参数。
与 HttpEntity 一样,这种类型的返回值也表示响应完成,无需经过视图的解析、渲染流程再生成响应,也可在处理器的 handleReturnValue() 方法中得以论证(省略源码)。
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() 方法中得以论证(省略源码)。
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。
介绍两个常见的消息转换器的实现:
-
MappingJackson2HttpMessageConverter -
MappingJackson2XmlHttpMessageConverter
一个 User 类:
@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 格式的数据:
@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());
}
}
{"name":"张三","age":18}
将 User 对象转换成 XML 格式的数据:
@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 时,需要额外导入依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<User><name>李四</name><age>20</age></User>
将 JSON 格式的数据转换成 User 对象:
@SneakyThrows
public static void test3() {
//language=JSON
String json = "{\n" +
" \"name\": \"李四\",\n" +
" \"age\": 20\n" +
"}";
MockHttpInputMessage message = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) {
Object read = converter.read(User.class, message);
System.out.println(read);
}
}
A28.User(name=李四, age=20)
如果存在多个消息转换器呢?
@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 格式的数据:
{"name":"张三","age":18}
调换添加的消息转换器顺序:
@SneakyThrows
public static void test4() {
// --snip--
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2XmlHttpMessageConverter(),
new MappingJackson2HttpMessageConverter()
));
// --snip--
}
这下会将 User 对象转换成 XML 格式的数据:
<User><name>张三</name><age>18</age></User>
再将添加的消息转换器顺序还原,在请求头中添加 Accept 信息,指定数据格式为 XML:
@SneakyThrows
public static void test4() {
// --snip--
request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));
// --snip--
}
尽管转换成 JSON 的转换器在前,但会以请求头中指定的 Accept 信息为主:
<User><name>张三</name><age>18</age></User>
在上文基础上,在指定响应的 Content-Type 为 application/json:
@SneakyThrows
public static void test4() {
// --snip--
request.addHeader(HttpHeaders.ACCEPT, MimeTypeUtils.APPLICATION_XML_VALUE);
response.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(Arrays.asList(
new MappingJackson2HttpMessageConverter(),
new MappingJackson2XmlHttpMessageConverter()
));
// --snip--
}
此时又会以 Content-Type 的信息为主:
{"name":"张三","age":18}
总结
@ResponseBody 注解由 RequestResponseBodyMethodProcessor 解析,但涉及到的数据格式转换由消息转换器完成。
当存在多个消息转换器时,如果选择 MediaType:
- 首先看
@RequestMapping注解的produces为主,相当于设置了响应的Content-Type,比如:
@ResponseBody
@RequestMapping(produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
public User user () {
return null;
}
- 再看请求头中的
Accept是否指定了目标格式 - 最后按照消息转换器的添加顺序进行转换
29. ControllerAdvice 之 ResponseBodyAdvice
ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置:
ResponseBodyAdvice 是一个接口,对于实现了这个接口并被 @ControllerAdvice 标记的类来说,能够在调用每个控制器方法返回结果前,调用重写的 ResponseBodyAdvice 接口中的 beforeBodyWrite() 方法对返回值进行增强。
现有一个控制器类与内部使用到的 User 类:
@Controller
public static class MyController {
@ResponseBody
public User user() {
return new User("王五", 18);
}
}
@Getter
@Setter
@ToString
@AllArgsConstructor
public static class User {
private String name;
private int age;
}
调用控制器方法,并输出响应数据:
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
context.getBean(WebConfig.MyController.class),
WebConfig.MyController.class.getMethod("user")
);
handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(Collections.emptyList(), null));
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
// 设置参数解析器(省略源码)
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
// 设置返回值处理器(省略源码)
handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandlers(context));
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request, response), container);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
context.close();
}
运行 main() 方法后,控制台输出:
{"name":"王五","age":18}
在实际开发场景中常常需要对返回的数据类型进行统一,比如都返回 Result 类型:
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
private int code;
private String msg;
private Object data;
@JsonCreator
private Result(@JsonProperty("code") int code, @JsonProperty("data") Object data) {
this.code = code;
this.data = data;
}
private Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
public static Result ok() {
return new Result(200, null);
}
public static Result ok(Object data) {
return new Result(200, data);
}
public static Result error(String msg) {
return new Result(500, "服务器内部错误:" + msg);
}
}
除了直接让控制器方法返回 Result 外,还可以使用 ResponseBodyAdvice 进行增强:
@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
/*
* 满足条件才转换
* 1. 控制器方法被 @ResponseBody 注解标记
* 2. 控制器方法所在类被 @ResponseBody 注解或包含 @ResponseBody 注解的注解标记
*/
return returnType.getMethodAnnotation(ResponseBody.class) != null
|| AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.ok(body);
}
}
进行上述增强后,再运行 main() 方法, 输出结果不变, 这是因为没有将实现的 ResponseBodyAdvice 添加到返回值处理器中。
public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers(AnnotationConfigApplicationContext context) {
// 添加 advice
List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context);
List<Object> responseBodyAdviceList = annotatedBeans.stream()
.filter(b -> b.getBeanType() != null
&& ResponseBodyAdvice.class.isAssignableFrom(b.getBeanType()))
.collect(Collectors.toList());
HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
// 省略其他返回值处理器的添加
composite.addHandler(new RequestResponseBodyMethodProcessor(
Collections.singletonList(new MappingJackson2HttpMessageConverter()),
responseBodyAdviceList
));
composite.addHandler(new ServletModelAttributeMethodProcessor(true));
return composite;
}
再次运行 main() 方法,控制台输出:
{"code":200,"data":{"name":"王五","age":18}}
如果将控制器方法修改成以下形式,也能输出相同的结果:
@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、嵌套异常和对异常处理方法的参数处理:
static class Controller1 {
public void foo() {
}
@ResponseBody
@ExceptionHandler
public Map<String, Object> handle(ArithmeticException e) {
return Collections.singletonMap("error", e.getMessage());
}
}
static class Controller2 {
public void foo() {
}
@ExceptionHandler
public ModelAndView handler(ArithmeticException e) {
return new ModelAndView("test2", Collections.singletonMap("error", e.getMessage()));
}
}
static class Controller3 {
public void foo() {
}
@ResponseBody
@ExceptionHandler
public Map<String, Object> handle(IOException e) {
return Collections.singletonMap("error", e.getMessage());
}
}
static class Controller4 {
public void foo() {}
@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(Exception e, HttpServletRequest request) {
System.out.println(request);
return Collections.singletonMap("error", e.getMessage());
}
}
@SneakyThrows
public static void main(String[] args) {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
// 调用该方法,添加默认的参数解析器和返回值处理器
resolver.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception e = new ArithmeticException("除以零");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
handlerMethod = new HandlerMethod(new Controller2(), Controller2.class.getMethod("foo"));
ModelAndView modelAndView = resolver.resolveException(request, response, handlerMethod, e);
System.out.println(modelAndView.getModel());
System.out.println(modelAndView.getViewName());
// 嵌套异常
handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
// 异常处理方法参数处理
handlerMethod = new HandlerMethod(new Controller4(), Controller4.class.getMethod("foo"));
e = new Exception("e4");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
运行 main() 方法后,控制台打印出:
{"error":"除以零"}
{error=除以零}
test2
{"error":"除以零"}{"error":"e3"}
org.springframework.mock.web.MockHttpServletRequest@7c1e2a9e
{"error":"除以零"}{"error":"e3"}{"error":"e4"}
31. ControllerAdvice 之 @ExceptionHandler
控制器中被 @ExceptionHandler 标记的异常处理方法只会在当前控制器中生效,如果想要某个异常处理方法全局生效,则需要将异常处理方法编写在被 @ControllerAdvice 注解标记的类中。
一个「朴素」的控制器类:
static class Controller1 {
public void foo() {
}
}
当不存在任何异常处理方法时,调用控制器中的 foo() 方法:
@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,并声明全局异常处理方法:
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice {
@ExceptionHandler
@ResponseBody
public Map<String, Object> handle(Exception e) {
return Collections.singletonMap("error", e.getMessage());
}
}
@Bean
public ExceptionHandlerExceptionResolver resolver() {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
// 无需调用 resolver.afterPropertiesSet(); 方法,这是 Spring 的提供的内置拓展,会在 Spring 生命周期中自动执行
return resolver;
}
}
ExceptionHandlerExceptionResolver 不再直接通过 new 关键词构造,而是从 Spring 容器中获取:
@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 处理:
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
// 解析 @RequestMapping
return new RequestMappingHandlerMapping();
}
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
// 注意默认的 RequestMappingHandlerAdapter 不会带 jackson 转换器
handlerAdapter.setMessageConverters(Collections.singletonList(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}
@Controller
public static class MyController {
@RequestMapping("test")
public ModelAndView test() {
int i = 1 / 0;
return null;
}
}
}
利用 AnnotationConfigServletWebServerApplicationContext 创建 Spring Web 容器,并输出所有的路径映射信息:
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 默认的错误处理路径,并添加后置处理器进行注册:
/**
* 修改了 Tomcat 服务器默认错误地址
*/
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
/*
* ErrorPageRegistrar 由 SpringBoot 提供,TomcatServletWebServerFactory 也实现了该接口
* 出现错误,会使用请求转发 forward 跳转到 error 地址
*/
return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}
@Bean
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
/*
* 在 TomcatServletWebServerFactory 初始化完成前,获取容器中所有的 ErrorPageRegistrar
* 并将这些 ErrorPageRegistrar 进行注册
*/
return new ErrorPageRegistrarBeanPostProcessor();
}
重启程序,再次在浏览器中访问 http://localhost:8080/test,此时页面上不再显示 Tomcat 的默认错误处理页,而是产生了 404 错误。
这是因为整个程序中并没有名称为 error 的页面,或者为 /error 的请求路径。在控制器中添加请求路径为 /error 的控制器方法,该方法被 @ResponseBody 标记,最终返回 JSON 格式的数据:
@RequestMapping("/error")
@ResponseBody
public Map<String, Object> error(HttpServletRequest request) {
// tomcat 会将异常对象存储到 request 作用域中,可以直接获取
Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
return Collections.singletonMap("error", e.getMessage());
}
再次重启程序,控制台输出的路径映射信息多了一条:
映射路径: { [/error]} 方法信息: indi.mofan.a32.WebConfig$MyController#error(HttpServletRequest)
映射路径: { [/test]} 方法信息: indi.mofan.a32.WebConfig$MyController#test()
在浏览器中访问 http://localhost:8080/test:

32.2 BasicErrorController
BasicErrorController 是由 SpringBoot 提供的类,它也是一个控制器:
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// --snip--
}
它的映射路径会先从配置文件中读取,在未进行任何配置的情况下,默认路径是 /error。
向容器中添加 BasicErrorController,构造 BasicErrorController 时需要传递两个参数:
errorAttributes:错误属性,可以理解成封装的错误信息对象errorProperties:也可以翻译成错误属性,用于对输出的错误信息进行配置
@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 的中包含的内容。
返回的数据中并没有显示异常信息,可以通过配置文件进行配置:
server.error.include-exception=true
也可以在添加 BasicErrorController 到 Spring 容器中时,设置错误属性 errorProperties:
@Bean
public BasicErrorController basicErrorController() {
ErrorProperties errorProperties = new ErrorProperties();
errorProperties.setIncludeException(true);
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}
重启程序,再次使用接口测试工具访问 http://localhost:8080/test:
{
"timestamp": 1674736991768,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.ArithmeticException",
"path": "/test"
}
使用浏览器访问
如果使用浏览器访问 http://localhost:8080/test,又会回到「解放前」,显示与 Tomcat 的默认错误处理页相同的内容。
这是因为使用浏览器访问时,将调用 BasicErrorController 中的 errorHtml() 控制器方法:
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
该方法返回 ModelAndView,并且在没有添加新的错误视图的情况下,尝试寻找视图名称为 error 的视图。
这里既没有添加新的错误视图,也没有名称为 error 的视图,因此最终又会交由 Tomcat 进行处理。
尝试向 Spring 容器中添加一个 View 视图,Bean 的名字 必须 是 error:
@Bean
public View error() {
return (model, request, response) -> {
System.out.println(model);
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("<h3>服务器内部错误</h3>");
};
}
为了能够在查找指定名称的视图时按照 View 类型的 Bean 的名称进行匹配,还需要添加一个解析器:
@Bean
public ViewResolver viewResolver() {
// View 类型的 Bean 的名称即为视图名称
return new BeanNameViewResolver();
}
重启程序,使用浏览器访问 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 接口。
现有三个控制器类:
@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:
@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();
}
}
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,使其不能被解析到。
@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 c1http://localhost:8080/c2,页面上显示404http://localhost:8080/c3,页面上显示this is c3
34. RouterFunctionMapping 与 HandlerFunctionAdapter
RouterFunctionMapping 在初始化时,在 Spring 容器中收集所有 RouterFunction,RouterFunction 包括两部分:
RequestPredicate:设置映射条件HandlerFunction:处理逻辑
当请求到达时,根据映射条件找到 HandlerFunction,即 handler,然后使用 HandlerFunctionAdapter 调用 handler。
@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"));
}
}
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 用于处理器。
@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():
@Override
public void afterPropertiesSet() throws Exception {
resolveResourceLocations();
if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
// --snip--
}
当使用的资源解析器列表为空时,默认添加最基本的资源解析器 PathResourceResolver。
尝试添加额外的资源解析器:
@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 不会自行对静态资源进行压缩,需要在配置类中提供压缩方法:
@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 文件:
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
return new WelcomePageHandlerMapping(
null,
context,
resource,
"/**"
);
}
程序会根据配置的欢迎页映射器生成一个实现了 Controller 接口的处理器,使用 SimpleControllerHandlerAdapter 执行生成的处理器:
@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映射到HandlerFunctionSimpleUrlHandlerMapping:静态资源映射
映射器之间的顺序也是有要求的,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
- JSP 不会匹配到
- 创建:在 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接口对请求体增强
- @ControllerAdvice 全局增强点 3️⃣:利用
- 调用
ServletInvocableHandlerMethod - 使用
HandlerMethodReturnValueHandler处理返回值- @ControllerAdvice 全局增强点 4️⃣:利用
RequestBodyAdvice对响应体增强
- @ControllerAdvice 全局增强点 4️⃣:利用
- 根据
ModelAndViewContainer获取ModelAndView- 如果返回的
ModelAndView为null,不走第 4 步视图解析及渲染流程。例如返回值处理器调用了HttpMessageConverter将结果转换为 JSON,这时ModelAndView就为null - 如果返回的
ModelAndView不为null,会在第 4 步走视图解析及渲染流程
- 如果返回的
- @ControllerAdvice 全局增强点 1️⃣:利用
- 调用拦截器的
postHandle()方法 - 处理异常或视图渲染
- 如果 1~3 步中出现异常,使用
ExceptionHandlerExceptionResolver处理异常流程- @ControllerAdvice 全局增强点 5️⃣:利用
@ExceptionHandler进行统一异常处理
- @ControllerAdvice 全局增强点 5️⃣:利用
- 未出现异常时,进行视图解析及渲染流程
- 如果 1~3 步中出现异常,使用
- 调用拦截器的
afterCompletion()方法