SpringMVC支持的视图有很多种,JSP的视图为JstlView,同时也支持其他模版:FreeMaker对应的视图为FreeMarkerView,Velocity对应的视图为VelocityView。另外还支持Excel及PDF的视图。
在DispatcherServlet的核心处理doDispatch方法最后,视图的渲染由render方法执行。获取View对象,然后调用View对象的render方法完成数据绑定和视图渲染。
我们从View接口开始,深入介绍下SpringMVC视图的实现过程。
1.View和AbstractView
View接口只定义了两个方法: getContentType和render
public interface View {
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
getContentType由具体View来实现,而render方法则由AbstractView定义了模板实现。
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是抽象方法,由不同的子类自己实现视图的渲染和数据的输出。
2.InternalResourceView
SpringMVC默认配置的视图解析器ViewResolver为InternalResourceViewResolver,实现了ViewResolver接口的resolveViewName方法,最终调用的是buildView方法,返回InternalResourceView。
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来执行。
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);
}
以上我们介绍了SpringMVC的View的多种实现,都是基于统一的模板,然后整合一些第三方工具提供了便捷的处理方式,也可以继承AbstractView自定义处理过程,来支持特殊的需求。希望大家有所收获!