spring mvc 拓展 -- 同一url根据不同后缀返回不同的视图

本文代码已整理上传 github

前言
现在新的web项目基本都是前后分离的了, 但是我之前一个项目组页面全部是用的freemarker模版, 
没有前后分离, 然后项目也想前端分离, 
所以就有了这个需求, 尽量不该或者少改后端的代码,来同时适应前端代码, 同时尽量兼容之前的freemarker模版。

然后就开始找解决方案, 找到了spring mvc的ContentNegotiatingViewResolver,
结合项目的使用, 可以做到同一个url不加后缀就返回之前的html页面, 加了.json 后缀就返回json数据

下面演示下具体的使用效果, 结合我上一篇文章搭建的web工程作为示例springweb

配置ContentNegotiatingViewResolver

在使用spring mvc时, 在注册requestMapping的时候, 除了会注册写在controller注解上的url,还会注册url.*到requestMapping, 所以url后面加什么拓展名都能映射到 原controller 上

ContentNegotiatingViewResolver 可以做到根据url后面不同的拓展名来返回不同的视图, 当然还可以根据 mediaType, formmat 参数 进行判断, 这里只演示根据拓展名的。

接着上篇文章中的 SpringMvcConfig 加入下面的配置

     /**
     * 配置多视图解析器
     *
     * @param manager       manager 会自动构建,configureContentNegotiation可以进行配置
     * @param viewResolvers 当前项目的 viewResolver, (此时会包含上面配置的 freemarkerViewResolver)
     * @return ContentNegotiatingViewResolver
     * @see WebMvcConfigurerAdapter#configureContentNegotiation(org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer)
     */
    @Bean
    public ContentNegotiatingViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager, List<ViewResolver> viewResolvers) {

        ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver();
        viewResolver.setContentNegotiationManager(manager);

        // 设置默认view, default view 每次都会添加到 真正可用的视图列表中, json视图没有对应的ViewResolver
        View jackson2JsonView = new MappingJackson2JsonView();
        viewResolver.setDefaultViews(Collections.singletonList(jackson2JsonView));

        viewResolver.setViewResolvers(viewResolvers);
        return viewResolver;
    }

就是这么简单, 现在你的项目就可以根据拓展名返回不同的视图了

PS: json 视图是将model中的数据通过 jackson(默认)序列化返回

运行效果

新建一个实体类, 叫Goods
    public class Goods implements Serializable {

    private static final long serialVersionUID = -5018788390786034623L;

    public Goods(String code, String name, Double price) {
        this.code = code;
        this.name = name;
        this.price = price;
    }

    private Long id; // 商品编码
    private String code; // 编码
    private String name; // 品名
    private Double price; // 售价

}
新建一个controller, 叫GoodsController
    @Controller
    @RequestMapping("/goods")
    public class GoodsController {
    
        private static final List<Goods> GOODS_LIST = new ArrayList<>();
    
        static {
            GOODS_LIST.add(new Goods("998765", "哇哈哈矿泉水", 2.0));
            GOODS_LIST.add(new Goods("568925", "蒙牛真果粒", 4.7));
        }
    
        @RequestMapping("/list")
        public String list(GoodsCondition condition, Model model) {
            model.addAttribute("data", GOODS_LIST);
            return "goods";
        }
    }

这个controller的写法是我们项目的一般写法, 返回String的视图名称, 将页面需要的数据放到model中, 一般都是 data.

视图模版文件, goods.ftl, 放到 /resources/templates/ 下
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>商品页</title>
</head>
<body>
<h3>商品列表</h3>
<hr>
<table>
    <thead>
    <tr>
        <td style="width: 100px">code</td>
        <td style="width: 150px">name</td>
        <td style="width: 100px">price</td>
    </tr>
    </thead>
    <tbody>
    <#list data as item>
    <tr>
        <td>${item.code?html}</td>
        <td>${item.name!?html}</td>
        <td>${item.price!}</td>
    </tr>
    </#list>
    </tbody>
</table>

</body>
</html>

运行项目,
首先浏览器 访问 http://localhost:8080/cat/goods/list, 不加任何后缀效果如下:

clipboard.png

返回的是 freemarker的 html 视图

然后访问 http://localhost:8080/cat/goods/list.jon, 加上.json 后缀

clipboard.png

成功返回json数据!

源码解析

我们在配置ContentNegotiatingViewResolver的bean时候自动注入了 两个参数: ContentNegotiationManager 和 List<ViewResolver>

List<ViewResolver> 我们自己就配置了一个 freemarkerViewResolver 所以这个参数能注入, 没有问题, 那么 ContentNegotiationManager 是怎么来的?

接触过 spring mvc 注解配置的同学知道, spring 提供了WebMvcConfigurer接口供使用者自定义spring mvc 配置,
其实spring mvc有自己的一个配置类DelegatingWebMvcConfiguration来收集用户自定义配置,并提供一些默认配置

clipboard.png

看到没, 那个 setConfigurers 方法就是用来收集自定义配置的, 而这个本身也是个配置类, 继承了WebMvcConfigurationSupport类, spring mvc的各种初始化 就是从这里开始的

我们需要的参数ContentNegotiationManager就是定义在WebMvcConfigurationSupport里的

clipboard.png

而且 当在 classpath 下有 jackson 存在就会添加 json 拓展名映射,
jaxb存在就添加 xml拓展名映射, 很智能

这样,项目启动阶段就结束了,接下来分析运行阶段,

ContentNegotiatingViewResolver本身也ViewResolver, 我们还定义FreeMarkerViewResolver,两者同时存在, 为什么一定先执行的是ContentNegotiatingViewResolver?

如果之前配置过多视图共存(volecity, jsp, freemarker)的同学会知道, ViewResolver是有 order属性的, 执行的先后是按照order的顺序来的, order越小越先执行,

clipboard.png

看下两个类中order的定义, ContentNegotiatingViewResolver默认是最高优先级的,所以当一个请求走到返回视图阶段, 先执行的是ContentNegotiatingViewResolver

那ContentNegotiatingViewResolver做了什么事情呢? 先上源码

    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
        List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
        if (requestedMediaTypes != null) {
            List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
            View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
            if (bestView != null) {
                return bestView;
            }
        }
        if (this.useNotAcceptableStatusCode) {
            if (logger.isDebugEnabled()) {
                logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
            }
            return NOT_ACCEPTABLE_VIEW;
        }
        else {
            logger.debug("No acceptable view found; returning null");
            return null;
        }
    }

    private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
            throws Exception {

        List<View> candidateViews = new ArrayList<View>();
        for (ViewResolver viewResolver : this.viewResolvers) {
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                candidateViews.add(view);
            }
            for (MediaType requestedMediaType : requestedMediaTypes) {
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                for (String extension : extensions) {
                    String viewNameWithExtension = viewName + '.' + extension;
                    view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                }
            }
        }
        if (!CollectionUtils.isEmpty(this.defaultViews)) {
            candidateViews.addAll(this.defaultViews);
        }
        return candidateViews;
    }

    private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
        for (View candidateView : candidateViews) {
            if (candidateView instanceof SmartView) {
                SmartView smartView = (SmartView) candidateView;
                if (smartView.isRedirectView()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Returning redirect view [" + candidateView + "]");
                    }
                    return candidateView;
                }
            }
        }
        for (MediaType mediaType : requestedMediaTypes) {
            for (View candidateView : candidateViews) {
                if (StringUtils.hasText(candidateView.getContentType())) {
                    MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
                    if (mediaType.isCompatibleWith(candidateContentType)) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Returning [" + candidateView + "] based on requested media type '" +
                                    mediaType + "'");
                        }
                        attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
                        return candidateView;
                    }
                }
            }
        }
        return null;
    }

上面这段就是ContentNegotiatingViewResolver如何处理视图的逻辑
requestedMediaTypes 就是拿到根据请求拿到需要的 MediaType,
然后就是根据 requestedMediaTypes 找到所有可用的 View,
我们在配置ContentNegotiatingViewResolver里设置的 default view
每次都会出现在 candidateViews 中, 但是是在最后加上的。
然后就是在 candidateViews 中找到 bestView 返回

当我们不加后缀时,无法根据后缀映射 MediaType, 所以requestedMediaTypes就是
candidateViews 就是freemarker的 view 加上设置的 默认的 json view.
在getBestView的时候 requestedMediaTypes 和 freemarkerView的content-type: text/html匹配, 所以就返回了freemarkerView。

当加了.json后缀, 根据之前的拓展名和MediaType的映射, requestedMediaTypes是 application/json
candidateViews 依然是freemarker的 view 加上设置的 默认的 json view.
但是这次freemarkerView的content-type不匹配, 而是和json view的 application/json 匹配, 所以返回 json 视图。

后续

这样新的前端项目在访问url加上.json后缀就能拿到json数据了,
但是还有一个问题, 就是在接收请求参数时, 老的项目全是 form表单提交, 但是新的前端项目需要以json格式提交, 如果在controller中的方法中 都加上 @RequestBody 注解, 工作量不少, 而且不兼容form表单提交, 所以这样不可行。

下一篇讲下controller接收参数时如何自动判断是 form 提交还是 json 数据提交, 从而使用不同的参数处理器 来接收参数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`feign-spring-mvc-starter` 是一个 Feign 的扩展,它支持使用 Spring MVC 注解来定义和调用 REST 服务。使用 `feign-spring-mvc-starter`,你可以像使用 Spring MVC 控制器一样定义 Feign 客户端,从而更方便地进行 REST 服务的开发。 在使用 `feign-spring-mvc-starter` 之前,你需要先了解 Feign 和 Spring MVC 的基本概念和用法。 Feign 是一个声明式的 Web 服务客户端,它可以帮助你更方便地定义和调用 REST 服务。Feign 的基本使用方法是定义一个接口,用于描述 REST 服务的 API,然后使用 Feign 注解来声明这个接口。 Spring MVC 是一个基于 Java 的 Web 框架,它提供了一组注解和 API,用于处理 Web 请求和响应。 `feign-spring-mvc-starter` 将 Feign 和 Spring MVC 结合起来,使你可以使用 Spring MVC 注解来定义和调用 REST 服务。使用 `feign-spring-mvc-starter`,你可以更方便地使用 Feign 来调用 REST 服务。 以下是一个使用 `feign-spring-mvc-starter` 的示例: 1. 添加 Maven 依赖 在 pom.xml 文件中添加以下依赖项: ```xml <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-spring-mvc</artifactId> <version>5.3.1</version> </dependency> ``` 2. 定义 Feign 接口 定义一个 Feign 接口,用于描述 REST 服务的 API。例如: ```java @FeignClient(name = "example-service") public interface ExampleClient { @GetMapping("/example") String getExample(); } ``` 在这个接口中,我们使用了 `@FeignClient` 注解来声明这个接口是一个 Feign 客户端,并指定了服务的名称。然后,我们定义了一个 `getExample()` 方法,用于调用 example-service 服务的 /example 路径。 3. 定义 Spring MVC 控制器 定义一个 Spring MVC 控制器,用于处理来自客户端的请求。例如: ```java @RestController public class ExampleController { private final ExampleClient exampleClient; public ExampleController(ExampleClient exampleClient) { this.exampleClient = exampleClient; } @GetMapping("/") public String index() { return exampleClient.getExample(); } } ``` 在这个控制器中,我们注入了 `ExampleClient`,并在 `index()` 方法中使用它来调用 example-service 服务的 /example 路径。 4. 运行应用程序 现在,你可以运行应用程序并访问 http://localhost:8080/ ,你应该会看到来自 example-service 服务的响应。 这就是一个使用 `feign-spring-mvc-starter` 的示例。使用 `feign-spring-mvc-starter`,你可以更方便地使用 Feign 来调用 REST 服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值