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自定義處理過程,來支持特殊的需求。但願你們有所收穫!

相關文章
相關標籤/搜索