Skip to content

在SpringMVC中ViewResolver组件会将viewName解析成View对象,View对象再调用render完成结果的渲染。在上一篇已经完成了View的开发,本篇来完成ViewResolver研发。

我们主要开发两个视图的解析器InternalResourceViewResolverContentNegotiatingViewResolver

12.1 开发步骤讲解

本节源代码的分支:viewResolver

ViewResolver

public interface ViewResolver {
    View resolveViewName(String viewName) throws Exception;
}

首先我们来定义视图解析器的接口ViewResolver,因为ViewResolver组件的作用是将viewName解析成View对象,所以参数是viewName,处理完成后返回的对象是View

AbstractCachingViewResolver

因为启动一直一般会运行很长时间,很多用户都会请求同一个视图名称,为了避免每次都需要把viewName解析成View,所以我们需要做一层缓存,当有一次成功解析了viewName之后我们把返回的View缓存起来,下次直接先从缓存中取

public abstract class AbstractCachingViewResolver implements ViewResolver {
    private final Object lock = new Object();
    private static final View UNRESOLVED_VIEW = (model, request, response) -> {
    };
    private Map<String, View> cachedViews = new HashMap<>();

    @Override
    public View resolveViewName(String viewName) throws Exception {
        View view = cachedViews.get(viewName);
        if (Objects.nonNull(view)) {
            return (view != UNRESOLVED_VIEW ? view : null);
        }

        synchronized (lock) {
            view = cachedViews.get(viewName);
            if (Objects.nonNull(view)) {
                return (view != UNRESOLVED_VIEW ? view : null);
            }

            view = createView(viewName);
            if (Objects.isNull(view)) {
                view = UNRESOLVED_VIEW;
            }
            cachedViews.put(viewName, view);
        }
        return (view != UNRESOLVED_VIEW ? view : null);
    }

    protected abstract View createView(String viewName);

}
  1. 定义一个默认的空视图UNRESOLVED_VIEW,当通过viewName解析不到视图返回null时,把默认的视图放入到缓存中
  2. 由于可能存在同一时刻多个用户请求到同一个视图,所以需要使用synchronized加锁
  3. 如果缓存中获取到的视图是UNRESOLVED_VIEW,那么就返回null

UrlBasedViewResolver

public abstract class UrlBasedViewResolver extends AbstractCachingViewResolver {
    public static final String REDIRECT_URL_PREFIX = "redirect:";
    public static final String FORWARD_URL_PREFIX = "forward:";

    private String prefix = "";
    private String suffix = "";


    @Override
    protected View createView(String viewName) {
        if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
            String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
            return new RedirectView(redirectUrl);
        }

        if (viewName.startsWith(FORWARD_URL_PREFIX)) {
            String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
            return new InternalResourceView(forwardUrl);
        }

        return buildView(viewName);
    }

    protected abstract View buildView(String viewName);

    //getter setter省略
}
  1. 当viewName以redirect:开头,那么返回RedirectView视图
  2. 当viewName以forward:开头,那么返回InternalResourceView视图
  3. 如果都不是,那么就执行模板方法buildView

InternalResourceViewResolver

public class InternalResourceViewResolver extends UrlBasedViewResolver {
    @Override
    protected View buildView(String viewName) {
        String url = getPrefix() + viewName + getSuffix();
        return new InternalResourceView(url);
    }
}

实现了UrlBasedViewResolver中的模板方法buildView,拼接了url的前缀和后缀,返回视图InternalResourceView

ContentNegotiatingViewResolver

视图协同器ContentNegotiatingViewResolver定义了所有ViewResolver以及默认支持的View,当接收到用户的请求后根据头信息中的Accept匹配出最优的视图

public class ContentNegotiatingViewResolver implements ViewResolver, InitializingBean {
    private List<ViewResolver> viewResolvers;
    private List<View> defaultViews;

    @Override
    public View resolveViewName(String viewName) throws Exception {
        List<View> candidateViews = getCandidateViews(viewName);
        View bestView = getBestView(candidateViews);
        if(Objects.nonNull(bestView)){
            return bestView;
        }
        return null;
    }

    /**
     * 根据请求找出最优视图
     *
     * @param candidateViews
     * @return
     */
    private View getBestView(List<View> candidateViews) {
        Optional<View> viewOptional = candidateViews.stream()
                .filter(view -> view instanceof RedirectView)
                .findAny();
        if (viewOptional.isPresent()) {
            return viewOptional.get();
        }

        HttpServletRequest request = RequestContextHolder.getRequest();
        Enumeration<String> acceptHeaders = request.getHeaders("Accept");
        while (acceptHeaders.hasMoreElements()) {
            for (View view : candidateViews) {
                if (acceptHeaders.nextElement().equals(view.getContentType())) {
                    return view;
                }
            }
        }
        return null;
    }

    /**
     * 先找出所有候选视图
     *
     * @param viewName
     * @return
     * @throws Exception
     */
    private List<View> getCandidateViews(String viewName) throws Exception {
        List<View> candidateViews = new ArrayList<>();
        for (ViewResolver viewResolver : viewResolvers) {
            View view = viewResolver.resolveViewName(viewName);
            if (Objects.nonNull(view)) {
                candidateViews.add(view);
            }
        }
        if (!CollectionUtils.isEmpty(defaultViews)) {
            candidateViews.addAll(defaultViews);
        }
        return candidateViews;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(viewResolvers, "viewResolvers can not null");
    }

    //getter setter 省略
}
  1. getCandidateViews: 通过视图名字使用ViewResolver解析出所有不为null的视图,如果默认视图不为空,把所有视图返回作为候选视图
  2. getBestView: 从request中拿出头信息Accept,根据视图的ContentType从候选视图中匹配出最优的视图返回

在这里我们还使用到了一个工具类RequestContextHolder,在当前线程中存放了当前请求的HttpServletRequest

public abstract class RequestContextHolder {
    private static final ThreadLocal<HttpServletRequest> inheritableRequestHolder =
            new NamedInheritableThreadLocal<>("Request context");

    /**
     * Reset the HttpServletRequest for the current thread.
     */
    public static void resetRequest() {
        inheritableRequestHolder.remove();
    }

    public static void setRequest(HttpServletRequest request) {
        inheritableRequestHolder.set(request);
    }

    public static HttpServletRequest getRequest() {
        return inheritableRequestHolder.get();
    }
}

12.2 单元测试

到此本篇所有的视图解析器都已经完成,本篇的单元测试我们主要测试ContentNegotiatingViewResolver,检查能否正确的返回视图对象

@Test
public void resolveViewName() throws Exception {
    ContentNegotiatingViewResolver negotiatingViewResolver = new ContentNegotiatingViewResolver();
    negotiatingViewResolver.setViewResolvers(Collections.singletonList(new InternalResourceViewResolver()));

    MockHttpServletRequest request = new MockHttpServletRequest();
    request.addHeader("Accept", "text/html");
    RequestContextHolder.setRequest(request);

    View redirectView = negotiatingViewResolver.resolveViewName("redirect:/silently9527.cn");
    Assert.assertTrue(redirectView instanceof RedirectView); //判断是否返回重定向视图

    View forwardView = negotiatingViewResolver.resolveViewName("forward:/silently9527.cn");
    Assert.assertTrue(forwardView instanceof InternalResourceView); //

    View view = negotiatingViewResolver.resolveViewName("/silently9527.cn");
    Assert.assertTrue(view instanceof InternalResourceView); //通过头信息`Accept`,判断是否返回的`InternalResourceView`

}

执行的结果如下:

12.3 总结

本篇我们完成了ViewResolver,相信大家对springmvc的视图解析过程也有了一定的了解,下篇我们将开始研发DispatcherServlet,把我们之前开发完成的HandlerMapping、HandlerAdapter等组件串联起来使用。

12.4 延展

本篇完成后可以对照着去springmvc中的视图解析器,比如:ContentNegotiatingViewResolverBeanNameViewResolverXmlViewResolver等,特别是ContentNegotiatingViewResolver,我们自己实现的是简版,springmvc的支持头信息,url后缀等方法。