SpringMVC详解(四)

SpringMVC详解(四)

视图和视图解析器

视图是渲染数据模型展示给用户的组件,在SpringMVC中又分为逻辑视图和非逻辑视图。逻辑图是需要视图解析器(ViewResolver)进行进一步定位的。

现在我们来看看视图是如何设计和使用的。

视图设计

视图的类型有很多,比如前面的HTML页面就是最为经典的视图,但是也有很多其他的视图,比如JSON视图,JSP视图,当然不止仅有网络视图,连Excel、PDF也算作为一种视图。

视图都会现实SpringMVC中的View接口:

1
2
3
4
5
6
7
8
9
10
11
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<String,?>model,HttpServletRequest request,HttpServletResponse response)throws Exception;

这段代码中有两个方法,其中getContentType方法是获取HTTP响应类型的,它可以返回的类型是文本、JSON数据集或者文件等,而rend巳r方法则是将数据模型渲染到视图的,这是视图的核心方法,所以有必要进一步地讨论它。

在它的参数中,model是数据模型,实际就是从控制器(或者由处理器自动绑定)返回的数据模型,这样render方法就可以把它渲染出来。

渲染视图是比较复杂的过程,为了简化视图渲染的开发,在SpringMVC中已经给开发者提供了许多开发好的视图类,所以在大部分的情况下并不需要自己开发自己的视图。

如图所示,SpringMVC中己经开发好了各种各样的视图,所以在大部分情况下,只需要定义如何将数据模型渲染到视图中展示给用户即可。

例如,之前看到的MappingJackson2JsonView视图,因为它不是逻辑视图,所以并不需要使用视图解析器(ViewResolver)去定位视图,它会将数据模型渲染为JSON数据集展示给用户查看;而常用的视图JstlView,则是一个逻辑视图,于是可以在控制器返回一个字符串,使用视图解析器去定位对应的JSP文件,就能够找到对应的JSP文件,将数据模型传递进入,Jst!View就会将数据模型渲染,展示数据给用户。对于PDF和Excel视图等类型的视图,它们只需要接收数据模型,然后通过自定义的渲染即可。为了说明视图的使用方法,下面将介绍如何使用PDF视图——AbstractPdtView。

视图实例——导出PDF文件

可以从字面意思上看出,AbstractPdtView是一个抽象类,而且,AbstractPdtView属于非逻辑视图,因此它不需要任何的视图解析器去定位。我们先来看看这个

AbstractPdtView的源码的抽象方法:
1
2
3
4
5
6
7
8
/*通过数据模型自定义创建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<String,Object> model,Document document,PdfWriter writer,HttpServletRequest request,HttpServletResponse response)throws Exception;

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

pom.xml

当然也需要加入依赖:

1
2
3
4
5
6
7
8
9
10
11
<!--        PDF-->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>core-renderer</artifactId>
<version>R8</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.12</version>
</dependency>

然后便可以开始程序设计了

定义PDF导出接口
1
2
3
4
5
public interface PdfExportService {
public void make(Map<String, Object> model, Document document,
PdfWriter writer, HttpServletRequest request,
HttpServletResponse response);
}

只要继承这个接口,就能够自定义导出逻辑了。

接着是写一个类,让其继承AbstractPdfView的非抽象类。

PDF导出视图类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PdfView extends AbstractPdfView {
// 导出服务接口
private PdfExportService pdfExportService = null;

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

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

这样,创建PDF对象的时候,便调用这个构造函数,而这个构造函数将调用AbstractPdfView接口的抽象方法,完成内部实现。

由控制器导出PDF数据

接下来就是由控制器去导出PDF文件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 导出接口
@GetMapping("/export/pdf")
public ModelAndView exportPdf(String userName, String note) {
// 查询用户信息列表
List<User> userList = userService.findUsers(userName, note);
// 定义PDF视图
View view = new PdfView(exportService());
ModelAndView mv = new ModelAndView();
// 设置视图
mv.setView(view);
// 加入数据模型
mv.addObject("userList", userList);
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("note", f8));
cell.setHorizontalAlignment(1);
table.addCell(cell);
// 获取数据模型中的用户列表
List<User> userList = (List<User>) model.get("userList");
for (User user : userList) {
document.add(new Chunk("\n"));
cell = new PdfPCell(new Paragraph(user.getId() + ""));
table.addCell(cell);
cell = new PdfPCell(new Paragraph(user.getUserName()));
table.addCell(cell);
String note = user.getNote() == null ? "" : user.getNote();
cell = new PdfPCell(new Paragraph(note));
table.addCell(cell);
}
// 在文档中加入表格
document.add(table);
} catch (DocumentException e) {
e.printStackTrace();
}
};
}

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

测试

打开浏览器,输入:http://localhost:8888/user/export/pdf?userName=张三&note=人

踩坑

这其中有个最为常见的坑,那就是PDF不显示中文的现象,上述代码通过加:

1
2
3
4
BaseFont Chinese = BaseFont.createFont("C:\\Windows\\Fonts\\simsun.ttc,1",BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED);
//使得:
Font f8 = new Font(Chinese);
//最后在cell和Paragraph构造中加入f8就行了

截取部分:

1
2
3
4
5
6
7
8
9
10
				Font f8 = new Font(Chinese);
//.............
cell = new PdfPCell(new Paragraph("id", f8));
//.................
List<User> userList = (List<User>) model.get("userList");
for (User user : userList) {
document.add(new Chunk("\n"));
cell = new PdfPCell(new Paragraph(user.getId() + "",f8));
//...................
}

文件上传

文件上传在web中可以说是必须有的操作了,SpringMVC也对其有着很好的支持。

SpringMVC对文件上传的支持

首先,DispatcherServlet会使用适配器模式,将HttpServletRequest接口对象转换为MultipartHttpServletRequest象。MultipartHttpServletRequest接口扩展了HttpServletRequest接口的所有方法,而且定义了一些操作文件的方法,这样通过这些方法就可以实现对上传文件的操作。

这里对于文件上传的场景,SpringMVC会将HttpServletRequest对象转化为MultipartHttpServlet­Request对象。

从MultipartHttpServletRequest接口的定义看,它存在许多的方法用来处理文件,这样SpringMVC中操作文件就十分便捷。只是在使用SpringMVC上传文件时,还需要配置MultipartHttpServletRequest,这个任务是通过MultipartResolver接口实现的。

对于MultipartResover接口,它又存在两个实现类,这两个实现类分是StandardServletMultipartResolver和CommonsMultipartResolver,可以使用它们中的任意一个来现文件上传。在默认的情况下Spring推荐使用的是StandardServletMultipartResolver,因为它只需要依赖于ServletAPI提供的包,而对于CommonsMultipartResolver,则需要依赖于Apache提供的第三方包来实,这然没有StandardServletMultipartResolver来实在。

从实用的角度来说,因为Spring3.1之后己经能够支持StandardServletMultipartR巳solver,所以CommonsMultipartResolver已经渐渐被废弃了,因此这里不再对其进行介绍。

文件上传的配置

打开application.yml

1
2
3
4
5
6
7
8
servlet:
multipart:
enabled: true #是否开启SpringMVC多分布上传功能
file-size-threshold: 0 #将文件写入磁盘阈值,值可以使用后缀“MB”或者是“KB”来表示兆字节大小
location: #指定默认上传文件夹,一般不设置
max-file-size: 5MB #设置单个文件最大值
max-request-size: 100MB #设置所有文件最大值
resolve-lazily: false #是否延迟多部件文件请求的参数和文件解析

根据这些配置,SpringBoot会自动生成StandardServletMultipartResolv巳r对象这样就能够对上传的文件进行配置。对于文件的上传可以使用ServletAPI提供的Part接口或者SpringMVC提供的MultipartFile接作为参数。

开发文件上传功能

upload
1
2
3
4
5
6
7
<body>
<form method="post"
action="./part" enctype="multipart/form-data">
<input type="file" name="file" value="请选择上传的文件" />
<input type="submit" value="提交" />
</form>
</body>

注意,这里的的form表单的类型必须声明为: enctype=”multipart/form-data”。如果没有这个声明,SpringMVC就不能自动对其解析。

FileController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Controller
@RequestMapping("/file")
public class FileController {
/**
* 打开文件上传请求页面
* @return 指向HTML的字符串
*/
@GetMapping("/page")
public String uploadPage() {
return "upload";
}

// 使用HttpServletRequest作为参数
@PostMapping("/request")
@ResponseBody
public Map<String, Object> 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("/multipart")
@ResponseBody
public Map<String, Object> 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("/part")
@ResponseBody
public Map<String, Object> uploadPart(Part file) {
// 获取提交文件名称
String fileName = file.getSubmittedFileName();
try {
// 写入文件
file.write(fileName);
} catch (Exception e) {
e.printStackTrace();
return dealResultMap(false, "上传失败");
}
return dealResultMap(true, "上传成功");
}

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

代码中uploadPage方法用来映射上传文件的HTML页面,所以只需要请求它便能够打开上传文件的页面。

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

uploadMultipartFile则是直接使用MultipartFile对象获取上传的文件,从而进行操作。uploadPart方法是使用Servlet的API,可以使用其write方法直接写入文件,这种方式更为巧妙。

测试

拦截器

虽然我们已经拥有Security来做我们的拦截器了,但是还是很有必要了解一下SpringMVC自带的拦截器。

在刚提起SpringMVC的时候,就说过DispatcherServlet,请求来到DispatcherServlet,它会根据HandlerMapping的机制找到处理器,这样就会返回一个HandlerExecutionChain对象,这个对象包含处理器和拦截器。这里的拦截器会对处理器进行拦截,这样通过截器就可以增强处理器的功能。

拦截器的设计

所有的拦截器都继承了一个接口:HandlerInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
public interface HandlerInterceptor {
//处理执行前
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
//处理器处理方法后
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
//处理器完成方法后
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}

执行流程比较直观,如图:

  1. 执行preHandle方法,该方法会返回一个布尔值。如果为false,则结束所有流程:如果为true,则执行下一步。
  2. 执行处理器逻辑,它包含控制器的功能。
  3. 执行postHandle方法。
  4. 执行视图解析和视图渲染。
  5. 执行afterCompletion方法。

因为这个接口是Java8的接口,所以3个方法都被声明为default,并且提供了空实现。当我们需要自己定义方法的时候,只需要实现HandlerInterceptor,覆盖其对应的方法即可。

开发拦截器

拦截器实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

public class Interceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
System.out.println("处理器前方法");
// 返回true,不会拦截后续的处理
return true;
}

@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("处理器后方法");
}

@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("处理器完成方法");
}
}

这里的代码实现了Handlerlnterceptor,然后按照自己的需要重写了3具体的拦截器方法。在这些方法中都打印了一些信息,这样就可以定位拦截器方法的执行顺序。

其中这里的preHandle法返回的是true,也可以将其修改为返回false,再观察其不同。有了这个拦截器,SpringMVC并不会发现它,它还需要进行注册才能够拦截处理器,为此需要在配文件中实现WebMvcConfigurer接口:

配置拦截器
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class WebMVCConfiguration implements WebMvcConfigurer {


@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器到Spring MVC机制,然后它会返回一个拦截器注册
InterceptorRegistration ir = registry.addInterceptor(new Interceptor1());
// 指定拦截匹配模式,限制拦截器拦截请求
ir.addPathPatterns("/interceptor/*");
}
}

里通过实现WebMvcConfigurer接口,重写其中的addlnterceptors方法,进而加入自定义拦截器一一Interceptor!,然后指定其拦截的模式,所以它只会拦截与正则式“/interceptor/*”匹配的请求。

控制器和页面
1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/interceptor")
public class InterceptorController {
@GetMapping("/start")
public String start() {
System.out.println("执行处理器逻辑");
return "/welcome";
}
}
1
2
3
4
5
<title>Title</title>
</head>
<body>
<h1>你好,现在进行测试</h1>
</body>

这里非常简单,不多赘述。

测试:

这里是后台信息,可以从这里看出,拦截器的运行流程。

多个拦截器的顺序

如果我们还有多个拦截器,那么它运行的顺序又是怎么样的呢?首先需要多个拦截器实例,当然和前面是一样的,这里仅仅列出来:

多个拦截器实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

public class MulitiInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
System.out.println("【" + this.getClass().getSimpleName()
+"】处理器前方法");
// 返回true,不会拦截后续的处理
return true;
}

@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("【" + this.getClass().getSimpleName()
+"】处理器后方法");
}

@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("【" + this.getClass().getSimpleName()
+"】处理器完成方法");
}
}
//......省略同样的三个类

然后,注册拦截器

注册拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注册拦截器到Spring MVC机制中
InterceptorRegistration ir = registry.addInterceptor(new
MulitiInterceptor1());
// 指定拦截匹配模式
ir.addPathPatterns("/interceptor/*");
// 注册拦截器到Spring MVC机制中
InterceptorRegistration ir2 = registry.addInterceptor(new
MulitiInterceptor2());
// 指定拦截匹配模式
ir2.addPathPatterns("/interceptor/*");
// 注册拦截器到Spring MVC机制中
InterceptorRegistration ir3 = registry.addInterceptor(new
MulitiInterceptor3());
// 指定拦截匹配模式
ir3.addPathPatterns("/interceptor/*");

这些代码放在WebMVCConfiguration 下面。

测试

同样的,我们直接去查看控制台:

我们发现,这个结果是责任链模式的规则,对于处理器前方法采用先注册先执行,而处理器后方法完成方法则是先注册后执行的规则。只是上述仅测试了处理器前(preHandle)方法返回为true的场景,在某些时候还可能返回为false,这个时候又如何呢?为此,可以将Mulitilnterceptor2的preHandle法修改返回为false,然后再进行测试,其日志如下:

上面的日志可以看出,处理器前(preHandle)方法会执行,但是一旦返回false,则续的拦截器、处理器和l所有拦截器的处理器后(postHandle)方法都不会被执行。完成方法afterCompletion则不一样,它只会执行返回true的拦截器的完成方法,而且顺序是先注册后执行。