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 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 <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); View view = new PdfView(exportService()); ModelAndView mv = new ModelAndView(); mv.setView(view); mv.addObject("userList" , userList); return mv; } @SuppressWarnings ("unchecked" )private PdfExportService exportService () { return (model, document, writer, request, response) -> { try { document.setPageSize(PageSize.A4); document.addTitle("用户信息" ); document.add(new Chunk("\n" )); 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=张三¬e=人
踩坑 这其中有个最为常见的坑,那就是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);
截取部分:
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对象转化为MultipartHttpServletRequest对象。
从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 file-size-threshold: 0 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 { @GetMapping ("/page" ) public String uploadPage () { return "upload" ; } @PostMapping ("/request" ) @ResponseBody public Map<String, Object> uploadRequest (HttpServletRequest request) { boolean flag = false ; MultipartHttpServletRequest mreq = null ; if (request instanceof MultipartHttpServletRequest) { mreq = (MultipartHttpServletRequest) request; } else { return dealResultMap(false , "上传失败" ); } 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 , "上传成功" ); } @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 { } }
执行流程比较直观,如图:
执行preHandle方法,该方法会返回一个布尔值。如果为false,则结束所有流程:如果为true,则执行下一步。
执行处理器逻辑,它包含控制器的功能。
执行postHandle方法。
执行视图解析和视图渲染。
执行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("处理器前方法" ); 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) { 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() +"】处理器前方法" ); 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 InterceptorRegistration ir = registry.addInterceptor(new MulitiInterceptor1()); ir.addPathPatterns("/interceptor/*" ); InterceptorRegistration ir2 = registry.addInterceptor(new MulitiInterceptor2()); ir2.addPathPatterns("/interceptor/*" ); InterceptorRegistration ir3 = registry.addInterceptor(new MulitiInterceptor3()); ir3.addPathPatterns("/interceptor/*" );
这些代码放在WebMVCConfiguration 下面。
测试 同样的,我们直接去查看控制台:
我们发现,这个结果是责任链模式的规则,对于处理器前方法采用先注册先执行,而处理器后方法完成方法则是先注册后执行的规则。只是上述仅测试了处理器前(preHandle)方法返回为true的场景,在某些时候还可能返回为false,这个时候又如何呢?为此,可以将Mulitilnterceptor2的preHandle法修改返回为false,然后再进行测试,其日志如下:
上面的日志可以看出,处理器前(preHandle)方法会执行,但是一旦返回false,则续的拦截器、处理器和l所有拦截器的处理器后(postHandle)方法都不会被执行。完成方法afterCompletion则不一样,它只会执行返回true的拦截器的完成方法,而且顺序是先注册后执行。