• 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> 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解析依赖的包:

            
            <dependency>
                <groupId>jstlgroupId>
                <artifactId>jstlartifactId>
                <version>1.2version>
            dependency>
            
            <dependency>
                <groupId>org.eclipse.jettygroupId>
                <artifactId>apache-jspartifactId>
                <version>9.4.8.v20171121version>
            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 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 getDefaultReturnValueHandlers() {
        List 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 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

     

  • 相关阅读:
    1、Elasticsearch 8.X 概述与安装
    【Leetcode】2864. 最大二进制奇数
    嵌入式开发:选择嵌入式GUI生成器时要注意什么
    解决 Docker容器因 iptables无法启动的问题
    怎么做好web服务器安全措施
    Git常用命令(面试+复习)
    【黄啊码】垃圾回收可以赚钱,那php的垃圾回收机制你懂多少?
    使用C接口访问MySQL数据库
    初识Springboot
    LeetCode --- 1460. Make Two Arrays Equal by Reversing Subarrays 解题报告
  • 原文地址:https://www.cnblogs.com/xiaoxi666/p/16600654.html