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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家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());
  }
}
相关文章
|
4月前
|
JavaScript 前端开发 开发者
哇塞!Vue.js 与 Web Components 携手,掀起前端组件复用风暴,震撼你的开发世界!
【8月更文挑战第30天】这段内容介绍了Vue.js和Web Components在前端开发中的优势及二者结合的可能性。Vue.js提供高效简洁的组件化开发,单个组件包含模板、脚本和样式,方便构建复杂用户界面。Web Components作为新兴技术标准,利用自定义元素、Shadow DOM等技术创建封装性强的自定义HTML元素,实现跨框架复用。结合二者,不仅增强了Web Components的逻辑和交互功能,还实现了Vue.js组件在不同框架中的复用,提高了开发效率和可维护性。未来前端开发中,这种结合将大有可为。
167 0
|
4月前
|
Web App开发 JavaScript 前端开发
[译] 用 Web Worker 改善 Vue 组件性能
[译] 用 Web Worker 改善 Vue 组件性能
|
2月前
|
前端开发 JavaScript 开发者
Web组件:一种新的前端开发范式
【10月更文挑战第9天】Web组件:一种新的前端开发范式
64 2
|
2月前
|
前端开发 JavaScript Go
前端开发趋势:从响应式设计到Web组件的探索
【10月更文挑战第1天】前端开发趋势:从响应式设计到Web组件的探索
43 3
|
1月前
|
消息中间件 监控 Kafka
Apache Kafka 成为处理实时数据流的关键组件。Kafka Manager 提供了一个简洁的 Web 界面
随着大数据技术的发展,Apache Kafka 成为处理实时数据流的关键组件。Kafka Manager 提供了一个简洁的 Web 界面,方便管理和监控 Kafka 集群。本文详细介绍了 Kafka Manager 的部署步骤和基本使用方法,包括配置文件的修改、启动命令、API 示例代码等,帮助你快速上手并有效管理 Kafka 集群。
47 0
|
3月前
|
前端开发 安全 Java
技术进阶:使用Spring MVC构建适应未来的响应式Web应用
【9月更文挑战第2天】随着移动设备的普及,响应式设计至关重要。Spring MVC作为强大的Java Web框架,助力开发者创建适应多屏的应用。本文推荐使用Thymeleaf整合视图,通过简洁的HTML代码提高前端灵活性;采用`@ResponseBody`与`Callable`实现异步处理,优化应用响应速度;运用`@ControllerAdvice`统一异常管理,保持代码整洁;借助Jackson简化JSON处理;利用Spring Security增强安全性;并强调测试的重要性。遵循这些实践,将大幅提升开发效率和应用质量。
72 7
|
3月前
|
负载均衡 网络协议 应用服务中间件
web群集--rocky9.2源码部署nginx1.24的详细过程
Nginx 是一款由 Igor Sysoev 开发的开源高性能 HTTP 服务器和反向代理服务器,自 2004 年发布以来,以其高效、稳定和灵活的特点迅速成为许多网站和应用的首选。本文详细介绍了 Nginx 的核心概念、工作原理及常见使用场景,涵盖高并发处理、反向代理、负载均衡、低内存占用等特点,并提供了安装配置教程,适合开发者参考学习。
|
4月前
|
Java 开发者 前端开发
Struts 2、Spring MVC、Play Framework 上演巅峰之战,Web 开发的未来何去何从?
【8月更文挑战第31天】在Web应用开发中,Struts 2框架因强大功能和灵活配置备受青睐,但开发者常遇配置错误、类型转换失败、标签属性设置不当及异常处理等问题。本文通过实例解析常见难题与解决方案,如配置文件中遗漏`result`元素致页面跳转失败、日期格式不匹配需自定义转换器、`&lt;s:checkbox&gt;`标签缺少`label`属性致显示不全及Action中未捕获异常影响用户体验等,助您有效应对挑战。
96 0
|
4月前
|
Java 前端开发 Apache
Apache Wicket与Spring MVC等Java Web框架大PK,究竟谁才是你的最佳拍档?点击揭秘!
【8月更文挑战第31天】在Java Web开发领域,众多框架各具特色。Apache Wicket以组件化开发和易用性脱颖而出,提高了代码的可维护性和可读性。相比之下,Spring MVC拥有强大的生态系统,但学习曲线较陡;JSF与Java EE紧密集成,但在性能和灵活性上略逊一筹;Struts2虽成熟,但在RESTful API支持上不足。选择框架时还需考虑社区支持和文档完善程度。希望本文能帮助开发者找到最适合自己的框架。
53 0
|
2月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
159 3
下一篇
DataWorks