Application content negotiation in view of [View] enjoy learning Spring MVC

Each one

Life is very interesting: first of all have to live longer. Live longer be able to see yourself, and then you can see the long beings

Foreword

After the first two understand articles Spring MVCafter content negotiation, I believe you have been able to skillfully use Spring MVCthis ability to provide, with the RESTfulplay of its efficacy. In fact, this will achieve our purpose 80%, also reached the purpose of my writing this piece of knowledge points.

Why is it 80%then? Because I think the complete separation of the front and back of today, most of the scenes are using this situation to complete the coverage.
Why is there 20%too? Since content negotiation is not only can be used HttpMessageon, can also be used in Viewthe view, which is the focus of this article would like to add content.

In content negotiation HttpMessageapplication on

Examples of the first two articles are based on this. When I mentioned to explain the principles of: entrance processing in AbstractMessageConverterMethodProcessor.writeWithMessageConverters()the method, see the subclasses of this abstract class can be discerned:
Here Insert Picture Description
from the sub-class implementation, you can understand: It HttpMessageis strongly related, both through the HttpMessageConverterprocess message do content negotiation.

That is the most common comment we present two implementation class treatment: @ResponseBody. The return value is directly orHttpEntity/ResponseEntity类型(也就是不能是RequestEntity就成)

There is no doubt, based on the @ResponseBodyRest of the interface mode complete separation of the front and back already today is the mainstream way, so I said the first two articles covering the 80%scene should not be an exaggeration it ~


I search ContentNegotiationManager.resolveMediaTypes()methods in ContentNegotiatingViewResolveruse to the inside, so I naturally think of the contents of the consultation can also be combined use with the ViewRenderer ~

Content negotiation in view of Viewthe application

As the previous examples I gave are based Http消息, there is no view at all. This article is here to explain the need to use content negotiation to resolve in terms of view: the same URL, with different views as to show the way .

We already know: RequestMappingInfoHandlerMapping(@RequestMapping)it's on with the suffix httpwhen a request to match, if not find a precise pattern, it will pattern+.*then match the url, it will deal with a number of different forms that url, but returned to the same View . This article will teach you to use a @RequestMappingcan return more than View~

Note: I refer here to the View to return, for this embodiment returns the message body, this discussion is not at the category it belongs to the case 1.

View resolver ViewResolver

About the contents of the view, can be found here: View
content on view resolver, can be found here: ViewResolver

This paper briefly again "review" about Spring MVCthe use of the process ViewResolver:

Use at:DispatcherServlet.resolveViewName()
obtained after logical view, a good view through the already registered resolver ViewResolverto resolve the logical view of the real viewView

DispatcherServlet:
    @Nullable
    protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
        if (this.viewResolvers != null) {
            // 按照顺序:一个一个执行。第一个最先解析到不返回null的  就是最终返回的view视图
            for (ViewResolver viewResolver : this.viewResolvers) {
                View view = viewResolver.resolveViewName(viewName, locale);
                if (view != null) {
                    return view;
                }
            }
        }
        return null;
    }

Load at:DispatcherServlet.initViewResolvers()
This explain in Spring MVCdetail the nine assembly when said load

DispatcherServlet:
    private void initViewResolvers(ApplicationContext context) {
        // 1、若detectAllViewResolvers=true,去容器中找到所有的ViewResolver Bean们。排序后返回
        // 2、若不是探测全部。就只找BeanName=viewResolver它的这一个Bean
        // 2、若一个都没有找到,就走默认策略:从DispatcherServlet.properties里配置的读取默认的配置
    }

The search strategy for our reasonable registration, management view resolvers are very useful, it can be a little attention

Statement at:WebMvcConfigurationSupport.mvcViewResolver()

WebMvcConfigurationSupport:

    // @since 4.1 向容器注册一个ViewResolver Bean  
    // 使用的是容器管理方式:ViewResolverComposite 
    @Bean
    public ViewResolver mvcViewResolver() {
        
        // mvcContentNegotiationManager:内容协商管理器(本文重点之一)
        ViewResolverRegistry registry = new ViewResolverRegistry(mvcContentNegotiationManager(), this.applicationContext);
        // protected方法,回调给我们调用者,允许自定义ViewResolverRegistry 
        configureViewResolvers(registry);

        // 它的意思是:如果你没有自定义(或者自定义了但一个解析器都木有)
        // 那就主动去容器里找。如果仅仅仅仅只知道一个:那它就是InternalResourceViewResolver(注意此处是new的)
        // 注意此处的处理方式哦~~~~
        if (registry.getViewResolvers().isEmpty() && this.applicationContext != null) {
            String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.applicationContext, ViewResolver.class, true, false);
            if (names.length == 1) {
                registry.getViewResolvers().add(new InternalResourceViewResolver());
            }
        }

        // 最终使用ViewResolverComposite把这些(多个)装起来,便于管理~
        ViewResolverComposite composite = new ViewResolverComposite();
        composite.setOrder(registry.getOrder());
        composite.setViewResolvers(registry.getViewResolvers());
        if (this.applicationContext != null) {
            composite.setApplicationContext(this.applicationContext);
        }
        if (this.servletContext != null) {
            composite.setServletContext(this.servletContext);
        }
        return composite;
    }

Here we can find that it is used by default in the case of default we said above, ContentNegotiationManagerto handle content negotiation. Therefore, the following come today to focus on the protagonist ContentNegotiatingViewResolverwho

ContentNegotiatingViewResolver: content negotiation view resolver

ContentNagotiatingViewResolverIt does not render the view, but the view delegated to other processors.

To make this work properly parser, order number other than the need to set a higher priority processor views (the default is the highest)

// @since 3.0
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean {
    // 用于内容协商的管理器
    @Nullable
    private ContentNegotiationManager contentNegotiationManager;
    private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();

    // 如果没有合适的view的时候,是否使用406这个状态码(HttpServletResponse#SC_NOT_ACCEPTABLE)
    // 默认值是false:表示没有找到就返回null,而不是406
    private boolean useNotAcceptableStatusCode = false;
    // 当无法获取到具体的视图时,会走defaultViews
    @Nullable
    private List<View> defaultViews;
    
    @Nullable
    private List<ViewResolver> viewResolvers;
    private int order = Ordered.HIGHEST_PRECEDENCE; // 默认,优先级就是最高的

    // 复写:WebApplicationObjectSupport的方法
    // 它在setServletContext和initApplicationContext会调用(也就是容器启动时候会调用)
    @Override
    protected void initServletContext(ServletContext servletContext) {
        Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
        //容器内找到了  就以容器内所有已经配置好的视图解析器都拿出来(包含父容器)
        if (this.viewResolvers == null) {
            this.viewResolvers = new ArrayList<>(matchingBeans.size());
            for (ViewResolver viewResolver : matchingBeans) {
                if (this != viewResolver) { // 排除自己
                    this.viewResolvers.add(viewResolver);
                }
            }
        } else { // 进入这里证明是调用者自己set进来的
            for (int i = 0; i < this.viewResolvers.size(); i++) {
                ViewResolver vr = this.viewResolvers.get(i);
                if (matchingBeans.contains(vr)) {
                    continue;
                }
                String name = vr.getClass().getName() + i;
                // 对视图解析器完成初始化工作~~~~~
                // 关于AutowireCapableBeanFactory的使用,参见:https://blog.csdn.net/f641385712/article/details/88651128
                obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
            }

        }

        // 找到所有的ViewResolvers排序后,放进ContentNegotiationManagerFactoryBean里
        AnnotationAwareOrderComparator.sort(this.viewResolvers);
        this.cnmFactoryBean.setServletContext(servletContext);
    }

    // 从这一步骤可以知道:contentNegotiationManager 可以自己set
    // 也可以通过工厂来生成  两种方式均可
    @Override
    public void afterPropertiesSet() {
        if (this.contentNegotiationManager == null) {
            this.contentNegotiationManager = this.cnmFactoryBean.build();
        }
        if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
            logger.warn("No ViewResolvers configured");
        }
    }

    // 处理逻辑视图到View 在此处会进行内容协商
    @Override
    @Nullable
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");

        // getMediaTypes()这个方法完成了
        // 1、通过contentNegotiationManager.resolveMediaTypes(webRequest)得到请求的MediaTypes
        // 2、拿到服务端能够提供的MediaTypes  producibleMediaTypes
        // (请注意因为没有消息转换器,所以它的值的唯一来源是:request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE))
        // (若没有指定producers的值,那就是ALL)
        // 3、按照优先级,协商出`selectedMediaTypes`(是个List)
        List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());

        // 进入此处:说明协商出了有可用的MediaTypes(至少有一个嘛)
        if (requestedMediaTypes != null) {

            // getCandidateViews()这个很重要的方法,见下文
            List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);

            // 上面一步骤解析出了多个符合条件的views,这里就是通过MediaType、attrs等等一起决定出一个,一个,一个最佳的
            // getBestView()方法描述如下:
            // 第一大步:遍历所有的candidateViews,只要是smartView.isRedirectView(),就直接return
            // 第二大步:遍历所有的requestedMediaTypes,针对每一种MediaType下再遍历所有的candidateViews
            // 1、针对每一种MediaType,拿出View.getContentType(),只会看这个值不为null的
            // 2、view的contentType!=null,继续看看mediaType.isCompatibleWith(candidateContentType) 若不匹配这个视图就略过
            // 3、若匹配:attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST)  然后return掉此视图作为best最佳的
            View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
            if (bestView != null) { // 很显然,找到了最佳的就返回渲染吧
                return bestView;
            }
        }
        
        ... 
        // useNotAcceptableStatusCode=true没找到视图就返回406
        // NOT_ACCEPTABLE_VIEW是个private内部静态类View,它的render方法只有一句话:
        // response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
        if (this.useNotAcceptableStatusCode) {
            return NOT_ACCEPTABLE_VIEW;
        } else {
            return null;
        }
    }

    // 根据viewName、requestedMediaTypes等等去得到所有的备选的Views~~
    // 这这里会调用所有的viewResolvers.resolveViewName()来分别处理~~~所以可能生成多多个viewo ~
    private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
        List<View> candidateViews = new ArrayList<>();
        if (this.viewResolvers != null) {
            Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");

            // 遍历所有的viewResolvers,多逻辑视图一个一个的处理
            for (ViewResolver viewResolver : this.viewResolvers) {
                View view = viewResolver.resolveViewName(viewName, locale);
                if (view != null) {
                    candidateViews.add(view); // 处理好的就装进来
                }


                // 另外还没有完:遍历所有支持的MediaType,拿到它对应的扩展名们(一个MediaType可以对应多个扩展名)
                // 如果viewName + '.' + extension能被处理成一个视图,也是ok的
                // 也就是说index和index.jsp都能被解析成view视图~~~
                for (MediaType requestedMediaType : requestedMediaTypes) {
                    // resolveFileExtensions()方法可以说这里是唯一调用的地方
                    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); // 带上后缀名也能够处理的  这种视图也ok
                        }
                    }
                }
            }
        }
        // 若指定了默认视图,把视图也得加上(在最后面哦~)
        if (!CollectionUtils.isEmpty(this.defaultViews)) {
            candidateViews.addAll(this.defaultViews);
        }
        return candidateViews;
    }
}

About ContentNegotiatingViewResolverme summarize the details of the following elements:

  1. ContentNegotiationManagerStrategies for content negotiation can be set manually specify, can also FactoryBeanautomatically generate
  2. viewResolversThe default is to find all of course, you can also manually set to come within the container ~
  3. Request using media types, in accordance with selection of different extensions of different output formats view
  4. Not handle their own view, but to different ViewResolver agents to handle different view;
  5. The default is to support Acceptand extension of the negotiation. And also supports 逻辑视图名.后缀the view resolution mode ~
  6. According to the View.getContentTypematch MediaTypeto complete the best match

how to use?

We already know that, by default, Spring MVCcan not use this view resolver content negotiation, so if the same resources, demand for multi-view show, we need to manually configure (open) support.

By retrieving you can see ViewResolverRegistryit provides a convenient way for our use:

Of course, you can also configure a single ContentNegotiatingViewResolverdo @Bean way, the principle is very simple and well explained. In this paper I will give an example of a best practice reference

public class ViewResolverRegistry {
    ...
    public void enableContentNegotiation(View... defaultViews) {
        initContentNegotiatingViewResolver(defaultViews);
    }
    public void enableContentNegotiation(boolean useNotAcceptableStatus, View... defaultViews) {
        ContentNegotiatingViewResolver vr = initContentNegotiatingViewResolver(defaultViews);
        vr.setUseNotAcceptableStatusCode(useNotAcceptableStatus);
    }
    // 初始化一个内容协商视图解析器
    private ContentNegotiatingViewResolver initContentNegotiatingViewResolver(View[] defaultViews) {
        // ContentNegotiatingResolver in the registry: elevate its precedence!
        // 请保证它是最高优先级的:在所有视图解析器之前执行
        // 这样即使你配置了其它的视图解析器  也会先执行这个(后面的被短路掉)
        this.order = (this.order != null ? this.order : Ordered.HIGHEST_PRECEDENCE);

        // 调用者自己已经配置好了一个contentNegotiatingResolver,那就用他的
        if (this.contentNegotiatingResolver != null) {
            // 若存在defaultViews,那就处理一下把它放进contentNegotiatingResolver里面
            if (!ObjectUtils.isEmpty(defaultViews) && !CollectionUtils.isEmpty(this.contentNegotiatingResolver.getDefaultViews())) {
                List<View> views = new ArrayList<>(this.contentNegotiatingResolver.getDefaultViews());
                views.addAll(Arrays.asList(defaultViews));
                this.contentNegotiatingResolver.setDefaultViews(views);
            }
        } else { // 若没配置就自己new一个 并且设置好viewResolvers
            this.contentNegotiatingResolver = new ContentNegotiatingViewResolver();
            this.contentNegotiatingResolver.setDefaultViews(Arrays.asList(defaultViews));
            // 注意:这个viewResolvers是通过此ViewResolverRegistry配置进来的
            // 若仅仅是容器内的Bean,这里可捕获不到。所以如果你有特殊需求建议你自己set
            // 若仅仅是jsp()/tiles()/freeMarker()/groovy()/beanName()这些,内置的支持即可满足要求儿聊
            // ViewResolverRegistry.viewResolver()可调用多次,因此可以多次指定  若有需要个性化,可以调用此方法
            this.contentNegotiatingResolver.setViewResolvers(this.viewResolvers);
            if (this.contentNegotiationManager != null) {
                this.contentNegotiatingResolver.setContentNegotiationManager(this.contentNegotiationManager);
            }
        }
        return this.contentNegotiatingResolver;
    }
}

Point: Although there is some new view resolver out, but do not worry about the final will be executed InitializingBean, ApplicationContextAware... and so some of the interface methods. Because these are handed over to ViewResolverCompositea unified do it through (and therefore does not need into the Spring container can also reduce the burden of the container is also an optimized)

Above "review" when mentioned, Spring MVCis ready ViewResolverRegistryafter the callback us, so actual use can be configured (best practices) by this entry:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(); // 开启内容协商视图解析器
    }
}

I am going to introduce in the case == To facilitate the negotiation process of a small partner to control and understand ==, I offer the entire contents of this follows the implementation of the principle of flow chart (Figure if there is an error message can point out, thank you) as an aid to understanding :

Here Insert Picture Description

---

Examples of Use

A mule is a horse, or to always pull out yo. Here I am with a concrete case work, to show you its usage.

Demand: the same RESTfulURL, I want to get a PDF view, JSON view, Html view? ? ?

Implementation code

Because it is the same URL, and also requires that there are different views, so here use ContentNegotiatingViewResolverto do content negotiation is very handy.

1, ready to deal with these three views for the ViewResolverimplementation class:

    // 自定义三个视图分别用于处理对应的视图需求
    private final ViewResolver pdf_viewresolver= (viewName, locale) -> new View() {
        @Override
        public String getContentType() {
            return MediaType.APPLICATION_PDF_VALUE;
        }
        @Override
        public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().write("<html><body style='color:red'>this is pdf view</body></html>");
        }
    };
    private final ViewResolver excel_viewresolver= (viewName, locale) -> new View() {
        @Override
        public String getContentType() {
            return MediaType.APPLICATION_JSON_UTF8_VALUE;
        }
        @Override
        public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().write("<html><body style='color:yellow'>this is json view</body></html>");
        }
    };
    private final ViewResolver html_viewresolver= (viewName, locale) -> new View() {
        @Override
        public String getContentType() {
            return MediaType.TEXT_HTML_VALUE;
        }
        @Override
        public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().write("<html><body style='color:green'>this is html view</body></html>");
        }
    };

Note: getContentType three (), rendering the contents of color are not the same

Description: I'm just here because simulation, so all my anonymous classes to implement, ladies and small partners to understand what's theoretically no obstacle should it (a problem can give me a message ~)

2, open Spring MVCon a view to ContentNegotiationsupport content negotiation:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(pdf_viewresolver);
        registry.viewResolver(excel_viewresolver);
        registry.viewResolver(html_viewresolver);

        // 上面三个注册方法必须在此方法之上执行
        registry.enableContentNegotiation(false);
    }
}

3, test code

@Controller
@RequestMapping
public class HelloController {

    @GetMapping("/test/{type}")
    public String testContentNegotiation() {
        return "test ContentNegotiation";
    }
}

Order request: /test/a.pdf, /test/a.json, /test/a.html, /test/a(无后缀)respectively of page screenshot below (one to one)
Here Insert Picture Description
Here Insert Picture Description
Here Insert Picture Description
Here Insert Picture Description
using the AcceptDemo as follows:
Here Insert Picture Description
Here Insert Picture Description
Here Insert Picture Description
suffix higher priority than Accept, in line with our previous theoretical knowledge. If you do not specify a suffix, Acceptit will take effect.

Description: I am here because resolveViewName()it is given returns a view, equivalent can resolve any extension. so even if your extension does not exist it will be resolved eventually show up in html format. In the real scene is not so engaging

Also: this case there is an even easier test program -> No offer view resolvers, simply provide default view can be, you are interested can try their own small partners, deepen understanding.

to sum up

With the help of an example, explain the ContentNegotiatingViewResolverapplication of analytical content negotiation in terms of views, called to fill the remaining 20%content.
Although now view the back-end technology to use for a relatively small, but after all, thymeleafis still very good, as a full stack engineer, you also have reason to master a language template engine (skilled Vue、ReactWhen I did not say)

Related Reading

ContentNegotiation content negotiation mechanisms (a) --- Spring MVC 4 Zhong built-in support content negotiation [enjoy learning Spring MVC]
ContentNegotiation content negotiation (two) --- Spring MVC content negotiation the principle and learn to enjoy the custom configuration [Spring MVC]
ContentNegotiation content negotiation mechanisms (c) application --- in view view of: ContentNegotiatingViewResolver depth analysis [enjoy learning Spring MVC]

Knowledge Exchange

== The last: If you think this article helpful to you, may wish to point a praise chant. Of course, your circle of friends to share so that more small partners also are seeing 作者本人许可的~==

If interested in technology content can join the group wx exchange: Java高工、架构师3群.
If the group fails two-dimensional code, please add wx number: fsx641385712(or two-dimensional code is scanned beneath wx). And Note: "java入群"the word, will be invited into the group manually

== If for Spring, SpringBoot, MyBatis source code analysis and other interested can add me wx: fsx641385712, manually invite you into the group took off ==

Guess you like

Origin www.cnblogs.com/fangshixiang/p/11420824.html