写点什么

SpringMvc 如何同时支持 Jsp 和 Json 接口?

作者:xiaoxi666
  • 2022 年 8 月 18 日
    湖北
  • 本文字数:4192 字

    阅读完需:约 14 分钟

后端同学基本都会见过这种场景:在同一个工程中,有些页面使用 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@Slf4jpublic class MyController {
/** * 这个接口将会返回json数据 */ @GetMapping("/toJson") @ResponseBody // 注意这个注解,有了它就会返回json数据 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"; }}
@Datapublic 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。


发布于: 刚刚阅读数: 4
用户头像

xiaoxi666

关注

一枚后端coder,一起来玩儿一起学! 2017.11.09 加入

GitHub : https://github.com/xiaoxi666 文章多平台同步,欢迎关注交流: - 微信公众号:xiaoxi666 - 博客园:www.cnblogs.com/xiaoxi666/ - 知乎:www.zhihu.com/people/xiao-xi-12-55-99

评论

发布
暂无评论
SpringMvc如何同时支持Jsp和Json接口?_xiaoxi666_InfoQ写作社区