SpringMVC源码(七)-View的多种实现

SpringMVC支持的视图有不少种,JSP的视图为JstlView,同时也支持其余模版:FreeMaker对应的视图为FreeMarkerView,Velocity对应的视图为VelocityView。另外还支持Excel及PDF的视图。java

在DispatcherServlet的核心处理doDispatch方法最后,视图的渲染由render方法执行。获取View对象,而后调用View对象的render方法完成数据绑定和视图渲染。git

咱们从View接口开始,深刻介绍下SpringMVC视图的实现过程。spring

1.View和AbstractView

View接口只定义了两个方法: getContentType和renderapache

public interface View {

	String getContentType();

	void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

}

getContentType由具体View来实现,而render方法则由AbstractView定义了模板实现。mvc

public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 合并全部的数据Model
	Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
	// 响应前准备
	prepareResponse(request, response);
	// 渲染视图并输出数据
	renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
	
}

createMergedOutputModel方法将全部须要暴露出去的数据合并成一个Map,prepareResponse方法执行响应前准备。renderMergedOutputModel是抽象方法,由不一样的子类本身实现视图的渲染和数据的输出。app

2.InternalResourceView

SpringMVC默认配置的视图解析器ViewResolver为InternalResourceViewResolver,实现了ViewResolver接口的resolveViewName方法,最终调用的是buildView方法,返回InternalResourceView。ide

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
	InternalResourceView view = (InternalResourceView) super.buildView(viewName);
	if (this.alwaysInclude != null) {
		view.setAlwaysInclude(this.alwaysInclude);
	}
	view.setPreventDispatchLoop(true);
	return view;
}

InternalResourceView做为JSP默认的视图,支持了正常的JSP请求响应。来看下其实现的renderMergedOutputModel方法。函数

protected void renderMergedOutputModel(
		Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

	// 将Model数据设置到request属性上
	exposeModelAsRequestAttributes(model, request);

	// 暴露帮助类做为request属性
	exposeHelpers(request);

	// 决定要响应的路径
	String dispatcherPath = prepareForRendering(request, response);

	// 得到目标资源的RequestDispatcher
	RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);

	// include操做
	if (useInclude(request, response)) {
		response.setContentType(getContentType());
		rd.include(request, response);
	}

	else {
		// 默认为forward操做
		rd.forward(request, response);
	}
}

将Model数据绑定到request属性上,就是遍历Model数据,而后调用request的setAttribute方法。注意,若是Model数据中的属性设置为null,则移除request对应的属性。工具

protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
	for (Map.Entry<String, Object> entry : model.entrySet()) {
		String modelName = entry.getKey();
		Object modelValue = entry.getValue();
		if (modelValue != null) {
			request.setAttribute(modelName, modelValue);
		}
		else {
			request.removeAttribute(modelName);
		}
	}
}

视图的渲染和数据的输出则是由Servlet的RequestDispatcher来执行。oop

JstlView是InternalResourceView的子类,实现了exposeHelpers,将JSTL相关属性绑定到request上。

InternalResourceView的用法很简单,只要经过ModelAndView设置ViewName便可。

@GetMapping("/example")
public ModelAndView index(){
	return new ModelAndView("example/example");
}

3.RedirectView

经常使用的response方式都是forward,但有时也会用到重定向操做。在SpringMVC中,使用重定向的方式由两种:

  • 使用RedirectView

  • 直接返回ViewName,加上redirect:前缀

    // RedirectView
      @GetMapping("/redirectView")
      public ModelAndView redirect(RedirectAttributes attrs){
          ModelAndView mav = new ModelAndView();
      	mav.setView(new RedirectView("example"));
      	return mav;
      }
    
      // redirect:
      @GetMapping("/redirect/prefix")
      public String redirectPrefix(){
      	return "redirect:example";
      }

须要注意的是重定向的路径会覆盖当前URL最后一个/后的路径,若是想替换前面的路径,须要使用..符号。

RedirectView的renderMergedOutputModel方法的实现,主要有两部分:重定向属性的保存和执行重定向。

protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
		HttpServletResponse response) throws IOException {

    // 获取重定向url
	String targetUrl = createTargetUrl(model, request);
	targetUrl = updateTargetUrl(targetUrl, model, request, response);

    // 使用FlashMap保存重定向的Model数据
	FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
	if (!CollectionUtils.isEmpty(flashMap)) {
		UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
		flashMap.setTargetRequestPath(uriComponents.getPath());
		flashMap.addTargetRequestParams(uriComponents.getQueryParams());
		FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
		if (flashMapManager == null) {
			throw new IllegalStateException("FlashMapManager not found despite output FlashMap having been set");
		}
		flashMapManager.saveOutputFlashMap(flashMap, request, response);
	}

    // 执行重定向
	sendRedirect(request, response, targetUrl, this.http10Compatible);
}

使用FlashMap的机制来保存重定向携带的Model数据,结合DispatcherServlet的doService方法中的FlashMap的获取和更新,完成重定向操做时Model数据的转移。

执行重定向时直接使用response的sendRedirect方法执行。

4.ExcelView

SpringMVC整合了POI对Excel的操做,定义了View实现类便捷地处理Excel。SpringMVC在4.2版本后,结合poi-ooxml使用流式函数支持office 2007的XLSX,不过须要poi的版本在3.9及以上。 能够直接引入poi-ooxml:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.14</version>
</dependency>

先使用@ModelAttribute定义一个数据Model,返回一个表格数据

@ModelAttribute
private void getUser(Model model){
	List<String> headers = new ArrayList<>();
	headers.add("Name");
	headers.add("Sex");
	headers.add("Age");
	
	List<User> users = new ArrayList<>();
	users.add(new User("Jack", "M", 20));
	users.add(new User("Emily", "W", 24));
	users.add(new User("Tom", "M", 23));
	
	Map<String, Object> data = new HashMap<>();
	data.put("header", headers);
	data.put("user", users);
	
	model.addAttribute("model", data);
}

表格输出的结果以下:

Name Sex Age
Jack M 20
Emily W 24
Tom M 23

实例化AbstractXlsxStreamingView内部类,完成Excel的操做,经过ModelAndView返回视图和数据。

@GetMapping("/excel")
public ModelAndView renderExcel(@ModelAttribute("model") Map<String, Object> model){
	AbstractXlsxStreamingView excelView = new AbstractXlsxStreamingView() {
		@SuppressWarnings("unchecked")
		@Override
		protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request,
				HttpServletResponse response) throws Exception {
			Sheet sheet = workbook.createSheet();
			
			// header
			Row headerRow = sheet.createRow(0);
			List<String> headerNames = (List<String>)model.get("header");
			for(int column=0; column<headerNames.size(); column++){
				headerRow.createCell(column).setCellValue(headerNames.get(column));
			}
			
			// body
			List<User> users = (List<User>)model.get("user");
			for(int row=1; row<users.size();row++){
				Row userRow = sheet.createRow(row);
				userRow.createCell(0).setCellValue(users.get(row).getName());
				userRow.createCell(1).setCellValue(users.get(row).getSex());
				userRow.createCell(2).setCellValue(users.get(row).getAge());
			}
			
			// set output file name
			response.setHeader(
                    "Content-disposition",
                    "attachment; filename=" + URLEncoder.encode("user", "utf-8")
                            + new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(new Date()) + ".xlsx");
		}
	};
	
	return new ModelAndView(excelView, model);
}

能够看到Excel的业务处理由用户自定义的buildExcelDocument方法完成。其实在AbstractXlsxStreamingView的父类AbstractXlsView中,将AbstractView的renderMergedOutputModel方法有进行了模板化。

protected final void renderMergedOutputModel(
		Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

	// 建立Excel的Workbook
	Workbook workbook = createWorkbook(model, request);

	// 将Excel的处理委托给子类
	buildExcelDocument(model, workbook, request, response);

	// 设置contentType为application/vnd.ms-excel
	response.setContentType(getContentType());

	// 将数据写到response中
	renderWorkbook(workbook, response);
}

5.PdfView

一样的方式,对PDF的输出,SpringMVC也结合了iText,提供了一个方便的View实现类AbstractPdfView。复用ExcelView中的Model数据,实现上面表格的PDF输出。

@GetMapping("/pdf")
public ModelAndView renderPdf(@ModelAttribute("model") Map<String, Object> model){
	AbstractPdfView pdfView = new AbstractPdfView() {
		@Override
		protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer,
				HttpServletRequest request, HttpServletResponse response) throws Exception {
			List<String> headerNames = (List<String>)model.get("header");
			List<User> users = (List<User>)model.get("user");
			
			document.newPage();
			PdfPTable table = new PdfPTable(3);
			table.setHeaderRows(0);
			
			// header
			for(int i=0;i<headerNames.size();i++){
				PdfPCell cell = new PdfPCell();
				cell.setBackgroundColor(Color.GRAY);
				cell.setPhrase(new Phrase(headerNames.get(i)));
				table.addCell(cell);
			}
			
			// body
			for(int row=0; row<users.size();row++){
				PdfPCell nameCell = new PdfPCell();
				nameCell.setPhrase(new Phrase(users.get(row).getName()));
				table.addCell(nameCell);
				
				PdfPCell sexCell = new PdfPCell();
				sexCell.setPhrase(new Phrase(users.get(row).getSex()));
				table.addCell(sexCell);
				
				PdfPCell ageCell = new PdfPCell();
				ageCell.setPhrase(new Phrase(users.get(row).getAge().toString()));
				table.addCell(ageCell);
			}
			
			document.add(table);
		}
	};
	
	return new ModelAndView(pdfView, model);
}

在AbstractPdfView中,也是将renderMergedOutputModel方法模板化。

protected final void renderMergedOutputModel(
		Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

	// IE workaround: write into byte array first.
	ByteArrayOutputStream baos = createTemporaryOutputStream();

	// 建立文档
	Document document = newDocument();
	PdfWriter writer = newWriter(document, baos);
	prepareWriter(model, writer, request);
	// 创建Pdf元数据,由子类实现
	buildPdfMetadata(model, document, request);

	// 处理Pdf内容
	document.open();
	buildPdfDocument(model, document, writer, request, response);
	document.close();

	// 将数据刷新到response
	writeToResponse(response, baos);
}

源码可见https://gitee.com/lntea/springmvc-demo/blob/master/src/main/java/com/lcifn/springmvc/controller/ViewController.java

以上咱们介绍了SpringMVC的View的多种实现,都是基于统一的模板,而后整合一些第三方工具提供了便捷的处理方式,也能够继承AbstractView自定义处理过程,来支持特殊的需求。但愿你们有所收获!

相关文章
相关标签/搜索