SpringMVC授课笔记5
1 文件上传
底层基于commons-fileupload组件,进行封装,简化操作。
2 添加依赖(pom.xml)
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency> |
---|
3 配置解析器(springmvc.xml)
<bean id=”multipartResolver” class=”org.springframework.web.multipart.commons.CommonsMultipartResolver”> <property name=”defaultEncoding” value=”utf-8”></property> </bean> |
---|
4 准备页面
@Data @AllArgsConstructor @NoArgsConstructorpublic class User { private String userId; private String realName; private Integer age; private String photo;//照片的路径} |
---|
<h3>文件上传</h3> <form th:action=”@{/user/register}” method=”post” enctype=”multipart/form-data”> 用户名:<input type=”text” name=”userId”><br> 真实姓名:<input type=”text” name=”realName”><br> 年龄:<input type=”text” name=”age”><br> 照片:<input type=”file” name=”photo”><br> </form> |
- 第一点:请求方式必须是 POST
- 第二点:请求体的编码方式必须是 multipart/form-data(通过 form 标签的 enctype 属性设置)
- 第三点:使用 input 标签、type 属性设置为 file 来生成文件上传框
5 准备控制器
| @Controller
@Slf4jpublic class UserController {
@RequestMapping(“/user/register”)
public String register(User user, MultipartFile photo1) throws IOException {
log.debug(user.toString());
log.debug(photo1.toString());
user.setPhoto(photo1.getOriginalFilename());
//上传文件到指定的路径
photo1.transferTo(new File(“d:/upload/“+photo1.getOriginalFilename()));
return “result”;
}
} | | —- |
6 进行测试
注意:因为User类中有String photo属性,所以MultipartFile 的变量名不要重复
7 文件上传-认识MultipartFile类
log.debug(photo1.getOriginalFilename()); //上传文件的原始名称 zsf.webp zcs.jpglog.debug(photo1.getContentType()); //MIME类型 tomcat web.xml中有MIME列表 html text/html; jpg images/jpeg txt text/plainlog.debug(photo1.getName()); //表单的上传字段名 log.debug(photo1.getSize()+“”); //文件大小 //log.debug(photo1.getBytes()+””); //文件对对应的字节数组内容log.debug(photo1.getInputStream()+“”);//上传文件需要输入流,下载文件需要输出流 |
---|
8 文件上传-完善
9 如果目录不存在就创建
| @RequestMapping(“/user/register”)public String register(User user, MultipartFile photo1) throws IOException {
log.debug(user.toString());
log.debug(photo1.toString());
user.setPhoto(photo1.getOriginalFilename());
//完善1:如果文件夹不存在,就创建<br /> File dir = **new **File(**"d:/upload"**);<br /> **if**(!dir.exists()){<br /> //dir.mkdir(); //只能创建一级目录 d:/upload<br /> dir.mkdirs();//可以创建多 级目录 d:/upload/abc/def/ghi<br /> }
//上传文件到指定的路径<br /> //photo1.transferTo(new File("d:/upload/"+photo1.getOriginalFilename()));<br /> photo1.transferTo(**new **File(dir,photo1.getOriginalFilename()));<br /> // 写用户信息到数据库(MyBatis,包括文件路径)<br /> **return "result"**;<br />} |
| —- |
10 避免同名文件覆盖
UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。
只保证唯一,不保证有序
UUID由以下几部分的组合:
(1)当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
(2)时钟序列。
(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
| @RequestMapping(“/user/register”)public String register(User user, MultipartFile photo1) throws IOException {
log.debug(user.toString());
log.debug(photo1.toString());
user.setPhoto(photo1.getOriginalFilename());
//完善1:如果文件夹不存在,就创建<br /> File dir = **new **File(**"d:/upload"**);<br /> **if**(!dir.exists()){<br /> //dir.mkdir(); //只能创建一级目录 d:/upload<br /> dir.mkdirs();//可以创建多 级目录 d:/upload/abc/def/ghi<br /> }<br /> //完善2:避免同名文件覆盖(给每个上传文件起一个唯一的名字 UUID)<br /> String fileNewName = UUID._randomUUID_().toString(); //36f0b33c-e10d-440e-927b-2d96e6d9f051<br /> String extName = photo1.getOriginalFilename().substring(photo1.getOriginalFilename().lastIndexOf(**"."**)); //zadfad.adfad.sf.webp<br /> fileNewName += extName;<br /> //上传文件到指定的路径<br /> //photo1.transferTo(new File("d:/upload/"+photo1.getOriginalFilename()));<br /> //photo1.transferTo(new File(dir,photo1.getOriginalFilename()));<br /> photo1.transferTo(**new **File(dir,fileNewName));<br /> // 写用户信息到数据库(MyBatis,包括文件路径)<br /> **return "result"**;<br />} |
| —- |
11 限制上传文件类型
//完善3:限制文件上传类型 //方法1:按照扩展名限制 // if(extName!=null &&(extName.equals(“.jpg”) || extName.equals(“.gif”))){ } //方法2:按照MIME类型限制 String contentType = photo1.getContentType().toLowerCase(); if(!“image/jpeg”.equals(contentType) && !“image/gif”.equals(contentType)){ //如果不是image/jpeg image/gif model.addAttribute(“error”,“只能上传jpg和gif文件”); return “error”; } |
---|
12 限制上传文件大小
//完善4:限制上传文件的大小(不推荐使用该方式)long size = photo1.getSize();if(size>101024){ //10K model.addAttribute(“error”,“上传文件不能超过10K”); *return “error”; } |
---|
<bean id=”multipartResolver” class=”org.springframework.web.multipart.commons.CommonsMultipartResolver”> <property name=”defaultEncoding” value=”utf-8”></property> <property name=”maxUploadSize” value=”100000”></property> <property name=”maxUploadSizePerFile” value=”10000”></property> </bean> |
---|
13 使用逻辑路径取代物理路径
原因:
- 在Linux中没有盘符的概念,在WINDOWS中盘符的概念。上传文件夹无法统一的指定,比如“d:/upload”
- 也可以让上传文件夹在项目中(不是开发时目录,是部署后目录),不同的服务器目录可能不同。因为Tomcat、idea的安装位置不同,导致真实目录不同。
解决:使用逻辑路径取代物理路径
- 如何操作
| //完善5:使用逻辑路径取代物理路径String upDir = “/upload”; //逻辑路径String realPath = servletContext.getRealPath(upDir);
//物理路径 c:/adfa/adfadf/adfad/upload /adfad/adfad/upload
//完善1:如果文件夹不存在,就创建File dir = new File(realPath);if(!dir.exists()){
//dir.mkdir(); //只能创建一级目录 d:/upload
dir.mkdirs();//可以创建多 级目录 d:/upload/abc/def/ghi} |
| —- |
- 注意事项
如果发布项目时选择的发布包是,会打一个war包,并自动部署到tomcat的webapp下,启动服务器是该war包自动解压
如果发布项目时选择的发布包是,就不会打成war包,也不会发布到tomcat的webapp下。
14 文件上传-使用文件服务器
缺陷
- Web 应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
- 项目运行很长时间后,会导致上传的文件积累非常多,体积非常大,从而拖慢 Tomcat 运行速度。
- 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据不一致。
- 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致 Java 代码需要重新编写、重新编译,进而导致整个项目重新部署。
解决方案:提供专门的文件服务器(一个可以)。运行Tomcat的叫应用服务器(集群下会有多个)。
文件服务器类型
第三方平台:阿里的 OSS 对象存储服务、七牛云
自己搭建服务器:FastDFS等
15 文件下载
<h3>文件下载</h3> <img th:src=”@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}”><br> <a th:href=”@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}”>下载(直接打开)</a> <hr> <a th:href=”@{/user/download(photoName=35b27821-710e-444a-8f5e-226dd7fb5116.gif)}”>下载(弹框提示下载)</a> |
---|
@RequestMapping(“/user/download”)public void download(String photoName, HttpServletResponse response) throws IOException { //创建输入流和输出流 String realPath = servletContext.getRealPath(“/upload”); /C:\\Users\\Administrator\\IdeaProjects\\springmvc\\pro11updownload\\target\\pro11_updownload-1.0-SNAPSHOT\\upload File file = new File(realPath,photoName);// InputStream is = new FileInputStream(file); OutputStream os = response.getOutputStream(); //写到客户端,那就是下载 //指定三个响应头(其实就一个),控制浏览器的处理,不是直接显示,而是弹框 //response.setContentLength((int)file.length()); //response.setContentType();//MIME类型 也是保存在数据库中 //建议:保存用户信息到数据库时,要保存照片的原始名称、UUID名称、MIME类型 response.setHeader(“Content-Disposition”,“attachment;filename=zwj.gif”); //attachment 附件 //使用输入流和输出流 IOUtils._copy(is,os); //关闭输入流和输出流 is.close(); os.close(); } |
如果文件不在项目目录下,而是在d:\upload目录下,该怎么办?
- 不可以在超链接中直接指定路径 | <a th:href=”@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}”>下载(直接打开)</a> | | —- |
- 不管是直接打开,还是附件下载,都要采用编程方式
| File file = new File(“d:/upload”,photoName);
response.setHeader(“Content-Disposition”,“attachment;filename=zwj.gif”); //attachment 附件response.setHeader(“Content-Disposition”,“inline”); //直接打开 | | —- |
16 其他不重要的内容
17 springmvc配置文件默认位置和名称
<servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> |
---|
18 @RequestMapping的更多参数
需求 | 映射方式 |
---|---|
根据 Accept-Language:zh-CN,zh;q=0.8 映射 | @RequestMapping ( value=”/xxx”, headers= “Accept-Language=zh-CN,en;q=0.8” ) |
请求参数中必须包含userName | @RequestMapping(value = “/xxx”, params=”userName”) |
请求参数中不能包含userName | @RequestMapping(value = “/xxx”, params=”!userName”) |
19 @ModelAttribute
@ModelAttributepublic void before(){ System.out.println(“————调用分控制器的每个方法之前先执行该方法—————“); } |
---|
效果1:在每个 handler 方法前执行(作用)
效果2:可以将某些数据提前存入请求域(应用)
20 SpringMVC运行原理-SpringMVC核心流程
执行流程
- 首先浏览器发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他组件进行处理,作为统一访问点,进行全局的流程控制
- DispatcherServlet——>HandlerMapping,处理器映射器将会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器对象、多个HandlerInterceptor拦截器)对象
- DispatcherServlet——>HandlerAdapter,处理器适配器将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器
- HandlerAdapter——>调用处理器相应功能处理方法,并返回一个ModelAndView对象(Model部分是业务对象返回的模型数据,View部分为逻辑视图名)
- DispatcherServlet——> ViewResolver,视图解析器将把逻辑视图名解析为物理视图,返回View对象
- DispatcherServlet——>View,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构
- DispatcherServlet——>响应,返回控制权给DispatcherServlet,由它返回响应给用户,到此一个流程结束
总结强调
- 总控制器不是撒手掌柜,每次调用其他组件都要求得到结果,并根据结果调用下一个组件
- 分控制器的返回值:ModelAndView。Model中是数据,View是要跳转到的视图。
- 如果要是Ajax请求,就没有ModelAndView了,而是之间返回数据片段,客户端通过回调函数来处理数据片段。
- SpringMVC流程固定,开发者的主要任务是:开发分控制器、视图文件,可能还有拦截器。
21 SpringMVC运行原理-SpringMVC核心API
SpringMVC的四大组件
- DispatcherServlet:总控制器
- HandlerMapping:建立了请求路径和分控制器方法之间的映射
- HandlerExecutionChain:总控制器调用HandlerMapping组件的返回值是一个执行链,不仅有要执行的分控制器方法,还有相应的多个拦截器,组成一个执行链。
- HandlerAdapter:调用分控制器的方法,不是由总控制器之间调用的,而是由HandlerAdapter来调用
- ViewResolver:逻辑视图(result)——->物理视图(/WEB-INF/templates/result.html)
①中央控制器DispatcherServlet
发现他的上级类中有HttpServlet,而Servlet的执行入口是service(),一会就从这个方法入手开始SpringMVC执行过程的讲解。②处理器映射器HandlerMapping
项目中会有多个@RequestMapping,每个RequestMapping对应一个类或者一个方法。用户给一个请求路径,如何获取该路径所对应的方法呢?这就要通过HandlerMapping来实现了。返回的结果就是访问路径所对应的处理器。
③处理器执行链HandlerExecutionChain
为什么请求Handler,要返回HandlerExecutionChain呢。因为Handler的执行前后会有一个或者多个拦截器执行,并且拦截器是链式执行的。所有HandlerExecutionChain中就包含了要执行的一个处理器和多个拦截器的信息。
④处理器适配器HandlerAdapter
处理器的执行不是由总控制器直接执行的,而是通过处理器适配器来执行的。
因为会有XML方式、注解方式等处理器形式,具体执行会有不同,通过不同的HandlerAdapter来实现
⑤视图解析器ViewResolver
ViewResolver实现逻辑视图到物理视图的解析,比如:对于如下视图解析器,”main”是逻辑视图,而添加了后缀前缀的“/WEB-INF/jsp/main.jsp”就是物理视图。
22 SpringMVC运行原理-请求处理过程
整个请求处理过程都是 doDispatch() 方法在宏观上协调和调度,把握了这个方法就理解了 SpringMVC 总体上是如何处理请求的。
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
23 SpringMVC运行原理-请求处理过程源码解读
①调用HandlerMapping获取执行链
发现执行链中要要执行的分控制器的方法UserController的login,封装成一个HandlerMethod。
发现其中有三个拦截器,其中两个是自定义拦截器,执行顺序和配置顺序一致。先2再1.
②获取HandlerAdapter
如果使用注解,调用的是RequestMappingHandlerAdapter。
③正序执行拦截器的preHandler
④执行分控制器的方法,返回值是ModelAndView
⑤逆序执行拦截器的postHandler
⑥逆序执行拦截器的afterCompletion
24 SpringMVC运行原理-启动过程
1、初始化操作调用路线图
ApplicationContext就是IoC容器的引用,如果不是web项目,之前我使用的是ClasspathApplicationContext。如果是Web项目,使用的是WebApplicationContext。
调试技巧:在调试页面中前进和后退:ctrl+alt+(<— —>)
2、initWebApplicationContext
3.createWebApplicationContext
4 定位获取springmvc配置文件路径名称的代码
4 定位读取读取springmvc配置文件的内容以及分控制器中@RequestMapping内容的代码
25 ContextLoaderListener原理源码
26 问题引入
SSM整合后,配置文件内容过多,可以分到两个配置文件中。这两个配置文件夹如何加载
方法1:DispatcherServlet加载所有的配置文件
<servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </*servlet> |
---|
只有一个Ioc容器,存放所有的Bean
方法2:DispatcherServlet加载springmvc的配置文件,使用ContextLoaderListener加载另外一个配置文件
会有两个Ioc容器
使用ContextLoaderListener加载另外一个配置文件创建的IoC容器是父容器。
DispatcherServlet加载springmvc的配置文件创建的IoC容器是子容器。
注意:Servlet、Filter、Listener的加载顺序:Listener、Filter、Servlet
27 使用Listener创建IoC容器
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-persist.xml</param-value> </context-param><listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> |
---|
ServletContextListener :监听项目上下文,只有两个方法。
public interface ServletContextListener extends EventListener { //项目启动时执行该方法 void contextInitialized(ServletContextEvent var1); //项目销毁时执行该方法 void contextDestroyed(ServletContextEvent var1); } |
---|
两个容器是什么关系:父子关系
使用ContextLoaderListener加载另外一个配置文件创建的IoC容器是父容器。
DispatcherServlet加载springmvc的配置文件创建的IoC容器是子容器。
28 出现的问题及其解决
如果
如何查看:将logback的总的日志级别改为DEBUG
缺点:
重复的bean会多占用资源
SpringMVC创建的Controller肯定是调用SpringMVC自己创建的Service和Dao,但是在SpringMVC的配置文件中并没有关于事务的设置,所以调用SpringMVC自己创建的Service和Dao,将无法使用到事务。这绝对不可以。
解决方案1:两个配置文件中扫描不同的包 | <context:component-scan base-package=”com.atguigu.service,com.atguigu.dao”></context:component-scan> | | —- | | <context:component-scan base-package=”com.atguigu.controller”></context:component-scan> |
结果:SpringMVC中创建了Controller,Listener中创建了Service并应用了事务。当SpringMVC在自己的IoC容器中找不到Service的时候,就会到父容器中去找Service。问题解决。
解决方案2:两个配置文件中扫描不同的包 | <!— 配置注解扫描基准路径:
use-default-filters=”true” @Controller @Service @Repository @Component
use-default-filters=”false” 只扫描基准包下被include-filter指定的注解
—><context:component-scan base-package=”com.atguigu” use-default-filters=”false”>
<context:include-filter type=”annotation” expression=”org.springframework.stereotype.Controller”/>
</context:component-scan> | | —- | | <context:component-scan base-package=”com.atguigu”>
<context:exclude-filter type=”annotation” expression=”org.springframework.stereotype.Controller”/>
</context:component-scan> |