我们知道RequestMappingHandlerAdapter一个重要的作用就是去执行控制器方法,而这个控制器方法就是下面的HandlerMethod
HandlerMethod 需要
Controller有了,Controller中的方法有了,那么我们就可以使用反射进行调用了。
当然这远远不够,我们的方法上有参数,我们要进行一系列的处理,所以RequestMappingHandlerAdapter其实最后执行的是ServletInvocableHandlerMethod,它具有以下四个组件:
准备阶段
根据这幅图我们可以看到:
调用阶段
这个时候万事俱备,就可以开始调用我们前面说的ServletInvocableHandlerMethod了,调用的时候会做三件事:
接下来我们帮上述过程使用代码模拟一下:
public class A26 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "张三");
/*
现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下
*/
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
new Controller1(), Controller1.class.getMethod("foo", User.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(factory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
ModelAndViewContainer container = new ModelAndViewContainer();
// 获取模型工厂方法
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);
// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());
context.close();
}
@ModelAttribute有两种用法:
这个地方要注意:
什么是ModelFactory?
ModelFactory
是Spring MVC框架中的一个接口,用于创建和管理模型对象(模型对象
是指用于在控制器方法和视图之间传递数据的Java对象,它可以让我们方便地将数据从控制器方法传递到视图中,从而实现动态生成页面的效果)。它的主要作用是创建模型对象,以及在控制器方法中共享模型对象。
具体来说,ModelFactory定义了两个方法:
- createModel(): 用于创建一个新的模型对象。该方法会在每次请求到达控制器方法时被调用,用于创建一个新的模型对象。
- mergeAttributes(): 用于将模型对象中的属性添加到ModelAndView对象中。该方法会在控制器方法执行完毕后被调用,用于将模型对象中的属性添加到ModelAndView对象中,以便在视图中使用。
在Spring MVC框架中,默认使用的是ExtendedModelMap类来实现ModelFactory接口。ExtendedModelMap类继承了LinkedHashMap类,因此它可以存储键值对,并且可以保持键值对的顺序。
前4种方法都要走视图渲染流程:
而后三种直接响应就行,不用走视图的解析、渲染流程,ModelAndViewContainer中是空的。这是因为他们的处理器中把ModelAndViewContainer.requestHandled 设为 true
MessageConverter是一个接口,它的主要作用是:将请求信息(如JSON、XML等)转换为对象,或将对象转换为响应信息。
当客户端发送HTTP请求时,请求中的数据可能以不同的格式进行传输,比如JSON、XML、文本等。而服务器端需要将这些数据转换成Java对象,以便后续的处理。此时,MessageConverter就可以派上用场了。它可以根据请求中的Content-Type头信息来自动选择相应的转换器,将请求中的数据转换成Java对象。
当服务器端需要将Java对象转换成HTTP响应时,同样可以使用MessageConverter来实现。比如,我们可以将Java对象转换成JSON格式的数据,然后将其作为HTTP响应返回给客户端。在这个过程中,MessageConverter会根据响应中的Content-Type头信息来自动选择相应的转换器,将Java对象转换成对应的格式。
Spring MVC中内置了许多实现,例如:
我们也可以自定义MessageConverter实现,以满足特定的需求。
接下来我们使用代码模拟一下消息转换器的工作过程:
//写入json
public static void test1() throws IOException {
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());
}
}
//写入xml
private static void test2() throws IOException {
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());
}
}
//读入json
private static void test3() throws IOException {
MockHttpInputMessage message = new MockHttpInputMessage("""
{
"name":"李四",
"age":20
}
""".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);
}
}
那么这么多MessageConverter是如何决定由谁来解析的呢?
相当于response.setContentType(“application/json”);
其次看 request 的 Accept 头有没有指定
最后按 MessageConverter 的顺序, 谁能谁先转换
@ControllerAdvice与@ResponseBodyAdvice、@RequestBodyAdvice可以配合,来实现对请求体/响应体的增强拓展,当然其本质上是对消息转换的拓展。
这里我们就以@ResponseBodyAdvice为例,ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置如下:
它的主要作用是:对@ResponseBody或ResponseEntity方法返回的内容进行后处理。
@ResponseBodyAdvice支持的后处理操作包括:
@ResponseBodyAdvice最常用的功能就是返回响应体前进行自定义的包装。其目的就是保持响应体的统一。
我们来看一个例子:
将{"name":"王五","age":18}
响应结果转换成 {"code":xx, "msg":xx, data: {"name":"王五","age":18} }
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
// 满足条件才转换
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
// returnType.getContainingClass().isAnnotationPresent(ResponseBody.class)) {
return true;
}
return false;
}
// 将 User 或其它类型统一为 Result 类型
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);
}
}
// @Controller
// @ResponseBody
@RestController
public static class MyController {
public User user() {
return new User("王五", 18);
}
}
}
注意:
这两者配套使用,就像RequestMappingHandlerMapping和RequestMappingHandlerAdapter,一个做路径映射,另一个做控制器方法调用。不过这一组更加的简单,他们属于SpringMVC更为早期的实现。
我们来看一下使用代码:
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
// /c1 --> /c1
// /c2 --> /c2
@Bean
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
@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;
};
}
}
代码解读:
/c1
,那么它也会去容器里面寻找一个名叫/c1
的bean。当然前提是这个bean的名字必须以/
开头,才会被当作处理请求的bean。接口Controller
(不是注解Controller)。接下来我们自己通过代码来实现一个:
@Configuration
public class WebConfig_1 {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
// /c1 --> /c1
// /c2 --> /c2
@Component
static class MyHandlerMapping implements HandlerMapping {
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
String key = request.getRequestURI();
Controller controller = collect.get(key);
if (controller == null) {
return null;
}
return new HandlerExecutionChain(controller);
}
@Autowired
private ApplicationContext context;
private Map<String, Controller> collect;
@PostConstruct
public void init() {
collect = context.getBeansOfType(Controller.class).entrySet()
.stream().filter(e -> e.getKey().startsWith("/"))
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
System.out.println(collect);
}
}
@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) {
controller.handleRequest(request, response);
}
return null;
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}
@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;
};
}
}
这一组较新,在Spring5.2才引入
使用代码:
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
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"), request -> ok().body("this is r1"));
}
@Bean
public RouterFunction<ServerResponse> r2() {
return route(GET("/r2"), request -> ok().body("this is r2"));
}
}
总结:
函数式控制器
a. RouterFunctionMapping, 通过 RequestPredicate 映射
b. handler 要实现 HandlerFunction 接口
c. HandlerFunctionAdapter, 调用 handler
对比
a. RequestMappingHandlerMapping, 以 @RequestMapping 作为映射路径
b. 控制器的具体方法会被当作 handler
c. RequestMappingHandlerAdapter, 调用 handler
函数式控制器就是在匹配方式上,调用方式上做了些改动。新是新但是功能并没有RequestMapping那套体系强大,毕竟对参数解析,返回值处理并没有那么多丰富的逻辑。好处就是十分的简洁,适合一些业务逻辑不复杂的场景。
又是一组,不过这一组一般是用来处理静态资源的。
使用代码:
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
Map<String, ResourceHttpRequestHandler> map
= context.getBeansOfType(ResourceHttpRequestHandler.class);
handlerMapping.setUrlMap(map);
return handlerMapping;
}
@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
return new HttpRequestHandlerAdapter();
}
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
return handler;
}
@Bean("/img/**")
public ResourceHttpRequestHandler handler2() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("images/")));
return handler;
}
这个handler其实还可以进行优化。我们来看看ResourceHttpRequestHandler 的初始化方法afterPropertiesSet(一位它实现了InitializingBean接口),在这个方法中我们可以看到他设置了一个资源解析器:
也就是说本质上他寻找那些静态资源是依靠的这个PathResourceResolver这个资源解析器。Spring内部还有其他几种资源解析器,可以增强它的功能。
增强代码:
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
handler.setResourceResolvers(List.of(
// ⬇️缓存优化
new CachingResourceResolver(new ConcurrentMapCache("cache1")),
// ⬇️压缩优化
new EncodedResourceResolver(),
// ⬇️原始资源解析
new PathResourceResolver()
));
return handler;
}
注意:
又叫欢迎页映射器,其作用是把访问根目录的请求映射到一个欢迎页上去。另外这个欢迎页映射器是SpringBoot提供的,传统的SpringMVC没有:
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
return new WelcomePageHandlerMapping(null, context, resource, "/**");
}
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
注意:
我们进入DispatcherServlet中遇到异常只是把异常捉住,赋值给一个Exception变量并没有马上处理,而是在接下来的processDispatchResult中去处理异常:
我们可以看到异常处理的核心方法是processHandlerException,我们继续进入这个方法,看看里面干了什么:
所有的异常处理器都继承了HandlerExceptionResolver接口,这里我们说一个其中比较重要的异常处理器ExceptionHandlerExceptionResolver。
我们来通过代码看看过程:
public class A30 {
public static void main(String[] args) throws NoSuchMethodException {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
resolver.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// 1.测试 json
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));
}
}
控制器:
接下来我们测试一下嵌套异常:
public class A30 {
public static void main(String[] args) throws NoSuchMethodException {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
resolver.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// 3.测试嵌套异常
HandlerMethod handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
Exception 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));
}
}
结果:
之所以可以可以进行嵌套循环是因为:
之后会拿这个异常的数组去进行匹配。
最后我们小总结一下:
关于ExceptionHandlerExceptionResolver:
@ControllerAdvice + @ExceptionHandler是一个全局异常处理方案
我们来看一段示例代码:
public class A31 {
public static void main(String[] args) throws NoSuchMethodException {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
// resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
// resolver.afterPropertiesSet();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ExceptionHandlerExceptionResolver resolver = context.getBean(ExceptionHandlerExceptionResolver.class);
HandlerMethod handlerMethod = new HandlerMethod(new Controller5(), Controller5.class.getMethod("foo"));
Exception e = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
static class Controller5 {
public void foo() {
}
}
}
那么我们的异常处理器是在什么时机找到的全局异常处理方法呢?
我们进入这个方法看看:
总结一下:
我们上面所涉及的异常处理其实能力是有限的,我们知道 @ExceptionHandler 只能处理发生在 mvc 流程中的异常,例如控制器内、拦截器内,那么如果是 Filter 出现了异常,如何进行处理呢?,这个时候我们需要一个更上层的异常处理者,也就是Tomcat异常处理。
我们可以看到Tomcat的错误页面是这样的:
那么我们能对其进行定制吗?
我们只需要加两个bean就可以完成这个需求:
@Bean // ⬅️修改了 Tomcat 服务器默认错误地址, 出错时使用请求转发方式跳转
public ErrorPageRegistrar errorPageRegistrar() {
return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}
@Bean // ⬅️TomcatServletWebServerFactory 初始化前用它增强, 注册所有 ErrorPageRegistrar
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
return new ErrorPageRegistrarBeanPostProcessor();
}
/error
也可以通过 ${server.error.path}
进行配置/error
这个地址
/error
,所以处理异常的职责就又回到了 SpringViewResolver 是一个接口,它的主要作用是:将逻辑视图名解析为实际的 View 对象。
在 Spring MVC 中,我们通常使用逻辑视图名来指定视图,例如:
return "user/list";
然后,ViewResolver 会根据这个逻辑视图名,解析出对应的 View 对象:
View view = viewResolver.resolveViewName("user/list", locale);
常见的 View 对象有:
Spring MVC 中默认配置了若干 ViewResolver,例如:
当我们返回一个逻辑视图名时,Spring MVC 会根据注册的 ViewResolver 解析成实际的 View 对象,然后由视图渲染工具(ViewRenderer)来渲染视图,生成客户端响应。
所以,ViewResolver 的主要特征是:
除此之外,ViewResolver 还有如下优点:
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
/
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器
DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
例如根据 /hello 路径找到 @RequestMapping(“/hello”) 对应的控制器方法
控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
DispatcherServlet 接下来会: