写在前面
本文阅读源码版本为spring5.3.1。
了解ViewResolver
ViewResolver的作用是根据处理器返回的ModelAndView中的逻辑视图名,为DispatchServlet返回一个可用的View实例。下面是ViewResolver接口
public interface ViewResolver {
/**
* @param 待解析的逻辑视图名
* @param 根据不同的locale返回不同的视图,这对于支持国际化的视图是必要的
*/
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
我们再来看一下ViewResolver类结构关系图,这里我将分成两块来讲,一块是继承AbstractCaching-ViewResolver,另一块则没有。先来看看这几个比较特殊的。
- BeanNameViewResolver
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = obtainApplicationContext();
if (!context.containsBean(viewName)) {
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Found bean named '" + viewName + "' but it does not implement View");
}
return null;
}
// 找到一个beanName为viewName的View实例
return context.getBean(viewName, View.class);
}
- ViewResolverComposite
ViewResolverComposite是ViewResolver组合模式的具体实现,它不负责解析视图,而是交给内部持有的ViewResolver集合来处理。默认优先级最低。 - ContentNegotiatingViewResolver
它也不负责解析视图,但比起ViewResolverComposite,它内部具体处理逻辑要复杂一点。默认最高的优先级。
// 它在初始化servlet容器的时候会去applicationContext上下文中查找所有的ViewResolver实例
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
// 获取客户端要求的媒体类型,默认获取请求头中Accept信息
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;
}
}
String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
" given " + requestedMediaTypes.toString() : "";
// 未找到客户端要求类型的视图,默认返回406的状态码
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
}
return NOT_ACCEPTABLE_VIEW;
}else {
logger.debug("View remains unresolved" + mediaTypeInfo);
return null;
}
}
protected List<MediaType> getMediaTypes(HttpServletRequest request) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
try {
ServletWebRequest webRequest = new ServletWebRequest(request);
// 这里我们可以设置内容协商策略来获取对应的内容类型
List<MediaType> acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request);
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
for (MediaType acceptable : acceptableMediaTypes) {
for (MediaType producible : producibleMediaTypes) {
// 此MediaType是否与给定的媒体类型兼容。
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
}
}
}
List<MediaType> selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
return selectedMediaTypes;
}catch (HttpMediaTypeNotAcceptableException ex) {
if (logger.isDebugEnabled()) {
logger.debug(ex.getMessage());
}
return null;
}
}
使用这个视图解析器的时候不建议直接配置,springBoot中可以通过实现WebMvcConfigurer接口覆写configureContentNegotiation方法进行配置。
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType (MediaType.APPLICATION_JSON);
}
@Bean
public ViewResolver cnViewResolver(ContentNegotiationManager manager){
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver ();
viewResolver.setContentNegotiationManager (manager);
return viewResolver;
}
}
springMvc中可以通过配置ContentNegotiationManagerFactoryBean。
<bean id="contentNegotiationConfigurer" class="org.springframework.web.
accept.ContentNegotiationManagerFactoryBean" p:defaultContentType="application/json"/>
<bean id="cnViewResolver" class="org.springframework.web.
servlet.view.ContentNegotiatingViewResolver" >
<property name="contentNegotiationManager">
<ref bean="contentNegotiationConfigurer"/>
</property>
</bean>
其实ContentNegotiationConfigurer配置类内部使用到的仍然是ContentNegotiationManagerFactory-Bean,它提供了一个属性参数设置列表,供大家参考。
Property Setter | Default Value | Underlying Strategy | Enabled Or Not | 中文释义 |
---|---|---|---|---|
favorParameter | false | ParameterContentNegotiationStrategy | Off | 是否使用请求参数format来决定请求参数类型,要使此选项工作,需要通过mediaType方法注册format参数与MediaType间的映射 |
favorPathExtension | false(as of 5.3) | Off | 是否应该使用URL路径中的路径扩展来确定请求的媒体类型。已过时 | |
ignoreAcceptHeader | false | HeaderContentNegotiationStrategy | Enabled | 是否禁用检查’Accept’请求头 |
defaultContentType | null | FixedContentNegotiationStrategy | Off | 当没有请求内容类型时,使用的默认内容类型 |
defaultContentTypeStrategy | null | ContentNegotiationStrategy | Off | 当没有请求内容类型时,设置自定义的内容协商策略来决定内容类型 |
5.0之后也可以通过strategies方法直接设置内容协商策略。关于favorPathExtension属性在springBoot中WebMvcProperties里面默认为false的。目前我看ContentNegotiationManagerFactoryBean中favor-PathExtension仍为true,但是对应方法上面的注释说已修改成false,给人造成混淆,这个问题有人已经提出来了,可能会在下一个版本中进行修正。
关于可能存在诸如RFD之类的潜在攻击,大家想了解RFD可以看这篇博客。
demo
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType (MediaType.APPLICATION_JSON).favorParameter (true).ignoreAcceptHeader (true);
}
@Bean
public ViewResolver myViewResolver(ContentNegotiationManager manager){
ContentNegotiatingViewResolver viewResolver = new ContentNegotiatingViewResolver ();
viewResolver.setContentNegotiationManager (manager);
return viewResolver;
}
}
@Component
public class JsonViewResolver implements ViewResolver {
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
MappingJackson2JsonView view = new MappingJackson2JsonView();
view.setPrettyPrint(true);
return view;
}
}
@Component
public class XmlResolver implements ViewResolver {
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
MappingJackson2XmlView view = new MappingJackson2XmlView ();
view.setPrettyPrint(true);
return view;
}
}
@Controller
@RequestMapping("/view")
public class ViewController {
// 可以通过访问http://localhost:8080/view/test3?format=json或xml形式分别展示json或者xml
@RequestMapping("/test3")
public User test3(Model model){
User user = new User ();
return user;
}
}
接下来展示另一块,它们全都继承AbstractCachingViewResolver,这个类提供了缓存的功能,不会针对每次请求重新实例化View对象。而这其中还分为面向单一视图类型与面向多类型的ViewResolver,不过5.3后面向多类型的ViewResolver已经废弃了。面向单一类型的这些ViewResolver类结构和下面的图类似。
它们都直接或间接继承于UrlBasedViewResolver,我们先梳理一下解析视图的主要逻辑:从缓存中找,找不到则创建对应的View实例,而加载视图这块的逻辑就位于UrlBasedViewResolver中。
protected View createView(String viewName, Locale locale) throws Exception {
// 如果这个解析器不应该处理给定的视图,返回null传递给链中的下一个解析器。
// 可以设定某个解析器只解析某几个视图,通过setViewNames
if (!canHandle(viewName, locale)) {
return null;
}
// 检查“redirect:”前缀。
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
// 这里isRedirectContextRelative(),isRedirectHttp10Compatible()默认为true
// RedirectView中contextRelative默认是flase
RedirectView view = new RedirectView(redirectUrl,
isRedirectContextRelative(), isRedirectHttp10Compatible());
String[] hosts = getRedirectHosts();
if (hosts != null) {
view.setHosts(hosts);
}
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}
// 检查“forward:”前缀。
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}
// 否则回到超类,最后调用loadView。
return super.createView(viewName, locale);
}
protected View loadView(String viewName, Locale locale) throws Exception {
// 构建View
AbstractUrlBasedView view = buildView(viewName);
// 应用包含ApplicationContext的生命周期方法
View result = applyLifecycleMethods(viewName, view);
return (view.checkResource(locale) ? result : null);
}
// 默认实现,子类可以覆盖
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
AbstractUrlBasedView view = instantiateView();
view.setUrl(getPrefix() + viewName + getSuffix());
view.setAttributesMap(getAttributesMap());
String contentType = getContentType();
if (contentType != null) {
view.setContentType(contentType);
}
String requestContextAttribute = getRequestContextAttribute();
if (requestContextAttribute != null) {
view.setRequestContextAttribute(requestContextAttribute);
}
Boolean exposePathVariables = getExposePathVariables();
if (exposePathVariables != null) {
view.setExposePathVariables(exposePathVariables);
}
Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
if (exposeContextBeansAsAttributes != null) {
view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
}
String[] exposedContextBeanNames = getExposedContextBeanNames();
if (exposedContextBeanNames != null) {
view.setExposedContextBeanNames(exposedContextBeanNames);
}
return view;
}
我们再来看一下AbstractTemplateViewResolver都干了什么。
/**
* @author
* @Description: 提供了一种方便的方法来指定AbstractTemplateView的暴露请求属性、会话属性和Spring的宏助手的标志。
* @Date 2020/12/8 11:03
*/
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
AbstractTemplateView view = (AbstractTemplateView) super.buildView(viewName);
view.setExposeRequestAttributes(this.exposeRequestAttributes);
view.setAllowRequestOverride(this.allowRequestOverride);
view.setExposeSessionAttributes(this.exposeSessionAttributes);
view.setAllowSessionOverride(this.allowSessionOverride);
view.setExposeSpringMacroHelpers(this.exposeSpringMacroHelpers);
return view;
}
了解View
public interface View {
// 在实际渲染尝试之前可以用来视图的内容类型
default String getContentType() {
return null;
}
//呈现指定模型的视图。
void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception;
}
各种View实现类的主要职责就是在render方法中实现最终的视图渲染,但这些对DispatchServlet是透明的,DispatchServlet只要接收到View实例(至于这些View是通过ViewResolver解析出来的,还是自定义的,这不重要),然后把视图渲染工作交给View即可。
DispatchServlet:
// 当我们的请求方法并没有返回String类型的视图名称时,这个方法会设置默认的视图名称
private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
if (mv != null && !mv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
mv.setViewName(defaultViewName);
}
}
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
//...
View view;
// 从ModelAndView中获取视图名称
String viewName = mv.getViewName();
if (viewName != null) {
// 不为空则交给视图解析器去解析
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}else {
// 为空的时候直接从ModelAndView中获取
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() + "'");
}
}
//视图渲染...
}
上面的代码告诉了我们一件事,我们可以返回string视图名称交给ViewResolver去解析,也可以直接返回包含了View的ModelAndView 。
可用的View实现类
通过查看View类继承结构图,可以发现所有的View实现类要么继承于AbstractView,要么是Smart-View,而SmartView是一个标志接口,只是判断该View是否需要重定向而已,所以我们只要关注AbstractView的主要逻辑就好了。
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 合并所有模型数据
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
// 当视图需要生成下载内容时,需要设置响应头
prepareResponse(request, response);
// 渲染,留给子类拓展
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model,
HttpServletRequest request, HttpServletResponse response) {
// 是否暴露pathVariables
Map<String, Object> pathVars = (this.exposePathVariables ?
(Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null);
// 合并静态和动态模型属性。
int size = this.staticAttributes.size();
size += (model != null ? model.size() : 0);
size += (pathVars != null ? pathVars.size() : 0);
Map<String, Object> mergedModel = CollectionUtils.newLinkedHashMap(size);
mergedModel.putAll(this.staticAttributes);
if (pathVars != null) {
mergedModel.putAll(pathVars);
}
if (model != null) {
mergedModel.putAll(model);
}
// 暴露RequestContext
if (this.requestContextAttribute != null) {
mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
}
return mergedModel;
}
AbstractView中定义了如下属性,子类可以进行设置:
属性名称 | 默认值 | 释义 |
---|---|---|
contentType | text/html;charset=ISO-8859-1 | |
requestContextAttribute | 空 | 设置了这个属性后,页面可以使用该名称引用到RequestContext |
attributesCSV | 空 | 静态属性,设置方式:aa={xx},bb={xxx} |
attributes | 空 | 静态属性,以Properties的方式传入静态属性 |
attributesMap | 空 | 静态属性,以Map的方式传入静态属性 |
继承AbstractView的实现类虽然有很多,但我们只会用到其中很少的一部分而已,这里先聊一下RedirectView,我们返回"redirect:xxx"时,最后构建的视图类型就是它,我们来看一下它的源码。
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws IOException {
// 获取请求url
String targetUrl = createTargetUrl(model, request);
// 看容器中有没有配置RequestDataValueProcessor,有就将url交由它处理
targetUrl = updateTargetUrl(targetUrl, model, request, response);
// Save flash attributes,可以通过RedirectAttributes#addFlashAttribute添加
RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);
// Redirect
sendRedirect(request, response, targetUrl, this.http10Compatible);
}
protected final String createTargetUrl(Map<String, Object> model, HttpServletRequest request)
throws UnsupportedEncodingException {
StringBuilder targetUrl = new StringBuilder();
String url = getUrl();
Assert.state(url != null, "'url' not set");
// url以“/”打头并且contextRelative为true,虽然contextRelative默认为false,但是
// UrlBasedViewResolver中构建RedirectView时默认设置为true的
if (this.contextRelative && getUrl().startsWith("/")) {
// Do not apply context path to relative URLs.将上下文路径应用到url上
targetUrl.append(getContextPath(request));
}
targetUrl.append(getUrl());
// 编码
String enc = this.encodingScheme;
if (enc == null) {
enc = request.getCharacterEncoding();
}
if (enc == null) {
// 默认ISO-8859-1
enc = WebUtils.DEFAULT_CHARACTER_ENCODING;
}
// 是否将重定向URL视为URI模板。expandUriTemplateVariables默认为true
if (this.expandUriTemplateVariables && StringUtils.hasText(targetUrl)) {
Map<String, String> variables = getCurrentRequestUriVariables(request);
// 替换Uri模板变量
targetUrl = replaceUriTemplateVariables(targetUrl.toString(), model, variables, enc);
}
// 是否传播当前URL的查询参数。默认为false
if (isPropagateQueryProperties()) {
// 将当前请求的查询字符串追加到目标重定向URL。
appendCurrentQueryParams(targetUrl, request);
}
// 是否暴露Model属性,默认为true
if (this.exposeModelAttributes) {
// 将模型属性字符串化、url编码和格式化为查询属性添加到url中,Model中设置的Value
// 要满足BeanUtils#isSimpleValueType才会加入url中
// url有关#的作用请参考这篇博客http://blog.sina.com.cn/s/blog_6d3a29310100w67y.html
appendQueryProperties(targetUrl, model, enc);
}
return targetUrl.toString();
}
通过上面源码阅读,当重定向需要传递数据时,我们就有两种方案可以选择:
- 使用URL模板以路径变量或查询参数的形式。
- 通过flase属性发送数据,这中方式可以传递对象。
其他的View基本上我都没有使用过,这里就选了几个入手简单的View写了个demo。
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// 使用BeanNameViewResolver
registry.beanName();
}
}
<!-- Pdf library 这个不支持中文,官方建议使用它的分支openpdf-->
<!--<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>-->
<!-- Pdf library 这个版本把包名改了,和AbstractPdfView/AbstractPdfStamperView声明类型对不上-->
<!--<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>-->
<!--<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>-->
<!-- Pdf library https://github.com/LibrePDF/OpenPDF/wiki,这个提供的文档还没有完成,大家可以参考-->
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.23</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
@Component
public class PdfReportView extends AbstractPdfView {
// 定义全局的字体静态变量
private static Font textfont;
static{
// 不同字体(这里定义为同一种字体:包含不同字号、不同style)
BaseFont bfChinese = null;
try {
bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
} catch (Exception e) {
e.printStackTrace ();
}
textfont = new Font(bfChinese, 10, Font.NORMAL);
}
@Override
protected void buildPdfDocument(Map<String, Object> map, Document document, PdfWriter pdfWriter, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
// PDF 标题
List<String> titles = (List<String>) map.get ("title");
Table table = new Table (titles.size ());
table.setPadding(5);
table.setSpacing(5);
if(!CollectionUtils.isEmpty (titles)){
for (String title : titles) {
table.addCell (new Phrase (title, textfont));
}
}
// PDF 内容
List<User> contents = (List<User>) map.get ("content");
if(!CollectionUtils.isEmpty (contents)){
for (User user : contents) {
table.addCell (new Phrase (user.getName (), textfont));
table.addCell (new Phrase (user.getSex (), textfont));
}
}
document.add(table);
}
@Component
public class XlsxView extends AbstractXlsxView {
@Override
protected void buildExcelDocument(Map<String, Object> map, Workbook workbook, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
String excelName = map.get("name").toString() + ".xlsx";
excelName = URLEncoder.encode (excelName, "utf-8");
httpServletResponse.setHeader("Content-Disposition", "attachment;filename=" + excelName);
List<String> titles = (List<String>) map.get ("title");
Sheet sheet = workbook.createSheet("User Detail");
sheet.setDefaultColumnWidth(30);
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setFontName("Arial");
style.setFillForegroundColor(HSSFColor.HSSFColorPredefined.BLUE.getIndex ());
style.setFillPattern(FillPatternType.forInt (1));
font.setBold(true);
font.setColor(HSSFColor.HSSFColorPredefined.WHITE.getIndex ());
style.setFont(font);
Row header = sheet.createRow(0);
ForEachUtils.forEach (0,titles,(index,title) -> {
header.createCell(index).setCellValue(title);
});
List<User> contents = (List<User>) map.get ("content");
int rowCount = 1;
for (User user : contents) {
Row userRow = sheet.createRow(rowCount++);
userRow.createCell(0).setCellValue(user.getName());
userRow.createCell(1).setCellValue(user.getSex ());
}
}
}
@Controller
@RequestMapping("/view")
public class ViewController {
/**
* @author
* @Description: 下面两个不使用ViewResolver也能渲染视图
*/
@RequestMapping("/test1")
public ModelAndView test1(Model model){
model.addAttribute ("user",new User ());
MappingJackson2JsonView jsonView = new MappingJackson2JsonView ();
ModelAndView modelAndView = new ModelAndView ();
modelAndView.setView (jsonView);
return modelAndView;
}
@RequestMapping("/test2")
public ModelAndView test2(Model model){
model.addAttribute ("user",new User ());
MappingJackson2XmlView xmlView = new MappingJackson2XmlView ();
ModelAndView modelAndView = new ModelAndView ();
modelAndView.setView (xmlView);
return modelAndView;
}
/**
* @author
* @Description: 下面两个使用BeanNameViewResolver解析视图
*/
@RequestMapping("/testPdf")
public String testPdf(Model model) {
model.addAttribute("title", Arrays.asList ("姓名","性别"));
model.addAttribute("content", Arrays.asList (new User ("夏油","男")
,new User ("五条悟","男")));
return "pdfReportView";
}
@RequestMapping("/testExcel")
public String testExcel(Model model) {
model.addAttribute("name", "咒术回转");
model.addAttribute("title", Arrays.asList ("姓名","性别"));
model.addAttribute("content", Arrays.asList (new User ("夏油","男")
,new User ("五条悟","男")));
return "xlsxView";
}
}
先写到这吧,知其然,知其所以然,学才不倦。