SpringMVC授课笔记5

1 文件上传

底层基于commons-fileupload组件,进行封装,简化操作。

2 添加依赖(pom.xml)

<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>

SpringMVC授课笔记5 - 图1

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. //完善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 /> }
  2. //上传文件到指定的路径<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”
;
}

SpringMVC授课笔记5 - 图2

<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 使用逻辑路径取代物理路径

原因:

  1. 在Linux中没有盘符的概念,在WINDOWS中盘符的概念。上传文件夹无法统一的指定,比如“d:/upload”
  2. 也可以让上传文件夹在项目中(不是开发时目录,是部署后目录),不同的服务器目录可能不同。因为Tomcat、idea的安装位置不同,导致真实目录不同。

解决:使用逻辑路径取代物理路径

  1. 如何操作 | //完善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} | | —- |

  1. 注意事项

如果发布项目时选择的发布包是SpringMVC授课笔记5 - 图3,会打一个war包,并自动部署到tomcat的webapp下,启动服务器是该war包自动解压
SpringMVC授课笔记5 - 图4
如果发布项目时选择的发布包是SpringMVC授课笔记5 - 图5,就不会打成war包,也不会发布到tomcat的webapp下。

SpringMVC授课笔记5 - 图6

14 文件上传-使用文件服务器

缺陷
  • Web 应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
  • 项目运行很长时间后,会导致上传的文件积累非常多,体积非常大,从而拖慢 Tomcat 运行速度。
  • 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据不一致。

SpringMVC授课笔记5 - 图7

  • 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致 Java 代码需要重新编写、重新编译,进而导致整个项目重新部署。

解决方案:提供专门的文件服务器(一个可以)。运行Tomcat的叫应用服务器(集群下会有多个)。
SpringMVC授课笔记5 - 图8
文件服务器类型
第三方平台:阿里的 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目录下,该怎么办?

  1. 不可以在超链接中直接指定路径 | <a th:href=”@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}”>下载(直接打开)</a> | | —- |
  1. 不管是直接打开,还是附件下载,都要采用编程方式 | 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>

SpringMVC授课笔记5 - 图9

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核心流程

SpringMVC授课笔记5 - 图10
执行流程

  1. 首先浏览器发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他组件进行处理,作为统一访问点,进行全局的流程控制
  2. DispatcherServlet——>HandlerMapping,处理器映射器将会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器对象、多个HandlerInterceptor拦截器)对象
  3. DispatcherServlet——>HandlerAdapter,处理器适配器将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器
  4. HandlerAdapter——>调用处理器相应功能处理方法,并返回一个ModelAndView对象(Model部分是业务对象返回的模型数据,View部分为逻辑视图名)
  5. DispatcherServlet——> ViewResolver,视图解析器将把逻辑视图名解析为物理视图,返回View对象
  6. DispatcherServlet——>View,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构
  7. DispatcherServlet——>响应,返回控制权给DispatcherServlet,由它返回响应给用户,到此一个流程结束

总结强调

  1. 总控制器不是撒手掌柜,每次调用其他组件都要求得到结果,并根据结果调用下一个组件
  2. 分控制器的返回值:ModelAndView。Model中是数据,View是要跳转到的视图。
  3. 如果要是Ajax请求,就没有ModelAndView了,而是之间返回数据片段,客户端通过回调函数来处理数据片段。
  4. SpringMVC流程固定,开发者的主要任务是:开发分控制器、视图文件,可能还有拦截器。

21 SpringMVC运行原理-SpringMVC核心API

SpringMVC的四大组件

  • DispatcherServlet:总控制器
  • HandlerMapping:建立了请求路径和分控制器方法之间的映射
  • HandlerExecutionChain:总控制器调用HandlerMapping组件的返回值是一个执行链,不仅有要执行的分控制器方法,还有相应的多个拦截器,组成一个执行链。
  • HandlerAdapter:调用分控制器的方法,不是由总控制器之间调用的,而是由HandlerAdapter来调用
  • ViewResolver:逻辑视图(result)——->物理视图(/WEB-INF/templates/result.html)

    ①中央控制器DispatcherServlet

    SpringMVC授课笔记5 - 图11
    发现他的上级类中有HttpServlet,而Servlet的执行入口是service(),一会就从这个方法入手开始SpringMVC执行过程的讲解。

    ②处理器映射器HandlerMapping

    SpringMVC授课笔记5 - 图12
    SpringMVC授课笔记5 - 图13

项目中会有多个@RequestMapping,每个RequestMapping对应一个类或者一个方法。用户给一个请求路径,如何获取该路径所对应的方法呢?这就要通过HandlerMapping来实现了。返回的结果就是访问路径所对应的处理器。

③处理器执行链HandlerExecutionChain

为什么请求Handler,要返回HandlerExecutionChain呢。因为Handler的执行前后会有一个或者多个拦截器执行,并且拦截器是链式执行的。所有HandlerExecutionChain中就包含了要执行的一个处理器和多个拦截器的信息。
SpringMVC授课笔记5 - 图14

④处理器适配器HandlerAdapter

处理器的执行不是由总控制器直接执行的,而是通过处理器适配器来执行的。
因为会有XML方式、注解方式等处理器形式,具体执行会有不同,通过不同的HandlerAdapter来实现

SpringMVC授课笔记5 - 图15
SpringMVC授课笔记5 - 图16

⑤视图解析器ViewResolver

ViewResolver实现逻辑视图到物理视图的解析,比如:对于如下视图解析器,”main”是逻辑视图,而添加了后缀前缀的“/WEB-INF/jsp/main.jsp”就是物理视图。
SpringMVC授课笔记5 - 图17

22 SpringMVC运行原理-请求处理过程

整个请求处理过程都是 doDispatch() 方法在宏观上协调和调度,把握了这个方法就理解了 SpringMVC 总体上是如何处理请求的。
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
SpringMVC授课笔记5 - 图18
SpringMVC授课笔记5 - 图19
SpringMVC授课笔记5 - 图20

23 SpringMVC运行原理-请求处理过程源码解读

①调用HandlerMapping获取执行链

发现执行链中要要执行的分控制器的方法UserController的login,封装成一个HandlerMethod。
发现其中有三个拦截器,其中两个是自定义拦截器,执行顺序和配置顺序一致。先2再1.
SpringMVC授课笔记5 - 图21

②获取HandlerAdapter

如果使用注解,调用的是RequestMappingHandlerAdapter。
SpringMVC授课笔记5 - 图22

③正序执行拦截器的preHandler

先2再1
SpringMVC授课笔记5 - 图23

④执行分控制器的方法,返回值是ModelAndView

SpringMVC授课笔记5 - 图24

⑤逆序执行拦截器的postHandler

SpringMVC授课笔记5 - 图25

⑥逆序执行拦截器的afterCompletion

SpringMVC授课笔记5 - 图26

24 SpringMVC运行原理-启动过程

1、初始化操作调用路线图

ApplicationContext就是IoC容器的引用,如果不是web项目,之前我使用的是ClasspathApplicationContext。如果是Web项目,使用的是WebApplicationContext。
调试技巧:在调试页面中前进和后退:ctrl+alt+(<— —>)
SpringMVC授课笔记5 - 图27

2、initWebApplicationContext

SpringMVC授课笔记5 - 图28

3.createWebApplicationContext

SpringMVC授课笔记5 - 图29
SpringMVC授课笔记5 - 图30

4 定位获取springmvc配置文件路径名称的代码

HttpServletBean的一个内部类中
SpringMVC授课笔记5 - 图31

4 定位读取读取springmvc配置文件的内容以及分控制器中@RequestMapping内容的代码

初始化了@RequestMapping
SpringMVC授课笔记5 - 图32
初始化了视图解析器
SpringMVC授课笔记5 - 图33

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);
}

SpringMVC授课笔记5 - 图34

两个容器是什么关系:父子关系
使用ContextLoaderListener加载另外一个配置文件创建的IoC容器是父容器。
DispatcherServlet加载springmvc的配置文件创建的IoC容器是子容器。
SpringMVC授课笔记5 - 图35
SpringMVC授课笔记5 - 图36

28 出现的问题及其解决

如果的路径设置不合理,就会重复的创建Bean。
如何查看:将logback的总的日志级别改为DEBUG
SpringMVC授课笔记5 - 图37

SpringMVC授课笔记5 - 图38

缺点:

  1. 重复的bean会多占用资源

  2. SpringMVC创建的Controller肯定是调用SpringMVC自己创建的Service和Dao,但是在SpringMVC的配置文件中并没有关于事务的设置,所以调用SpringMVC自己创建的Service和Dao,将无法使用到事务。这绝对不可以。

  3. 解决方案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。问题解决。

  1. 解决方案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> |