【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler (上)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler (上)

前言


Spring MVC处理入参靠的是HandlerMethodArgumentResolver这个接口,解析返回值靠的是HandlerMethodReturnValueHandler这个策略接口。


Spring MVC支持非常非常多的返回值类型,然后针对不同的返回值类型:比如Map、比如ViewName、比如Callable、比如异步的StreamingResponseBody等等都有其对应的处理器做处理,而它的顶层抽象为:HandlerMethodReturnValueHandler这个策略接口


知道了它的处理原理后,若我们有特殊的需要,我们自定义我们自己的返回值处理器,来自定义属于自己的返回值类型~~~~


还有比如我们需要对controller返回值前后做处理的情况,都可以在返回值上统一做手脚(比如加上接口执行耗时之类的~~~)


HandlerMethodReturnValueHandler


这个接口的命名有点怪:处理函数返回值的处理器?其实它就是一个处理Controller返回值的接口

// @since 3.1  出现得相对还是比较晚的。因为`RequestMappingHandlerAdapter`也是这个时候才出来
// Strategy interface to handle the value returned from the invocation of a handler method
public interface HandlerMethodReturnValueHandler {
  // 每种处理器实现类,都对应着它能够处理的返回值类型~~~
  boolean supportsReturnType(MethodParameter returnType);
  // Handle the given return value by adding attributes to the model and setting a view or setting the
  // {@link ModelAndViewContainer#setRequestHandled} flag to {@code true} to indicate the response has been handled directly.
  // 简单的说就是处理返回值,可以处理着向Model里设置一个view
  // 或者ModelAndViewContainer#setRequestHandled设置true说我已经直接处理了,后续不要需要再继续渲染了~
  void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}


由此可以看出,它采用的又是一种责任链的设计模式。

因为SpringMVC支持的返回值类型众多,而我们绝大部分情况下是无需自己自定义返回值处理器的,因此下面我们可以看看它的继承树:

直接实现类众多,同时也有交给用户扩展的优先级子接口,后面会举例。

下面从上至下:


MapMethodProcessor

它相对来说比较特殊,既处理Map类型的入参,也处理Map类型的返回值(本文只关注返回值处理器部分)


// @since 3.1
public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return Map.class.isAssignableFrom(parameter.getParameterType());
  }
  @Override
  @Nullable
  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
    return mavContainer.getModel();
  }
  // ==================上面是处理入参的,不是本文的重点~~~====================
  // 显然只有当你的返回值是个Map时候,此处理器才会生效
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return Map.class.isAssignableFrom(returnType.getParameterType());
  }
  @Override
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    // 做的事非常简单,仅仅只是把我们的Map放进Model里面~~~
    // 但是此处需要注意的是:它并没有setViewName,所以它此时是没有视图名称的~~~
    // ModelAndViewContainer#setRequestHandled(true) 所以后续若还有处理器可以继续处理
    if (returnValue instanceof Map){
      mavContainer.addAllAttributes((Map) returnValue);
    }
  }
}


这里有必要特别注意一下JavaDoc部分的说明:


 * <p>A Map return value can be interpreted in more than one ways depending
 * on the presence of annotations like {@code @ModelAttribute} or
 * {@code @ResponseBody}. Therefore this handler should be configured after
 * the handlers that support these annotations.

直译为:它可以被解释为多种途径,依赖于Handler上面的额注解,如:@ModelAttribute@ResponseBody这种注解。所以请务必使它放在这些处理器的后面,最后执行~~~


若我们这么使用,什么注解都不标注,就会出问题了,如下:


    @GetMapping(value = "/hello")
    public Map helloGet() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "fsx");
        map.put("age", 18);
        return map;
    }


因为返回是Map类型,最终肯定会进入到MapMethodProcessor来处理返回值。但是因为没有setViewName,so访问的结果如下:


image.png


原因就是因为你没有viewName,SpringMVC采用了默认的获取viewName的机制(还是hello),所以进行转发的时候发现是相同的进入死循环,就抛错了~~~


image.png


走了一个默认获取viewName的机制。因此如果Handler上没有相关注解,直接使用是不妥的~


针对于此提供一种方案,来解决这种情况:既能定位到view,又能把Map放在Model里处理~~~


可否曾想过,Spring MVC提供给你一些处理器,你却不能直接使用???什么情况???

其实还是真的,Spring MVC提供给我们的只是个半成品,真正要想有好的效果,你还得自己加工,后面还会介绍好几个这样子的“半成品”~


下面就以MapMethodProcessor为例,在返回值上让它成为一个有用的东西:

// 对半成品`MapMethodProcessor`进行扩展,指向指定的视图即可~~~~
public class MyMapProcessor extends MapMethodProcessor {
    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        super.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        // 设置一个视图 方便渲染~
        mavContainer.setViewName("world");
    }
}
// 把自定义的返回值处理器,添加进Spring MVC里(实际上是`RequestMappingHandlerAdapter`里)
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter implements InitializingBean {
    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
    @Override
    public void afterPropertiesSet() throws Exception {
        // 注意这里返回的是一个只读的视图  所以并不能直接操作里面的数据~~~~~
        List<HandlerMethodReturnValueHandler> returnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> result = new ArrayList<>();
        returnValueHandlers.forEach(r -> {
            // 换成我们自己的~~~~~~~
            if (r instanceof MapMethodProcessor) {
                result.add(new MyMapProcessor());
            } else {
                result.add(r);
            }
        });
        requestMappingHandlerAdapter.setReturnValueHandlers(result);
    }
    // ============这样只会在原来的15后面再添加一个,并不能起到联合的作用  所以只能使用上面的对原始类继承的方式~~~============
    //@Override
    //public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
    //    returnValueHandlers.add(new MyMapProcessor());
    //}
}
// 运行Controller就再也不会出现上面的报错了,而是能正常到world这个页面里面去~~~~
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public Map<String, Object> helloGet() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "fsx");
        map.put("age", 18);
        return map;
    }


其实还有另外一种方法就是我们自己配置一个视图解析器。比如指定前缀为WEB-INF/pages,后缀为.jsp,那么这个/hello请求最终自动会找到WEB-INF/pages/hello.jsp页面的`,相当于restful的url也能形成一种契约~~~


至于为何我们自定义的MyMapProcessor能够生效,因为我们已经实现了偷天换日~~~:

image.png


下面继续说到的一些“半成品”,各位若有兴趣,也可以自己配置一个视图解析器来处理(其实Spring MVC推荐是这么做的~,充分利用URL这种资源定位符)


ViewNameMethodReturnValueHandler


从名字可以看出它处理的是ViewName,所以大概率处理的都是字符串类型的返回值~~~


处理返回值类型是void和String类型的。从Spring4.2之后,支持到了CharSequence类型。比如我们常见的String、StringBuffer、StringBuilder等都是没有问题的~


// 可以直接返回一个视图名,最终会交给`RequestToViewNameTranslator`翻译一下~~~
public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
  // Spring4.1之后支持自定义重定向的匹配规则
  // Spring4.1之前还只能支持redirect:这个固定的前缀~~~~
  private String[] redirectPatterns;
  public void setRedirectPatterns(String... redirectPatterns) {
    this.redirectPatterns = redirectPatterns;
  }
  public String[] getRedirectPatterns() {
    return this.redirectPatterns;
  }
  protected boolean isRedirectViewName(String viewName) {
    return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
  }
  // 支持void和CharSequence类型(子类型)
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    Class<?> paramType = returnType.getParameterType();
    return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
  }
  // 注意:若返回值是void,此方法都不会进来
  @Override
  public void handleReturnValue(Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    // 显然如果是void,returnValue 为null,不会走这里。
    // 也就是说viewName设置不了,所以会出现和上诉一样的循环报错~~~~~ 因此不要单独使用
    if (returnValue instanceof CharSequence) {
      String viewName = returnValue.toString();
      mavContainer.setViewName(viewName);
      // 做一个处理:如果是重定向的view,那就
      if (isRedirectViewName(viewName)) {
        mavContainer.setRedirectModelScenario(true);
      }
    }
    // 下面这都是不可达的~~~~~setRedirectModelScenario(true)标记一下
    // 此处不仅仅是else,而是还有个!=null的判断  
    // 那是因为如果是void的话这里返回值是null,属于正常的~~~~ 只是什么都不做而已~(viewName也没有设置哦~~~)
    else if (returnValue != null) {
      // should not happen
      throw new UnsupportedOperationException("Unexpected return type: " +
          returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
    }
  }
}

若handler直接这么使用:


    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public void helloGet() {
        System.out.println("hello controller");
    }


浏览器获得的效果同上(Circular view path [hello]),所以不要单独使用。如果是返回字符串:它就自己回去找视图了:


    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String helloGet() {
        //return "hello"; // 若直接写hello,那不又到这个controller了吗,导致 Circular view path [hello]
        return "world";
    }


handler所有的方法,都不建议返回void,不管是页面还是json串~


ViewMethodReturnValueHandler


这个和上面非常类似,但是它的返回值不是一个字符串,而是一个View.class(它的实现类有很多,比如MappingJackson2JsonView、AbstractPdfView、MarshallingView、RedirectView、JstlView等等非常多的视图类型),进而渲染出一个视图给用户看。

// javadoc上有说明:此处理器需要配置在支持`@ModelAttribute`或者`@ResponseBody`的处理器们前面。防止它被取代~~~~
public class ViewMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
  // 处理素有的View.class类型
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return View.class.isAssignableFrom(returnType.getParameterType());
  }
  @Override
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    // 这个处理逻辑几乎完全同上
    // 最终也是为了mavContainer.setView(view);
    // 也会对重定向视图进行特殊的处理~~~~~~
    if (returnValue instanceof View) {
      View view = (View) returnValue;
      mavContainer.setView(view);
      if (view instanceof SmartView && ((SmartView) view).isRedirectView()) {
        mavContainer.setRedirectModelScenario(true);
      }
    }
    else if (returnValue != null) {
      // should not happen
      throw new UnsupportedOperationException("Unexpected return type: " +
          returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
    }
  }
}


Spring MVC认为后台产生的一份数据,可以是N多种方式来进行展示的。比如可议是Html页面、可议是word、可以是PDF、当然也可以是charts统计表格(比如我们古老的技术:JFrameChart就是生产报表的一把好手)~~


HttpHeadersReturnValueHandler

这个处理器非常有意思,它只处理请求头HttpHeaders。先效果~~~

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public HttpHeaders helloGet() {
        HttpHeaders httpHeaders = new HttpHeaders();
        // 这两个效果相同
        httpHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
        //httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
        httpHeaders.add("name", "fsx");
        return httpHeaders;
    }


请求一下,浏览器没有任何内容。但是控制台里可以看到如下:


image.png



这个处理器可以帮助我们在需要对请求头进行特殊处理的时候,进行一定程度的加工。它Spring4.0后才有


public class HttpHeadersReturnValueHandler implements HandlerMethodReturnValueHandler {
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return HttpHeaders.class.isAssignableFrom(returnType.getParameterType());
  }
  @Override
  @SuppressWarnings("resource")
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    // 请注意这里:已经标记该请求已经被处理过了~~~~~
    mavContainer.setRequestHandled(true);
    Assert.state(returnValue instanceof HttpHeaders, "HttpHeaders expected");
    HttpHeaders headers = (HttpHeaders) returnValue;
    // 返回值里自定义返回的响应头。这里会帮你设置到HttpServletResponse 里面去的~~~~
    if (!headers.isEmpty()) {
      HttpServletResponse servletResponse = webRequest.getNativeResponse(HttpServletResponse.class);
      Assert.state(servletResponse != null, "No HttpServletResponse");
      ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(servletResponse);
      outputMessage.getHeaders().putAll(headers);
      outputMessage.getBody();  // flush headers
    }
  }
}


ModelMethodProcessor

和MapMethodProcessor几乎一模一样。它处理org.springframework.ui.Model类型,处理方式几乎同Map方式一样(因为Model的结构和Map一样也是键值对)


ModelAndViewMethodReturnValueHandler


专门处理返回值类型是ModelAndView类型的。


ModelAndView = model + view + HttpStatus

public class ModelAndViewMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
  // Spring4.1后一样  增加自定义重定向前缀的支持
  @Nullable
  private String[] redirectPatterns;
  public void setRedirectPatterns(@Nullable String... redirectPatterns) {
    this.redirectPatterns = redirectPatterns;
  }
  @Nullable
  public String[] getRedirectPatterns() {
    return this.redirectPatterns;
  }
  protected boolean isRedirectViewName(String viewName) {
    return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
  }
  // 显然它只处理ModelAndView这种类型~
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return ModelAndView.class.isAssignableFrom(returnType.getParameterType());
  }
  @Override
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    // 如果调用者返回null 那就标注此请求被处理过了~~~~ 不需要再渲染了
    // 浏览器的效果就是:一片空白
    if (returnValue == null) {
      mavContainer.setRequestHandled(true);
      return;
    }
    ModelAndView mav = (ModelAndView) returnValue;
    // isReference()方法为:(this.view instanceof String)
    // 这里专门处理视图就是一个字符串的情况,else是处理视图是个View对象的情况
    if (mav.isReference()) {
      String viewName = mav.getViewName();
      mavContainer.setViewName(viewName);
      if (viewName != null && isRedirectViewName(viewName)) {
        mavContainer.setRedirectModelScenario(true);
      }
    }
    // 处理view  顺便处理重定向
    else {
      View view = mav.getView();
      mavContainer.setView(view);
      // 此处所有的view,只有RedirectView的isRedirectView()才是返回true,其它都是false
      if (view instanceof SmartView && ((SmartView) view).isRedirectView()) {
        mavContainer.setRedirectModelScenario(true);
      }
    }
    // 把status和model都设置进去
    mavContainer.setStatus(mav.getStatus());
    mavContainer.addAllAttributes(mav.getModel());
  }
}
相关文章
|
11天前
|
JSON API 数据库
从零到英雄?一篇文章带你搞定Python Web开发中的RESTful API实现!
在Python的Web开发领域中,RESTful API是核心技能之一。本教程将从零开始,通过实战案例教你如何使用Flask框架搭建RESTful API。首先确保已安装Python和Flask,接着通过创建一个简单的用户管理系统,逐步实现用户信息的增删改查(CRUD)操作。我们将定义路由并处理HTTP请求,最终构建出功能完整的Web服务。无论是初学者还是有经验的开发者,都能从中受益,迈出成为Web开发高手的重要一步。
33 4
|
5月前
|
前端开发 Java 测试技术
Java一分钟之Spring MVC:构建Web应用
【5月更文挑战第15天】Spring MVC是Spring框架的Web应用模块,基于MVC模式实现业务、数据和UI解耦。常见问题包括:配置DispatcherServlet、Controller映射错误、视图解析未设置、Model数据传递遗漏、异常处理未配置、依赖注入缺失和忽视单元测试。解决这些问题可提升代码质量和应用性能。注意配置`web.xml`、`@RequestMapping`、`ViewResolver`、`Model`、`@ExceptionHandler`、`@Autowired`,并编写测试用例。
328 3
|
1月前
|
前端开发 安全 Java
技术进阶:使用Spring MVC构建适应未来的响应式Web应用
【9月更文挑战第2天】随着移动设备的普及,响应式设计至关重要。Spring MVC作为强大的Java Web框架,助力开发者创建适应多屏的应用。本文推荐使用Thymeleaf整合视图,通过简洁的HTML代码提高前端灵活性;采用`@ResponseBody`与`Callable`实现异步处理,优化应用响应速度;运用`@ControllerAdvice`统一异常管理,保持代码整洁;借助Jackson简化JSON处理;利用Spring Security增强安全性;并强调测试的重要性。遵循这些实践,将大幅提升开发效率和应用质量。
54 7
|
2月前
|
前端开发 Java Spring
SpringMVC种通过追踪源码查看是哪种类型的视图渲染器(一般流程方法)
这篇文章通过示例代码展示了如何在Spring MVC中编写和注册拦截器,以及如何在拦截器的不同阶段添加业务逻辑。
SpringMVC种通过追踪源码查看是哪种类型的视图渲染器(一般流程方法)
|
2月前
|
存储 移动开发 前端开发
HTML5时代来临,这些新特性你掌握了吗?一篇文章带你玩转Web前端技术潮流!
【8月更文挑战第26天】HTML5(简称H5)作为新一代Web标准,相比HTML4带来了诸多增强功能。
43 2
|
2月前
|
Java 开发者 前端开发
Struts 2、Spring MVC、Play Framework 上演巅峰之战,Web 开发的未来何去何从?
【8月更文挑战第31天】在Web应用开发中,Struts 2框架因强大功能和灵活配置备受青睐,但开发者常遇配置错误、类型转换失败、标签属性设置不当及异常处理等问题。本文通过实例解析常见难题与解决方案,如配置文件中遗漏`result`元素致页面跳转失败、日期格式不匹配需自定义转换器、`&lt;s:checkbox&gt;`标签缺少`label`属性致显示不全及Action中未捕获异常影响用户体验等,助您有效应对挑战。
67 0
|
2月前
|
Java 前端开发 Apache
Apache Wicket与Spring MVC等Java Web框架大PK,究竟谁才是你的最佳拍档?点击揭秘!
【8月更文挑战第31天】在Java Web开发领域,众多框架各具特色。Apache Wicket以组件化开发和易用性脱颖而出,提高了代码的可维护性和可读性。相比之下,Spring MVC拥有强大的生态系统,但学习曲线较陡;JSF与Java EE紧密集成,但在性能和灵活性上略逊一筹;Struts2虽成熟,但在RESTful API支持上不足。选择框架时还需考虑社区支持和文档完善程度。希望本文能帮助开发者找到最适合自己的框架。
31 0
|
4月前
|
安全 前端开发 Java
挑战5分钟内基于Springboot+SpringMVC+Mybatis-plus快速构建web后端三层架构
挑战5分钟内基于Springboot+SpringMVC+Mybatis-plus快速构建web后端三层架构
45 1
|
5月前
|
JavaScript Java 测试技术
基于springboot+vue.js的基于Web教师个人成果管理系统附带文章和源代码设计说明文档ppt
基于springboot+vue.js的基于Web教师个人成果管理系统附带文章和源代码设计说明文档ppt
50 7
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的高校疫情防控web系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的高校疫情防控web系统附带文章源码部署视频讲解等
31 0
下一篇
无影云桌面