springMVC之ViewResolver与View

写在前面

本文阅读源码版本为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 SetterDefault ValueUnderlying StrategyEnabled Or Not中文释义
favorParameterfalseParameterContentNegotiationStrategyOff是否使用请求参数format来决定请求参数类型,要使此选项工作,需要通过mediaType方法注册format参数与MediaType间的映射
favorPathExtensionfalse(as of 5.3)PathExtensionContentNegotiationStrategyOff是否应该使用URL路径中的路径扩展来确定请求的媒体类型。已过时
ignoreAcceptHeaderfalseHeaderContentNegotiationStrategyEnabled是否禁用检查’Accept’请求头
defaultContentTypenullFixedContentNegotiationStrategyOff当没有请求内容类型时,使用的默认内容类型
defaultContentTypeStrategynullContentNegotiationStrategyOff当没有请求内容类型时,设置自定义的内容协商策略来决定内容类型

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中定义了如下属性,子类可以进行设置:

属性名称默认值释义
contentTypetext/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();
	}

通过上面源码阅读,当重定向需要传递数据时,我们就有两种方案可以选择:

  1. 使用URL模板以路径变量或查询参数的形式。
  2. 通过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";
    }
}

先写到这吧,知其然,知其所以然,学才不倦。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值