SpringMvc 如何同时支持 Jsp 和 Json 接口?
后端同学基本都会见过这种场景:在同一个工程中,有些页面使用jsp模版渲染,同时还有其他接口提供Json格式的返回值。为了同时支持这两种场景,我们一般是如何处理的呢?
其实非常简单:
1、在项目中为 SpringMvc 指定视图解析器 ViewResolver,并引入 jstl 和 apache-jsp 依赖,用于支持jsp页面的渲染。
2、在需要返回 Json 数据的方法上追加注解 @ResponseBody,并且配置对应的 Json 消息转换器。此时将不会使用指定的 ViewResolver 渲染页面,而是返回 Json 数据。
简单演示下:
1、配置Jsp视图解析器:
@Configuration
@AutoConfigureOrder
@AutoConfigureAfter({WebMvcAutoConfiguration.class})
public class SpringMvcConfig implements WebMvcConfigurer {
    /**
     * jsp视图解析
     *
     * @return
     */
    @Bean
    public ViewResolver getViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/view/");
        resolver.setSuffix(".jsp");
        resolver.setViewClass(JstlView.class);
        return resolver;
    }
    /**
     * json消息转换器
     *
     * @param converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        final MappingJackson2HttpMessageConverter jackson = new MappingJackson2HttpMessageConverter(objectMapper);
        jackson.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8));
        converters.add(0, new StringHttpMessageConverter(Charsets.UTF_8));
        converters.add(1, jackson);
    }
    ...
}配置好jsp解析依赖的包:
        <!-- JSP视图解析 -->
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <!-- 必须引入该依赖,解析JSP -->
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>apache-jsp</artifactId>
            <version>9.4.8.v20171121</version>
        </dependency>2、两种接口。一个返回Json数据,一个渲染Jsp页面:
@Controller
@Slf4j
public class MyController {
    /**
     * 这个接口将会返回json数据
     * 必须配置 @ResponseBody 注解
     */
    @GetMapping("/toJson")
    @ResponseBody
    public Response toJson() {
        Response response = new Response();
        response.setCode(200);
        response.setMsg("");
        response.setData("Json数据");
        return response;
    }
    /**
     * 这个接口将会渲染对应的jsp页面。
     * 注:需要在WEB-INF/view目录下配置好对应的demojsp.jsp文件
     */
    @GetMapping("/toJsp")
    public String toJsp() {
        return "demojsp";
    }
}
@Data
public class Response<T> implements Serializable {
    private int code;
    private String msg;
    private T data;
}看起来非常简单,对不?
那么问题来了:为什么加上 @ResponseBody 这个注解后,就能返回 Json 数据,而不加的话就会渲染 Jsp页面?
从现象上来看,@ResponseBody 似乎把响应数据的渲染路径改变了,之前明明要渲染页面,现在硬生生改成了返回 Json 数据。
没错,就是这样。只要加了 @ResponseBody 注解,就会直接把接口返回的数据通过Json写到响应中,后续的视图解析器将不会被执行,也就不存在视图渲染一说了。
为了加深印象,我们看看源码是怎么实现的(我们聚焦这两个处理器相关的代码,不再阐述SpringMvc处理的主线)。
Spring 容器初始化时,会自动添加 RequestResponseBodyMethodProcessor 和 ViewNameMethodReturnValueHandler 这两个处理器,它们分别用于处理不同类型的响应数据。具体可以参见 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter类的getDefaultReturnValueHandlers方法,其中的关键代码如下(注意两个处理器的顺序,这个很关键):
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
    List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
    ...
    handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
            this.contentNegotiationManager, this.requestResponseBodyAdvice));
    handlers.add(new ViewNameMethodReturnValueHandler());
    ...
    return handlers;
}其中,RequestResponseBodyMethodProcessor 用于处理方法带有 @ResponseBody 的处理器,而 ViewNameMethodReturnValueHandler 用于预处理带有名称的页面渲染逻辑。它们都实现了HandlerMethodReturnValueHandler 这个接口的 supportsReturnType 和 handleReturnValue 方法:
    // RequestResponseBodyMethodProcessor
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
                returnType.hasMethodAnnotation(ResponseBody.class));
    }
    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        // 注意这行代码,setRequestHandled为true表示当前请求已经处理完毕,不需要后续的渲染处理了。
        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
        // 这里会直接把响应数据写到输出流
        writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }    // ViewNameMethodReturnValueHandler
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        Class<?> paramType = returnType.getParameterType();
        return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
    }
    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        if (returnValue instanceof CharSequence) {
            String viewName = returnValue.toString();
            mavContainer.setViewName(viewName);
            if (isRedirectViewName(viewName)) {
                mavContainer.setRedirectModelScenario(true);
            }
        }
        else if (returnValue != null) {
            throw new UnsupportedOperationException("Unexpected return type: " +
                    returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
        }
    }如果 supportsReturnType 方法返回 true,接口返回的 Response 就会由该处理器的 handleReturnValue进行处理或者初步处理。
细心的读者会发现,前面我们提到 ViewNameMethodReturnValueHandler 用于预处理带有名称的页面渲染逻辑。这里的“预处理”是指这个处理器只是设置了视图的名称等属性,具体的渲染还要交由 RequestMappingHandlerAdapter 中的后续逻辑进行处理。源码参见 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 类的getModelAndView 方法:
    @Nullable
    private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
                                         ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
        modelFactory.updateModel(webRequest, mavContainer);
        // 如果当前请求已经处理完毕,就不需要再渲染视图了
        if (mavContainer.isRequestHandled()) {
            return null;
        }
        ModelMap model = mavContainer.getModel();
        ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
        if (!mavContainer.isViewReference()) {
            mav.setView((View) mavContainer.getView());
        }
        if (model instanceof RedirectAttributes) {
            Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
            HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
            if (request != null) {
                RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
            }
        }
        return mav;
    }从上面的流程可以看出,加了 @ResponseBody 注解后,RequestResponseBodyMethodProcessor处理器会得到执行,它会提前将响应数据写入输出流,并且标记请求已经被处理完成,从而阻止了后续的视图渲染流程。
思考题:如果接口 /toJson 对应的方法忘记使用注解,此时会发生什么?
提示:会根据返回值的类型落到对应的处理器中,对于我们的例子来说,会由 ModelAttributeMethodProcessor处理器执行:寻找 WEB-INF/view/toJson.jsp 页面尝试渲染,若找不到则重定向请求到 /error,进行后续的错误处理。
建议大家顺着源码调试一遍(包括将响应数据处理为 Json 的流程),以后遇到 @ResponseBody 注解后,能顺其自然地回想起相关的执行流程,跳出“它是用来将响应数据写入输出流”这样较为粗浅的认知。
最后,本文对应的完整演示项目已经上传到 Github。
SpringMvc 如何同时支持 Jsp 和 Json 接口?的更多相关文章
- springmvc 支持对象与json 自动转换的配置
		基于maven的工程, 需要在pom.xml中添加如下依赖 <dependency> <groupId>javax.servlet</groupId> <ar ... 
- java:Springmvc框架2(Ajax,Json,Interceptor,Upload,Exception)
		1.springmvcAjax: springmvc.xml: <?xml version="1.0" encoding="UTF-8"?> < ... 
- Loadrunner模拟JSON接口请求进行测试
		Loadrunner模拟JSON接口请求进行测试 一.loadrunner脚本创建 1.Insert - New step -选择Custom Request - web_custom_re ... 
- 纯CSS菜单样式,及其Shadow DOM,Json接口 实现
		先声明,要看懂这篇博客要求你具备少量基础CSS知识, 当然如果你只是要用的话就随便了,不用了解任何知识 完整项目github链接:https://github.com/git-Code-Shelf/M ... 
- SpringBoot(二)-- 支持JSP
		SpringBoot虽然支持JSP,但是官方不推荐使用.看网上说,毕竟JSP是淘汰的技术了,泪奔,刚接触 就淘汰.. SpringBoot集成JSP的方法: 1.配置application.prope ... 
- 【web】支持jsp+mvc访问
		直接使用SpringMVC时配置访问jsp页面时很容易的事,但是由于spring Boot使用内嵌的servlet容器,所以对jsp的支持不是很好,而且也不建议使用jsp,但是为了满足这种返回jsp页 ... 
- RestKit ,一个用于更好支持RESTful风格服务器接口的iOS库
		简介 RestKit 是一个用于更好支持RESTful风格服务器接口的iOS库,可直接将联网获取的json/xml数据转换为iOS对象. 项目主页: RestKit 最新示例: 点击下载 注意: 如果 ... 
- SpringBoot-(2)-Web的json接口,静态网页,动态页面
		一, 了解注解@Controller和@RestController @Controller:处理Http请求 @RestController:Spring4以后新增注解,相当于@Controller ... 
- [淘宝客技术篇008](无需登录)淘宝天猫优惠券JSON接口1
		今天,小星给大家分享的是一个非常重要,非常有意义的接口:获取淘宝天猫优惠券的JSON接口. 先上个链接: http://uland.taobao.com/cp/coupon_list?pid=mm_2 ... 
随机推荐
- syc-day2
			第1题:mod注意负数. 第2题:dp 第3题:构造(奇偶性) 第4题:线段树 
- Canvas 线性图形(五):多边形
			前言 CanvasRenderingContext2D 没有提供绘制多边形的函数,所以只能由我们自己来实现绘制多边形的函数.以六边形为基础,需要用到三角函数:sin 和 cos. 点 A 坐标 (一) ... 
- Redis集群搭建 三主三从
			Redis集群介绍 Redis 是一个开源的 key-value 存储系统,由于出众的性能,大部分互联网企业都用来做服务器端缓存.Redis在3.0版本之前只支持单实例模式 虽然支持主从模式,哨兵模式 ... 
- neo4j删除节点和关系
			两种方法: 一.用下列 Cypher 语句: match (n) detach delete n 原理:匹配所有的节点,然后进行删除. 二. 从文件系统上删除对应的数据库. 1.停掉服务: 2.删除 ... 
- 中国天气api接口xml,json
			http://m.weather.com.cn/data/101110101.html 大坑有木有??反应慢不说了,还老不更新!! 想贴段代码的,现在又打不 开了(貌似3月4号以后没更新过) ==== ... 
- ExtJS 布局-Anchor 布局(Anchor layout)
			更新记录: 2022年5月30日 发布本篇 1.说明 anchor布局类似auto布局从上到下进行堆叠,但不同的是其可以指定每个元素相对于容器大小的比例. 当调整父容器大小,容器根据指定的规则调整所有 ... 
- 关于VHDL中case语句多执行语句的书写方式(转载stackoverflow.com并做翻译汇总)
			很多国内的教材对于case语句的讲解非常单一,比如: 1 [标号:]CASE 多值表达式 IS 2 WHEN 选择值 => 被赋值变量 <=赋值变量: 3 WHEN 选择值 => 被 ... 
- TypeScript 泛型(generic)  入门介绍
			TypeScript 泛型函数 下面来创建第一个使用泛型的例子:identity函数.这个函数会返回任何传入它的值.你可以把这个函数当成是echo命令.不用泛型的话,这个函数可能是下面这样: func ... 
- SAP APO - Architecture
			SAP APO体系结构由多个组件组成-数据库,BI环境包含InfoCube和实时缓存. InfoCube是BI数据集市的一部分,实时缓存是您保留与计划和调度有关的所有数据的主要区域. 您可以在实时缓存 ... 
- 在Ubuntu系统下,可执行文件的表现形式
			在Windows系统下的可执行文件都带有.exe的后缀,而对于Linux系统下的可执行文件,则不会带有后缀,如下图 对于.txt文件,Ubuntu下也有相应的记事本程序打开,对于.xml,ubuntu ... 
