目录
3.@ModelAttribute注解 (每个handler方法执行前执行)
文件上传、下载
一、文件上传
底层基于commons-fileupload 组件,进行封装,简化操作。
上传文件的处理:
1、上传到服务器指定目录下
2、将文件路径写入到数据库表中,后续下载使用
注意:文件上传是将客户端的文件上传到服务器端,底层其实是Socket编程。进行了封装,不需要编写底层代码;目前客户端就是服务器端,在一台计算机。
添加依赖(pom.xml)
<!--文件上传-->
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
配置解析器(springmvc.xml)
springmvc.xml
<!-- 配置文件上传解析器-->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
准备页面
第一点:请求方式必须是 POST
第二点:请求体的编码方式必须是 multipart/form-data(通过 form 标签的 enctype 属性设置)
第三点:使用 input 标签、type 属性设置为 file 来生成文件上传框
<h1>文件上传</h1>
<form th:action="@{/user/regist2}" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="uid"> <br>
姓名:<input type="text" name="uname"> <br>
年龄:<input type="text" name="age"> <br>
图像:<input type="file" name="photo1"> <br>
<input type="submit" value="提交">
</form>
准备控制器
注意:因为User类有String photo属性,所以MultipartFile 的变量名不要重复
关键:MultipartFile,因为在xml中的配置
@Controller
@Slf4j
public class UserController {
@RequestMapping("/user/regist")
public String register(User user, MultipartFile photo1) throws IOException {
//将照片的路径写入User对象
user.setPhoto(photo1.getOriginalFilename());
log.debug("user:"+user);
log.debug("photo1"+photo1);
log.debug("文件名:"+photo1.getOriginalFilename());
log.debug("MIME类型:"+photo1.getContentType());
log.debug("文件大小:"+photo1.getSize());
log.debug("上传file表单项的name:"+photo1.getName());
//准备上传文件夹
File file = new File("e:/upload",photo1.getOriginalFilename());
//执行转存
photo1.transferTo(file);
return "result";
}
完善1:如果目录不存在就创建
@RequestMapping("/user/regist2")
public String register2(User user, MultipartFile photo1) throws IOException {
//将照片的路径写入User对象
log.debug("user:"+user);
log.debug("photo1"+photo1);
//完善1:如果文件夹不存在,就创建
File dir = new File("e:/upload");
if(!dir.exists()){
dir.mkdirs();//可以创建多级
//dir.mkdir();//只能创建一级
}
File file = new File(dir,photo1.getOriginalFilename());
photo1.transferTo(file);
return "result";
}
完善2:通过uuid,避免同名文件覆盖
UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。
UUID只保证唯一,不保证有序,字符串类型。UUID是一个128比特的数值,这个数值可以通过一定的算法计算出来。
UUID的唯一缺陷在于生成的结果串会比较长。关于UUID这个标准使用最普遍的是微软的GUID(Globals Unique Identifiers)。在ColdFusion中可以用CreateUUID()函数很简单地生成UUID,其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。而标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),可以从cflib 下载CreateGUID() UDF进行转换。
UUID由以下几部分的组合:
(1)UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
(2)时钟序列。
(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
//完善2:避免同名文件覆盖,通过uuid
String fileName = UUID.randomUUID().toString();
//拼接文件扩展(后缀)名
String extName = photo1.getOriginalFilename().substring(photo1.getOriginalFilename().lastIndexOf("."));
fileName = fileName+extName;
完善3:限制上传文件类型
//完善3:限制上传文件的类型
//方法1 按照扩展名来限制 .jpg .gif .png
//if(".extName".equals(extName.toLowerCase()) && !".gif".equals(extName.toLowerCase())){}
//方式2 通过MIME类型来限制 tomcat的web.xml中 有所有的MIME类型和扩展名的对应关系
String contentType = photo1.getContentType().toLowerCase();
//如果不同,错误信息
if(!"image/jpeg".equals(contentType) && !"image/gif".equals(contentType)){
model.addAttribute("error","上传类型只能是jpg和gif类型");
return "portal";
}
完善4:限制上传文件大小
在代码中限制,不采用。
//完善4:限制上传文件大小
long size = photo1.getSize();
if(size>1024*100){ //100K
model.addAttribute("error","上传大小不能超过100K");
return "portal";
}
在springmvc.xml中限制
<!--配置文件上传解析器-->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
<property name="defaultEncoding" value="UTF-8"/>
<!--限制多个上传文件的总大小-->
<property name="maxUploadSize" value="1024000"></property>
<!--限制单个上传文件的大小-->
<property name="maxUploadSizePerFile" value="1024000"></property>
</bean>
完善5:使用逻辑路径取代物理路径
原因:
- 在Linux中没有盘符的概念,在WINDOWS中盘符的概念。上传文件夹无法统一的指定,比如“d:/upload”
- 也可以让上传文件夹在项目中(不是开发时目录,是部署后目录),不同的服务器目录可能不同。因为Tomcat、idea的安装位置不同,导致真实目录不同。
解决:使用逻辑路径取代物理路径
//完善5:根据逻辑路径获取物理路径
String realPath = servletContext.getRealPath("/upload");
System.out.println("tomcat中项目真实路径:"+realPath);
//tomcat中项目真实路径:E:\tomcat-8.5.27\webapps\day13updownload\upload
File dir = new File(realPath);
最终代码:
@Controller
@Slf4j
public class UserController {
@Autowired
private ServletContext servletContext;
@RequestMapping("/user/regist2")
public String register2(User user, MultipartFile photo1, Model model) throws IOException {
//将照片的路径写入User对象
log.debug("user:"+user);
log.debug("photo1"+photo1);
//完善5:根据逻辑路径获取物理路径
String realPath = servletContext.getRealPath("/upload");
System.out.println("tomcat中项目真实路径:"+realPath);
File dir = new File(realPath);
//完善1:如果文件夹不存在,就创建
//File dir = new File("e:/upload");
if(!dir.exists()){
dir.mkdirs();//可以创建多级
//dir.mkdir();//只能创建一级
}
//完善2:避免同名文件覆盖,通过uuid
String fileName = UUID.randomUUID().toString();
//拼接文件扩展(后缀)名
String extName = photo1.getOriginalFilename().substring(photo1.getOriginalFilename().lastIndexOf("."));
fileName = fileName+extName;
//完善3:限制上传文件的类型
//方法1 按照扩展名来限制 .jpg .gif .png
//if(".extName".equals(extName.toLowerCase()) && !".gif".equals(extName.toLowerCase())){}
//方式2 通过MIME类型来限制 tomcat的web.xml中 有所有的MIME类型和扩展名的对应关系
String contentType = photo1.getContentType().toLowerCase();
//如果不同,错误信息
if(!"image/jpeg".equals(contentType) && !"image/gif".equals(contentType)){
model.addAttribute("error","上传类型只能是jpg和gif类型");
return "portal";
}
//完善4:限制上传文件大小
// long size = photo1.getSize();
// if(size>1024*100){ //上传大小不能超过100k
// model.addAttribute("error","上传大小不能超过100k");
// return "portal";
// }
//在配置文件中设置
File file = new File(dir,fileName);
photo1.transferTo(file);
return "result";
}
二、文件下载
就是文件的复制,注意输入输出流分别是谁
<h1>文件下载</h1> <br>
<img th:src="@{/upload/3c361511-4b57-40ec-8e96-f190dd3ec42d.jpg}" width="700xp" height="450xp"> <br>
<a th:href="@{/upload/3c361511-4b57-40ec-8e96-f190dd3ec42d.jpg}">打开图片</a> <br>
<a th:href="@{/user/download(photoName=3c361511-4b57-40ec-8e96-f190dd3ec42d.jpg)}">下载图片,弹框下载</a> <br>
@RequestMapping("/user/download")
public void download(String photoName, HttpServletResponse response) throws IOException {
//1.获取项目真实路径
String realPath = servletContext.getRealPath("/upload");
//2.创建一个输入流和输出流
File file = new File(realPath, photoName);
InputStream is = new FileInputStream(file);
OutputStream os = response.getOutputStream();//写到客户端
//3.指定响应的信息
response.setHeader("Content-Disposition", "attachment;filename=Miku.jpg");
//attachment 附件 下载 inline 在线打开
//response.setContentLength((int)file.length());//long-->int//指定大小
//response.setContentType("image/jpg");//根据具体图片类型来写,事先和文件名保存在数据库中
//4.使用输入流和输出流完成下载操作
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"); //直接打开
三、其他不重要的内容
1.springmvc配置文件默认位置和名称
<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:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
如果没有指定文件位置:默认在橙色路径寻找
IOException parsing XML document from ServletContext resource [/WEB-INF/dispatcherServlet-servlet.xml]; nested exception is java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/dispatcherServlet-servlet.xml]
2.@RequestMapping的更多参数
需求 | 映射方式 |
---|---|
请求参数中必须包含userName | @RequestMapping(value = "/xxx", params="userName") |
请求参数中不能包含userName | @RequestMapping(value = "/xxx", params="!userName") |
请求参数中必须包含userName 且值必须为Tom2015 | @RequestMapping(value = "/xxx", params="userName=Tom2015") |
请求参数中必须包含userName 但值不能为Tom2015 | @RequestMapping(value = "/xxx", params="userName=!Tom2015") |
请求参数中必须包含userName 且值为Tom2015, 同时必须包含userPwd但值不限 | @RequestMapping(value = "/xxx", params={"userName=Tom2015","userPwd"} ) |
3.@ModelAttribute注解 (每个handler方法执行前执行)
效果1:在每个 handler 方法前执行
效果2:可以将某些数据提前存入请求域
@Controller
public class ModelAttrHandler {
@ModelAttribute
public void doSthBefore(Model model) {
model.addAttribute("initAttr", "initValue");
}
}
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流程固定,开发者的主要任务是:开发分控制器、视图文件,可能还有拦截器。
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) 就是访问路径所对应的处理器和所需经过的拦截器。
③处理器执行链 HandlerExecutionChain
为什么请求Handler,要返回HandlerExecutionChain? 因为Handler的执行前后会有一个或者多个拦截器执行,并且拦截器是链式执行的。
所有HandlerExecutionChain中就包含了要执行的一个处理器和多个拦截器的信息。
④处理器适配器 HandlerAdapter
为什么总控制器不直接调用? 因为会有XML方式、注解方式等处理器形式,具体执行会有不同,通过不同的HandlerAdapter来实现。这里用到了适配器设计模式。所以调用分控制器的方法,不是由总控制器之间调用的,而是由 HandlerAdapter来调用。
⑤视图解析器ViewResolver
ViewResolver实现逻辑视图到物理视图的解析
比如:对于如下视图解析器,”main”是逻辑视图,而添加了后缀前缀的“/WEB-INF/jsp/main.jsp”就是物理视图。
二、请求处理过程
整个请求处理过程都是 doDispatch() 方法在宏观上协调和调度,把握了这个方法就理解了 SpringMVC 总体上是如何处理请求的。
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
核心方法中的核心代码,作用是执行handler:
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
所在类 | 所在方法 | 断点行数 | 作用 |
---|---|---|---|
DispatcherServlet | doDispatch() | 1037 | 创建调用链对象 |
DispatcherServlet | doDispatch() | 1044 | 创建 HandlerAdapter 对象 |
DispatcherServlet | doDispatch() | 1056 | 调用拦截器 preHandle()方法 |
DispatcherServlet | doDispatch() | 1061 | 执行目标 handler 方法 |
DispatcherServlet | doDispatch() | 1068 | 调用拦截器 postHandle()方法 |
DispatcherServlet | doDispatch() | 1078 | 执行所有后续操作 |
AbstractHandlerMapping | getHandlerExecutionChain() | 592 | 创建调用链对象 |
AbstractHandlerMapping | getHandlerExecutionChain() | 599 | 在调用链中添加拦截器 |
HandlerExecutionChain | applyPreHandle() | 146 | 调用拦截器 preHandle()方法 |
HandlerExecutionChain | applyPostHandle() | 163 | 调用拦截器 postHandle()方法 |
HandlerExecutionChain | triggerAfterCompletion | 175 | 调用拦截器 afterCompletion()方法 |
DataBinder | doBind() | 747 | 执行数据绑定 |
RequestMappingHandlerAdapter | invokeHandlerMethod() | 868 | 创建 ModelAndViewContainer 对象 |
RequestMappingHandlerAdapter | invokeHandlerMethod() | 893 | 将ModelAndViewContainer 对象传入调用目标 handler 的方法 |
DispatcherServlet | processDispatchResult() | 1125 | 处理异常 |
DispatcherServlet | processDispatchResult() | 1139 | 渲染视图 |
DispatcherServlet | processDispatchResult() | 1157 | 调用拦截器 afterCompletion()方法 |
WebEngineContext的内部类: RequestAttributesVariablesMap | setVariable() | 783 | 将模型数据存入请求域 |
请求处理过程源码解读 (代码debug)
①调用HandlerMapping获取执行链
发现执行链中要执行的分控制器的方法EmployController的findAll方法,封装成一个
HandlerMethod。
发现其中有三个拦截器,其中两个是自定义拦截器,执行顺序和配置顺序一致。先2再1.
②获取HandlerAdapter
如果使用注解,调用的是 RequestMappingHandlerAdapter。
③正序执行拦截器的preHandler
④执行分控制器的方法,返回值是ModelAndView
⑤逆序执行拦截器的postHandler
⑥逆序执行拦截器的afterCompletion
三、启动过程 (IoC容器初始化)
ApplicationContext就是IoC容器的引用,如果不是web项目,之前我们使用的是
ClasspathApplicationContext。如果是Web项目,使用的是WebApplicationContext。
调试技巧:在调试页面中前进和后退:ctrl+alt+(<-- -->)
1、initWebApplicationContext
2、createWebApplicationContext (wac)
3、定位获取springmvc配置文件路径名称的代码
在HttpServletBean的一个内部类中
4、定位读取读取springmvc配置文件的内容以及分控制器中@RequestMapping内容的代码
初始化了@RequestMapping
初始化了视图解析器
四、ContextLoaderListener原理源码
问题引入
SSM整合后,配置文件内容过多,可以分到两个配置文件中。这两个配置文件夹如何加载
方法1:DispatcherServlet加载所有的配置文件 (classpath:spring*.xml)
<!--配置SpringMVC总控制器,唯一的Servlet -->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!--指定SpringMVC配置文件的名称和位置,有默认位置 -->
<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
2、使用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容器是子容器。
2、出现的问题及其解决 (创建Bean重复,事务问题)
如果<context:component-scan >的路径设置不合理,就会重复的创建Bean。比如两个配置文件的扫描路径都是com.atguigu,就都会扫描Controller和Service的两个Bean。
缺点:
- 重复的bean会多占用资源。
- SpringMVC创建的Controller肯定是调用SpringMVC自己创建的Service和Dao,但是在SpringMVC的配置文件中并没有关于事务的设置,所以调用SpringMVC自己创建的Service和Dao,将无法使用到事务。这绝对不可以。
解决方案1:两个配置文件中扫描不同的包
结果:SpringMVC中创建了Controller,Listener中创建了Service并应用了事务。当SpringMVC在自己的IoC容器中找不到Service的时候,就会到父容器中去找Service。问题解决。
<!-- 配置注解扫描基准路径-->
<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>
解决方案二:使用 exclude-filter标签对重复包进行排除。
<!-- 配置注解扫描基准路径:
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>
<!-- 配置注解扫描基准路径:@Controller @Service @Repository @Component-->
<context:component-scan base-package="com.atguigu">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>