springboot学习12——springmvc(下)

五、数据模型

Spring MVC允许控制器自定义模型和视图(ModelAndView),其中模型是存放数据的地方,视图则是展示给用户。先来讨论数据模型的问题。
数据模型的作用是绑定数据,为后面的视图渲染做准备。首先看下Spring MVC使用的模型接口和类:
springboot学习12——springmvc(下)_第1张图片

在类ModelAndView中存在一个ModelMap类型的属性,ModelMap继承了LinkedHashMap类,所以它具备Map接口的一切特性,除此之外它还可以增加数据属性。
在Spring MVC的应用中,如果在控制器方法的参数中使用ModelAndView、Model或者ModelMap作为参数类型,Spring MVC会自动创建数据模型对象。

使用数据模型

@RequestMapping("/data")
@Controller
public class DataModelController {
    // 注入用户服务类
    @Autowired
    private UserService userService = null;

    // 测试Model接口
    @GetMapping("/model")
    public String useModel(Long id, Model model) {
        User user = userService.getUser(id);
        model.addAttribute("user", user);
        // 这里返回字符串,在Spring MVC中,会自动创建ModelAndView且绑定名称
        return "data/user";
    }

    // 测试modelMap类
    @GetMapping("/modelMap")
    public ModelAndView useModelMap(Long id, ModelMap modelMap) {
        User user = userService.getUser(id);
        ModelAndView mv = new ModelAndView();
        // 设置视图名称
        mv.setViewName("data/user");
        // 设置数据模型,此处modelMap并没有与mv绑定,这步系统会自动处理
        modelMap.put("user", user);
        return mv;
    }

    // 测试ModelAndView
    @GetMapping("/mav")
    public ModelAndView useModelAndView(Long id, ModelAndView mv) {
        User user = userService.getUser(id);
        // 设置数据模型
        mv.addObject("user", user);
		//设置视图名称
        mv.setViewName("data/user");
        return mv;
    }
}

useModel方法里,只是返回一个字符串,Spring MVC会自动生成对应的视图,并且绑定数据模型。
useModelMap方法,返回了ModelAndView对象,但是它没有绑定ModelMap对象,Spring MVC又会自动地绑定它。
上述数据对象,无论使用哪一个都是允许的。它们都是渲染同一个JSP视图,且该视图逻辑名称为/data/user,这样通过InternalResourceViewResolver的定位,它就会找到/WEB-INF/jsp/data/user.jsp作为视图,
然后将数据渲染到这个JSP上。

用户视图(/WEB-INF/jsp/data/user.jsp)

<%@ page language="java" contentType="text/html; charset=UTF-8"  
pageEncoding="UTF-8"%>




用户信息


    
编号 ${user.id}
用户名 ${user.userName}
备注 ${user.note}

这样就能够测试这些内容了。

六、视图和视图解析器

视图是渲染数据模型展示给用户的组件,在Spring MVC中又分为逻辑视图和非逻辑视图。逻辑视图是需要视图解析器(ViewResolver)进行进一步定位的。
例如,之前的例子所返回的字符串之所以能找到对应的JSP,就是因为使用了逻辑视图,经由视图解析器的定位后,才能找到视图将数据模型进行渲染展示给用户查看。
对于非逻辑视图,则并不需要进一步地定位视图的位置,它只需要直接将数据模型渲染出来即可。
在实际的工作中视图解析器InternalResourceViewResolver是比较常用的。

本节主要的任务是讨论Spring MVC中视图的使用,在使用视图之前,需要先了解在Spring MVC中视图是怎么设计的。

1.视图设计
对于视图,除了JSON和JSP视图之外,还有其他类型的视图,如Excel、PDF等。虽然视图具有多样性,但是它们都会实现Spring MVC定义的视图接口View。

Spring MVC视图接口定义

package org.springframework.web.servlet;
/**** imports ****/
public interface View {
    // 响应状态属性
    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";

    // 路径变量
    String PATH_VARIABLES = View.class.getName() + ".pathVariables";

    // 选择内容类型
    String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";

    // 响应类型
    String getContentType();

    // 渲染方法
    void render(Map model, HttpServletRequest request, 
        HttpServletResponse response) throws Exception;
}

其中getContentType方法是获取HTTP响应类型的,它可以返回的类型是文本、JSON数据集或者文件等,
而render方法则是将数据模型渲染到视图的,这是视图的核心方法,在它的参数中,model是数据模型,实际就是从控制器(或者由处理器自动绑定)返回的数据模型,
这样render方法就可以把它渲染出来。

渲染视图是比较复杂的过程,在Spring MVC中已经给开发者提供了许多开发好的视图类,所以在大部分的情况下并不需要自己开发自己的视图。Spring MVC所提供的视图接口和常用类:
springboot学习12——springmvc(下)_第2张图片

在Spring MVC中已经开发好了各种各样的视图,所以在大部分情况下,只需要定义如何将数据模型渲染到视图中展示给用户即可。例如,之前看到的MappingJackson2JsonView视图,因为它不是逻辑视图,所以并不需要使用视图解析器(ViewResolver)去定位视图,它会将数据模型渲染为JSON数据集展示给用户查看;而常用的视图JstlView,则是一个逻辑视图,于是可以在控制器返回一个字符串,使用视图解析器去定位对应的JSP文件,就能够找到对应的JSP文件,将数据模型传递进入,JstlView就会将数据模型渲染,展示数据给用户。

对于PDF和Excel视图等类型的视图,它们只需要接收数据模型,然后通过自定义的渲染即可。下面将介绍如何使用PDF视图——AbstractPdfView。

2.视图实例——导出PDF文件
AbstractPdfView属于非逻辑视图,因此它并不需要任何的视图解析器(ViewResolver)去定位。这个视图类的名称是以Abstract开头的,顾名思义它是一个抽象类,并且存在需要开发者自己实现的抽象方法,所以需要先来研究这个抽象方法。

AbstractPdfView文档生成抽象方法定义

/**
    * 通过数据模型自定义创建PDF文档
    * @param model 数据模型
    * @param document iText Document 代表一个PDF文档
    * @param writer PdfWriter PDF写入器
    * @param request HttpServletRequest请求对象
    * @param response HttpServletResponse响应对象
    * @throws Exception 异常
/
protected abstract void buildPdfDocument(Map model, 
    Document document, PdfWriter writer, HttpServletRequest request, 
    HttpServletResponse response) throws Exception;

通过PDF视图的定义,就只需要实现这个抽象方法便可以将数据模型渲染为PDF。方法中的参数,包含数据模型(model)对象、HTTP的请求(request)和响应(response)对象,通过这些就可以得到数据模型和上下文环境的参数,此外方法中还有与PDF文档有关的参数(document和writer),通过它们就可以定制PDF的格式和数据的渲染。

为了能够使用PDF,需要在Maven的配置文件中加入相关的依赖。
在pom.xml中加入PDF依赖


    org.xhtmlrenderer
    core-renderer
    R8


    com.itextpdf
    itextpdf
    5.5.12

因为AbstractPdfView是一个抽象类,在继承它后,就要实现其定义的抽象方法,从而完成导出的逻辑,而各个控制器都会有不同的导出逻辑。
为了适应不同控制器的自定义导出,这里先定义导出的接口。

定义PDF导出接口

/**
 * 定义pdf的导出接囗
 */
public interface PdfExportService {
    public void make(Map model, Document document,
                     PdfWriter writer, HttpServletRequest request,
                     HttpServletResponse response);
}

这样各个控制器只需要实现这个接口,就能够自定义其导出PDF的逻辑。
接着就是继承AbstractPdfView 的非抽象类,通过它调度PdfExportService的make方法就可以让控制器实现自定义的导出逻辑。

PDF导出视图类

public class PdfView extends AbstractPdfView {
    // 导出服务接口
    private PdfExportService pdfExportService = null;


    // 创建对象时载入导出服务接口
    public PdfView(PdfExportService pdfExportService){
        this.pdfExportService = pdfExportService;
    }

    // 调用接口实现
    @Override
    protected void buildPdfDocument(Map model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 调用导出服务接口类
        pdfExportService.make(model, document, writer, request, response);
    }

}

这里可以看到,在创建自定义PDF视图时,需要自定义一个导出服务接口(PdfExportService)。通过实现这个接口,每个控制器都可以自定义其导出的逻辑。

在用户控制器中导出PDF数据

@RequestMapping("/pdfController")
@Controller
public class PdfController {
    // 注入用户服务类
    @Autowired
    private MyBatisUserService userService = null;

    // 导出接口
    @GetMapping("/export/pdf")
    public ModelAndView exportPdf(Long id) {
        // 查询用户信息列表
        AppUserEntity user= userService.getUser(id);
        // 定义PDF视图
        View view = new PdfView(exportService());
        ModelAndView mv = new ModelAndView();
        // 设置视图
        mv.setView(view);
        // 加入数据模型
        mv.addObject("user", user);
        return mv;
    }

    // 导出PDF自定义
    @SuppressWarnings("unchecked")
    private PdfExportService exportService() {
        // 使用Lambda表达式定义自定义导出
        return (model, document, writer, request, response) -> {
            try {
                // A4纸张
                document.setPageSize(PageSize.A4);
                // 标题
                document.addTitle("用户信息");
                // 换行
                document.add(new Chunk("\n"));
                // 表格,3列
                PdfPTable table = new PdfPTable(3);
                // 单元格
                PdfPCell cell = null;
                // 字体,定义为蓝色加粗
                Font f8 = new Font();
                f8.setColor(Color.BLUE);
                f8.setStyle(Font.BOLD);
                // 标题
                cell = new PdfPCell(new Paragraph("id", f8));
                // 居中对齐
                cell.setHorizontalAlignment(1);
                // 将单元格加入表格
                table.addCell(cell);
                cell = new PdfPCell(new Paragraph("user_name", f8));
                // 居中对齐
                cell.setHorizontalAlignment(1);
                table.addCell(cell);
                cell = new PdfPCell(new Paragraph("phone", f8));
                cell.setHorizontalAlignment(1);
                table.addCell(cell);
                // 获取数据模型中的用户列表
                AppUserEntity user = (AppUserEntity) model.get("user");

                document.add(new Chunk("\n"));
                cell = new PdfPCell(new Paragraph(user.getId() + ""));table.addCell(cell);
                cell = new PdfPCell(new Paragraph(user.getName()));
                table.addCell(cell);
                String phone = user.getPhone() == null? "" : user.getPhone();
                cell = new PdfPCell(new Paragraph(phone));
                table.addCell(cell);
                // 在文档中加入表格
                document.add(table);
            } catch (DocumentException e) {
                e.printStackTrace();
            }
        };
    }
}

方法先通过查询后台数据得到用户列表,再放入模型和视图(ModelAndView)中,然后设置一个视图(PdfView)。
而定义PdfView时,使用Lambda表达式实现了导出服务接口,这样就可以很方便地让每一个控制器自定义样式和数据。

测试:http://localhost:8080/pdfController/export/pdf?id=1

七、文件上传

Spring MVC对文件上传提供了良好的支持,而在Spring Boot中可以更为简单地配置文件上传所需的内容。为了更好地理解Spring Boot的配置,首先从Spring MVC的机制谈起。

1.Spring MVC对文件上传的支持
首先,DispatcherServlet会使用适配器模式,将HttpServletRequest接口对象转换为MultipartHttp ServletRequest对象。
MultipartHttpServletRequest接口扩展了HttpServletRequest接口的所有方法,而且定义了一些操作文件的方法,这样通过这些方法就可以实现对上传文件的操作。
下面先探讨HttpServletRequest和MultipartHttpServletRequest的关系:
springboot学习12——springmvc(下)_第3张图片
文件请求转换类之间的关系

对于文件上传的场景,Spring MVC会将HttpServletRequest对象转化为MultipartHttpServletRequest对象。
从MultipartHttpServletRequest接口的定义看,它存在许多的方法用来处理文件,这样在Spring MVC中操作文件就十分便捷。

只是在使用Spring MVC上传文件时,还需要配置MultipartHttpServletRequest,这个任务是通过MultipartResolver接口实现的。对于MultipartResolver接口,它又存在两个实现类,这两个实现类分别是StandardServletMultipartResolver和CommonsMultipartResolver,可以使用它们中的任意一个来实现文件上传。

在默认的情况下Spring推荐使用的是StandardServletMultipartResolver,因为它只需要依赖于Servlet API提供的包,而对于CommonsMultipartResolver,则需要依赖于Apache提供的第三方包来实现,这显然没有StandardServletMultipartResolver来得实在。

从实用的角度来说,因为Spring 3.1之后已经能够支持StandardServletMultipartResolver,所以推荐使用它。
springboot学习12——springmvc(下)_第4张图片

MultipartResolver关系图
在Spring Boot的机制内,没有自定义MultipartResolver对象,会自动创建MultipartResolver对象,实际为StandardServletMultipartResolver。为了更加灵活,Spring Boot会提供如下配置项。

文件上传配置

# MULTIPART (MultipartProperties)
# 是否启用Spring MVC多分部上传功能
spring.servlet.multipart.enabled=true 
# 将文件写入磁盘的阈值。值可以使用后缀“MB”或“KB”来表示兆字节或字节大小
spring.servlet.multipart.file-size-threshold=0
# 指定默认上传的文件夹
spring.servlet.multipart.location=
# 限制单个文件最大大小
spring.servlet.multipart.max-file-size=1MB 
# 限制所有文件最大大小
spring.servlet.multipart.max-request-size=10MB 
# 是否延迟多部件文件请求的参数和文件的解析
spring.servlet.multipart.resolve-lazily=false

根据这些配置,Spring Boot会自动生成StandardServletMultipartResolver对象,这样就能够对上传的文件进行配置。
对于文件的上传可以使用Servlet API提供的Part接口或者Spring MVC提供的MultipartFile接口作为参数。其实无论使用哪个类都是允许的,更加推荐使用的是Part。

2.开发文件上传功能

Spring MVC上传文件配置

# 指定默认上传的文件夹
spring.servlet.multipart.location=D:/springboot
# 限制单个文件最大大小,这里设置为5MB
spring.servlet.multipart.max-file-size=5242880
# 限制所有文件最大大小,这里设置为20MB
spring.servlet.multipart.max-request-size=20MB

定义了上传的目标文件夹为D:/springboot,指定单个文件最大为5MB,所有文件最大为20MB。为了测试文件的上传,需要创建JSP文件。

文件上传JSP(/WEB-INF/jsp/file/upload.jsp)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>




文件上传

    
        

请注意,这里的表单声明为multipart/form-data,如果没有这个声明,Spring MVC就会解析文件请求出错,从而导致上传文件失败。
有了这个JSP文件,下面开发文件上传控制器(这个控制器将包括使用HttpServletRequest、MultipartFile和Part参数)来完成文件上传。

文件上传控制器

package com.springboot.chapter10.controller;
/**** imports ****/
@Controller
@RequestMapping("/file")
public class FileController {
    /**
     * 打开文件上传请求页面
     * @return 指向JSP的字符串
     */
    @GetMapping("/upload/page")
    public String uploadPage() {
        return "/file/upload";
    }

    // 使用HttpServletRequest作为参数
    @PostMapping("/upload/request")
    @ResponseBody
    public Map uploadRequest(HttpServletRequest request) {
        boolean flag = false;
        MultipartHttpServletRequest mreq = null;
        // 强制转换为MultipartHttpServletRequest接口对象
        if (request instanceof MultipartHttpServletRequest) {
            mreq = (MultipartHttpServletRequest) request;
        } else {
            return dealResultMap(false, "上传失败");
        }
        // 获取MultipartFile文件信息
        MultipartFile mf = mreq.getFile("file");
        // 获取源文件名称
        String fileName = mf.getOriginalFilename();
        File file = new File(fileName);
        try {
            // 保存文件
            mf.transferTo(file);
        } catch (Exception e) {
            e.printStackTrace();
            return dealResultMap(false, "上传失败");
        } 
        return dealResultMap(true, "上传成功");
    }

    // 使用Spring MVC的MultipartFile类作为参数
    @PostMapping("/upload/multipart")
    @ResponseBody
    public Map uploadMultipartFile(MultipartFile file) {
        String fileName = file.getOriginalFilename();
        File dest = new File(fileName);
        try {
            file.transferTo(dest);
        } catch (Exception e) {
            e.printStackTrace();
            return dealResultMap(false, "上传失败");
        } 
        return dealResultMap(true, "上传成功");
    }

    @PostMapping("/upload/part")
    @ResponseBody
    public Map uploadPart(Part file) {
		// 获取提交文件名称
        String fileName = file.getSubmittedFileName();
        try {
            // 写入文件
            file.write(fileName);
        } catch (Exception e) {
            e.printStackTrace();
            return dealResultMap(false, "上传失败");
        } 
        return dealResultMap(true, "上传成功");
    }

    // 处理上传文件结果
    private Map dealResultMap(boolean success, String msg) {
        Map result = new HashMap();
        result.put("success", success);
        result.put("msg", msg);
        return result;
    }
}

uploadRequest方法则将HttpServletRequest对象传递,在调用控制器之前,DispatcherServlet会将其转换为MultipartHttpServletRequest对象,所以方法中使用了强制转换,从而得到MultipartHttpServletRequest对象,然后获取MultipartFile对象,接着使用MultipartFile对象的getOriginalFilename方法就可以得到上传的文件名,而通过它的transferTo方法,就可以将文件保存到对应的路径中。

uploadMultipartFile则是直接使用MultipartFile对象获取上传的文件,从而进行操作。

uploadPart方法是使用Servlet的API,可以使用其write方法直接写入文件,这也是我推荐的方式。

测试:
http://localhost:8080/file/upload/request
http://localhost:8080/file//upload/multipart
http://localhost:8080/file/upload/part
请求头:Content-Type:multipart/form-data

你可能感兴趣的:(spring,springmvc,spring,boot)