之前面试被问到两次的问题,其中一次还是网龙面试时被问到的。对于这个问题,其实答案很简单,往下看前可以先思考一下。本文会先给出简洁明了的答案,具体的分析根据兴趣选择阅读。
先简单的描述一下spring mvc的工作过程:客户端请求发送到前端控制器(DispatchServlet),由前端控制器通过用户配置HandlerMapping将请求映射到业务处理器(Controller)处理业务,并且返回ModelAndView对象,再由ViewResolver解析视图名称为具体的View,由View对传进来的Model进行渲染,返回信息到客户端。
先简单的描述一下spring mvc的工作过程:客户端请求发送到前端控制器(DispatchServlet),由前端控制器通过用户配置HandlerMapping将请求映射到业务处理器(Controller)处理业务,并且返回ModelAndView对象,再由ViewResolver解析视图名称为具体的View,由View对传进来的Model进行渲染,返回信息到客户端。
面试题:前端控制器如何渲染JSP返回给客户端?
答案分割线---------------------------------------------
众所周知的是JSP会被编译成Servlet运行,这个是解这道题的基础。前端控制器得到View对象和Model对象后,调用View对象的render方法进行渲染,将Model(业务处理器处理的结果信息)设值到Request对象上,调用请求分发器(RequestDispatch)进行分发(forward)到JSP。
具体分析分割线----------------------------------------
DispatchServlet根据ViewResolver将视图名称解析为具体的视图对象View。DispatchServlet.render()方法代码:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale = this.localeResolver.resolveLocale(request);
response.setLocale(locale);
View view;
if (mv.isReference()) {
// We need to resolve the view name.
view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException(
"Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" +
getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
view.render(mv.getModelInternal(), request, response);
}
以上过程是DispatchServlet通过解析得到view对象,并通过调用view对象的render方法来完成数据的显示过程。接下来来了解一下如何通过解析视图的逻辑名得到视图对象的。
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
HttpServletRequest request) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
解析过程就是调用viewResolver进行解析,看一下常见的BeanNameViewResolver:
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = getApplicationContext();
if (!context.containsBean(viewName)) {
// Allow for ViewResolver chaining.
return null;
}
return context.getBean(viewName, View.class);
}
首先取得当前IOC容器,然后判断在IOC容器是否包含有指导名称的Bean,如果有则通过getBean获取。
ViewResolver还有其他的实现,比如AbstractCachingViewResolver,处理过程会对view对象进行缓存。
ViewResolver还有其他的实现,比如AbstractCachingViewResolver,处理过程会对view对象进行缓存。
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
// Ask the subclass to create the View object.
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
if (logger.isTraceEnabled()) {
logger.trace("Cached view [" + cacheKey + "]");
}
}
}
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
createView是一个模板方法,具体的实现有子类完成。
回到主题JSP视图的实现,Spring mvc中使用JstlView作为view对象。render方法在其基类AbstractView中实现。
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
" and static attributes " + this.staticAttributes);
}
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
renderMergedOutputModel(mergedModel, request, response);
}
将信息都放入到mergedModel中,如何在prepareResponse方法中设置Response的一些基本参数。最后调用renderMergedOutputModel方法,这个方法在InternalResourceView中找到。
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest requestToExpose = getRequestToExpose(request);
exposeModelAsRequestAttributes(model, requestToExpose);
exposeHelpers(requestToExpose);
String dispatcherPath = prepareForRendering(requestToExpose, response);
RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
if (useInclude(requestToExpose, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(requestToExpose, response);
}
else {
exposeForwardRequestAttributes(requestToExpose);
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(requestToExpose, response);
}
}
这个方法做了一些前置处理后,获得请求分发器,然后判断是否是使用include,如果不是则使用forward跳转到JSP。