第二十七讲-常见的返回值处理器

第二十七讲-常见的返回值处理器

本讲我们来了解一下常见的返回值处理器的作用

1. 返回值类型为ModelAndView

我们首先编写一段控制器代码:

static class Controller {
    private static final Logger log = LoggerFactory.getLogger(Controller.class);

    // 返回值类型为ModelAndView
    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
//        @RequestMapping("/test3")
    // 返回值类型为自定义User对象
    public User test3() {
        log.debug("test3()");
        return new User("李四", 20);
    }

    // 返回值类型为自定义对象,省略了(@ModelAttribute注解)
    public User test4() {
        log.debug("test4()");
        return new User("王五", 30);
    }

    // 返回值类型为HttpEntity类型,包括整个响应,如响应状态码,响应头,响应体(响应数据)
    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;
    }

    // 返回值类型为响应结果(一般为JSON序列化)
    @ResponseBody
    public User test7() {
        log.debug("test7()");
        return new User("钱七", 50);
    }
}

// 必须用 public 修饰, 否则 freemarker 渲染其 name, age 属性时失败
public static class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
               "name='" + name + '\'' +
               ", age=" + age +
               '}';
    }
}

针对上面的7种情况,我们利用不同的返回值处理器来演示它们的处理过程!

我们准备一下返回值处理器:

public static HandlerMethodReturnValueHandlerComposite getReturnValueHandler() {
    HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
    composite.addHandler(new ModelAndViewMethodReturnValueHandler());
    composite.addHandler(new ViewNameMethodReturnValueHandler());
    composite.addHandler(new ServletModelAttributeMethodProcessor(false));
    composite.addHandler(new HttpEntityMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
    composite.addHandler(new HttpHeadersReturnValueHandler());
    composite.addHandler(new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
    composite.addHandler(new ServletModelAttributeMethodProcessor(true));
    return composite;
}

我们前面提及过,在SpringMVC中,最终的结果会封装在一个ModelAndView对象中,在得到ModelAndView对象之后,会进行视图渲染,我们也会进一步了解一下SpringMVC的视图渲染过程(虽然现在处于前后端分离开发模式,视图渲染已经交给了前端进行处理,但是SpringMVC框架发展至今日,仍然保留了视图渲染机制,因此呢,我们可以了解一下SpringMVC的视图渲染机制),这里呢,我们使用FreeMarker技术继续视图渲染,我们编写一下有关FreeMarker相关的配置:

@Configuration
public class WebConfig {

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setDefaultEncoding("utf-8");
        configurer.setTemplateLoaderPath("classpath:templates");
        return configurer;
    }

    @Bean // FreeMarkerView 在借助 Spring 初始化时,会要求 web 环境才会走 setConfiguration, 这里想办法去掉了 web 环境的约束
    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;
    }
}

接下来我们看一下test1方法,该方法的返回值类型为ModelAndView类型:

public ModelAndView test1() {
    log.debug("test1()");
    ModelAndView mav = new ModelAndView("view1");
    mav.addObject("name", "张三");
    return mav;
}

对于返回值类型为ModelAndView的控制器,其对应的返回值处理器为ModelAndViewMethodReturnValueHandler

我们来编写相关测试代码:

private static void test1(AnnotationConfigApplicationContext context) throws Exception {
    // 创建方法对象
    Method method = Controller.class.getMethod("test1");
    // 创建控制器对象
    Controller controller = new Controller();
    // 反射调用该控制器的该方法,并获取该方法执行后的结果
    Object returnValue = method.invoke(controller); // 获取返回值
    // 创建一个方法处理器
    HandlerMethod methodHandle = new HandlerMethod(controller, method);
    // 存储暂存中间数据的容器
    ModelAndViewContainer container = new ModelAndViewContainer();
    // 测试返回值类型为ModeAndView
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    // 测试请求
    ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
    // 检查是否支持此类型的返回值
    if (composite.supportsReturnType(methodHandle.getReturnType())) {
        // 处理ModelAndView类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        // 检查容器中视图名称和数据是否被正确填充
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        // 渲染视图
        renderView(context, container, webRequest);
    }
}

渲染视图的方法:

// 渲染视图
private static void renderView(AnnotationConfigApplicationContext 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));
}

在resources目录下建立一个templates目录,并创建一个视图文件view1.ftl

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>view1</title>
</head>
<body>
    <h1>Hello! ${name}</h1>	<!-- 获取模型数据值 -->
</body>
</html>

编写主方法测试一下:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    test1(context);
}

测试如下:

18:59:02.717 [main] DEBUG com.cherry.a27.A27$Controller - test1()
{name=张三}
view1
18:59:03.095 [main] DEBUG com.cherry.a27.A27 - >>>>>> 渲染视图
18:59:03.096 [main] DEBUG com.cherry.a27.A27 - 没有获取到视图名, 采用默认视图名: view1
18:59:03.170 [main] DEBUG com.cherry.a27.WebConfig$1$1 - View name 'view1', model {name=张三}
18:59:03.175 [main] DEBUG com.cherry.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>

我们发现,视图和数据都被成功渲染!

2. 返回值类型为字符串(仅仅返回视图)

接下来我们看一下返回值类型为字符串的情形

// 返回值类型为字符串类型
public String test2() {
    log.debug("test2()");
    return "view2";
}

这个控制器相比于上一个相比更简单,只是单纯的返回一个视图名, 当然,解析这个字符串返回值的操作是由ViewMethodReturnValueHandler这个处理器来完成返回值处理的(把返回值当作视图名进行解析)。

private static void test2(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test2");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值
	
    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        renderView(context, container, webRequest); // 渲染视图
    }
}

在templates目录下建立一个view2.ftl文件:

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>view2</title>
</head>
<body>
    <h1>Hello!</h1>
</body>
</html>

编写主方法测试:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    test2(context);
}
{}
view2
19:07:17.490 [main] DEBUG com.cherry.a27.A27 - >>>>>> 渲染视图
19:07:17.491 [main] DEBUG com.cherry.a27.A27 - 没有获取到视图名, 采用默认视图名: view2
19:07:17.568 [main] DEBUG com.cherry.a27.WebConfig$1$1 - View name 'view2', model {}
19:07:17.581 [main] DEBUG com.cherry.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>

OK!不解释。

3. 返回值类型为@ModelAttribute

@ModelAttribute注解标注在方法上表示将来方法的返回值会作为模型的数据将来添加到ModelAndViewContainer中去。

@ModelAttribute
//        @RequestMapping("/test3")
// 返回值类型为自定义User对象
public User test3() {
    log.debug("test3()");
    return new User("李四", 20);
}

关于@ModelAttribute对应的返回值处理器为ServletModelAttributeMethodProcessor。我们编写相关测试代码:

private static void test3(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test3");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    // 设置请求URI
    request.setRequestURI("/test3");
    // 将/test3存入resuqets scope,将来处理时如果没有视图名就会将/test3作为默认的视图名
    UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
    ServletWebRequest webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        renderView(context, container, webRequest); // 渲染视图
    }
}

在templates建立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>

编写主方法测试

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    test3(context);
}
19:22:56.564 [main] DEBUG com.cherry.a27.A27$Controller - test3()
{user=User{name='李四', age=20}}
null
19:22:57.146 [main] DEBUG com.cherry.a27.A27 - >>>>>> 渲染视图
19:22:57.149 [main] DEBUG com.cherry.a27.A27 - 没有获取到视图名, 采用默认视图名: test3,默认的视图名为路径名
19:22:57.247 [main] DEBUG com.cherry.a27.WebConfig$1$1 - View name 'test3', model {user=User{name='李四', age=20}}
19:22:57.255 [main] DEBUG com.cherry.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>

4. 返回值类型为ModelAttribute(不加@ModelAttribute)

这种返回值类型分析和上面的很相似,区别就是把@ModelAttribute注解省略了,仅此而已。这里不做解释,只做演示:

public User test4() {
    log.debug("test4(	)");
    return new User("王五", 30);
}
private static void test4(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test4");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setRequestURI("/test4");
    UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
    ServletWebRequest webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        renderView(context, container, webRequest); // 渲染视图
    }
}

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>
public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    test4(context);
}
19:30:48.399 [main] DEBUG com.cherry.a27.A27$Controller - test4()
{user=User{name='王五', age=30}}
null
19:30:48.827 [main] DEBUG com.cherry.a27.A27 - >>>>>> 渲染视图
19:30:48.830 [main] DEBUG com.cherry.a27.A27 - 没有获取到视图名, 采用默认视图名: test4
19:30:48.913 [main] DEBUG com.cherry.a27.WebConfig$1$1 - View name 'test4', model {user=User{name='王五', age=30}}
19:30:48.922 [main] DEBUG com.cherry.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>

5. 返回值类型为ResponseEntity

// 返回值类型为HttpEntity类型,包括整个响应,如响应状态码,响应头,响应体(响应数据)
public HttpEntity<User> test5() {
    log.debug("test5()");
    return new HttpEntity<>(new User("赵六", 40));
}

返回值类型为ResponseEntity所对应的返回值处理器为ResponseBodyEmitterReturnValueHandler来完成处理工作。

编写相关测试代码:

private static void test5(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test5");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        if (!container.isRequestHandled()) {	// 请求如果已经被处理,就无需渲染视图了,直接走响应即可
            renderView(context, container, webRequest); // 渲染视图
        } else {
            System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
        }
    }
}

主方法测试:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    test5(context);
}
19:47:58.195 [main] DEBUG com.cherry.a27.A27$Controller - test5()
19:47:58.712 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json]
19:47:58.725 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor - Writing [User{name='赵六', age=40}]
{}
null
{"name":"赵六","age":40}

我们发现,结果转为了JSON

6. 返回值类型为响应头

// 返回值类型为响应头
public HttpHeaders test6() {
    log.debug("test6()");
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "text/html");
    return headers;
}

返回值类型为响应头对应的返回值处理器为HttpHeadersReturnValueHandler,该处理器返回的结果只包含响应头和状态码,不包括响应体了。

如下面的代码:

private static void test6(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test6");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        if (!container.isRequestHandled()) {
            renderView(context, container, webRequest); // 渲染视图
        } else {
            // 获取全部响应头名字和对应的值
            for (String name : response.getHeaderNames()) {
                System.out.println(name + "=" + response.getHeader(name));
            }
            System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
        }
    }
}
public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    test6(context);
}
19:52:41.437 [main] DEBUG com.cherry.a27.A27$Controller - test6()
{}
null
Content-Type=text/html

我们发现响应的结果只包含响应头,响应体为空。

7. 返回值类型为添加了@ResponseBody注解

@ResponseBody注解标注的方法是由RequestResponseBodyMethodProcessor处理器解析处理返回值的。该处理器也是将返回值当作响应体,然后经过消息转换器将数据转为JSON。

我们这次测试一下:

@ResponseBody
public User test7() {
    log.debug("test7()");
    return new User("钱七", 50);
}
private static void test7(AnnotationConfigApplicationContext context) throws Exception {
        Method method = Controller.class.getMethod("test7");
        Controller controller = new Controller();
        Object returnValue = method.invoke(controller); // 获取返回值

        HandlerMethod methodHandle = new HandlerMethod(controller, method);

        ModelAndViewContainer container = new ModelAndViewContainer();
        HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
            composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
            System.out.println(container.getModel());
            System.out.println(container.getViewName());
            if (!container.isRequestHandled()) {
                renderView(context, container, webRequest); // 渲染视图
            } else {
                for (String name : response.getHeaderNames()) {
                    System.out.println(name + "=" + response.getHeader(name));
                }
                System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
            }
        }
    }
}
public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    test7(context);
}
19:59:37.895 [main] DEBUG com.cherry.a27.A27$Controller - test7()
19:59:38.296 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Using 'application/json', given [*/*] and supported [application/json, application/*+json]
19:59:38.311 [main] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Writing [User{name='钱七', age=50}]
{}
null
Content-Type=application/json
{"name":"钱七","age":50}
posted @   LilyFlower  阅读(4)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示