前言
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访问的结果如下:
原因就是因为你没有viewName,SpringMVC采用了默认的获取viewName的机制(还是hello),所以进行转发的时候发现是相同的进入死循环,就抛错了~~~
走了一个默认获取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能够生效,因为我们已经实现了偷天换日~~~:
下面继续说到的一些“半成品”,各位若有兴趣,也可以自己配置一个视图解析器来处理(其实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; }
请求一下,浏览器没有任何内容。但是控制台里可以看到如下:
这个处理器可以帮助我们在需要对请求头进行特殊处理的时候,进行一定程度的加工。它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()); } }