一·图片的存储的两种解决方案:
1.保存到服务器文件夹,数据库中保存图片路径。
比较适合图片不是非常重要的情况,比如在论坛上发几张图。就没有必要把图存到数据库里,只要把图片存到服务器的文件夹,进行命名,再把这个名字存在数据库的对应字段。
2.保存到数据库的blob字段上
这个是真的要把图片保存到数据库的对应字段上,比如说:身份证的照片。
保存在文件夹,数据库备份的时候是没法备份的,图片没法跟着走
blob: binary large object 在mysql中4G
(tinyblob 255字节, smallblob 64k,mediumblob (16M)… …)
做word或者图片上传还是倾向使用mediumblob
clob: character large object(字符型大对象)
数据库相关的字符串字段是varchar (8000),它最多只能到8000个字符,就不能用来保存红楼梦之类的。这时使用clob,有些数据库叫text。
二·
1·修改Student
给domain增加一个属性,用以保存学生照片
照片以字节数组来存储,在domain下Student添加,属性“stuPic”:
private byte[] stuPic;//学生照片
添加Getter和Setter:
2·修改数据库表结构
复制原来的docs到stuinfo下:
修改内容为:
create table tbl_student(
stu_no int primary key,
stu_name varchar(30) not null,
stu_age decimal(5,2) not null
);
alter table tbl_student add stu_pic mediumblob;
数据库已经有数据了,只能以修改的方式来增加一个图片存储字段。修改数据库表结构用alter,修改数据用update。允许空,原来是有记录的。
登入进数据库,进行修改:
查看结果,为了不删掉数据,允许空,不提供照片也可以:
3·修改网页模态窗与设置默认图片
在创建学生时如何发送图片信息呢?
现在我们只有学生学号,学生姓名和学生成绩:
在其中添加:
<div class="form-group">
<label>学生照片</label>
<input type="file" class="form-control-file" name="stuPic">
</div>
效果:
点击“选择文件”按钮出现选择文件的窗口。是因为我们有一个类型是file。
找一张默认照片:
随便找一张:
当用户没有提供照片时就显示这张照片。在resources下新建文件夹“pics”:
把图片命名为“default.png”贴到pics下:
访问“http://localhost:8080/stuinfo/resources/pics/default.png”就可以看到这张图了:
这张图的分辨率是390x388。
在学生照片的div上添加:
<img src="/stuinfo/resources/pics/default.png" width="150px" height="150px">
效果:
但是选择文件没办法产生预览,只会这样子显示:
在list_student.jsp的script处添加:
// 传进来的部件就是文件域
function previewImage(file){
//获得这个图片
var img = document.getElementById('picImg');
//首先要判断文件域有没有文件,如果有文件,就说明你选择里一个文案要上传了。确认file有文件,且第一个文件不为空,做相关的事情。就开始其读数据
if (file.files && file.files[0])
{
//1·准备数据读取器
//准备一个文件读取器对象,并告诉它文件读取完毕之后要做什么。
var reader = new FileReader();
//成功读取了图片信息后,把读取结果赋予
//2·告诉数据读取器,读完之后要做什么
//FileReader.readAsDataURL()
//开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的字符串以表示所读取文件的内容。
reader.onload = function(evt){//onload告诉他文件读取完毕后干什么,“=”后面是一个回调函数
img.src= evt.target.result;//当读取完毕之后,把读到的内容赋给src,放到img身上来,就是赋予这个输入控件。就是把这个输入控件读到的数据往img上赋值。从而将img原来的src的值替代掉。实现了预览效果。
console.log("read ok! img src get value!"+evt.target.result);//打印出read ok
}
//3·开始去读
console.log("start to read!")
reader.readAsDataURL(file.files[0]);
}
else{
//如果没有内容的话就用默认值
img.src="/stuinfo/resources/pics/default.png";
// alert("no upload file!");
}
}
这个脚本的目标是预览图片:
1·var img = document.getElementById('picImg');
获得这个图片
给img一个id:
修改img为:<img id="picImg" src="/stuinfo/resources/pics/default.png" width="150px" height="150px"/>
2·修改input,添加onchange="previewImage(this)"
:
<input type="file" class="form-control-file" name="stuPic" onchange="previewImage(this)">
“previewImage(this)”是将当前这个输入控件当做参数传到函数里面去
测试:
查看控制台:
可以看到打印出了一堆图片的信息,以64进制来显示的2进制,这样非常长的2进制显示出来会比较小。
不想选了,点击“选择文件”按钮,到选择文件界面,直接点击取消。就会到了原来的默认图片。
4·数据落地
要使数据落地还需要两个库,fileupload和io:
在pom.xml中添加:
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
fileupload会把io带进来。他们是层级依赖:
青色框的部分是会自动封装进去的,但是红色框的呢?
准备一个多段文件用来接收多段提交的东西,
多段提交:如果要提交的网页中,是有包含文件上传域的,那么编码模式必须设成多段提交。
因为要保存的数据体积是很大的,他必须分成多段来慢慢送。
在list_student.jsp的新生注册模态窗如下位置添加enctype="multipart/form-data"
:
来到控制器:
在“createStudent”方法的参数上加上MultipartFile stuPic
:
在方法内加一句stu.setStuPic(stuPic.getBytes());
:
就可以了。
那么对于表单上传上来的文件,我们准备好一个多段文件,文件名和网页上的name是一致的:
它就会自动的传上去之后,在“createStudent”方法中作为参数被解析出来。把字节都取出来,填充的字段上。接下来是保存。
保存
保存时是Dao层,Dao层的实现类是没有的,要去修改映射文件
来到StudentMapper,修改addStu:
<insert id="addStu" parameterType="Student">
insert into tbl_student
values(#{stuNo},#{stuName},#{stuMark},#{stuPic})
</insert>
我们是按照顺序写的,所以没有必要写出字段名。到时候会从Student中取到stuPic送到对应字段上。
字节数组映射到blob字段是Mybatis所支持的。要不然要对框架进行定制,根据框架提供的插件开发机制,使用接口,在框架上再配置一下。
测试:
来到控制器设个断点:
点击“新生注册”
他还是空。
现在,我们的表单已经设成了多段,控制器中也准备了多段文件。
那么,表单中的file类型的输入控件,怎么样才能变成学生控制器中的多段文件呢?要提供一个部件,让springmvc知道怎么处理多段提交的文件。
在dispatcher-servlet.xml添加:
<!-- 遇到多段提交数据,一律使用这个解析器去处理,其会调用commons-fileupload去解决问题 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--上传文件的最大大小,单位为字节 -->
<property name="maxUploadSize" value="17367648787"></property>
<!-- 上传文件的编码 -->
<property name="defaultEncoding" value="UTF-8"></property>
</bean>
必须要加上多段解析器,否则springmvc将无法对多段数据进行处理。定义了一个springmvc所用的bean,只要是多段提交,这个bean都将被启动。
因为学生的属性是“stuPic”和网页的name的“stuPic”是同名的,他优先往学生的字段里面送。系统就不写了。
将网页的name的“stuPic”更名为“stuPhoto”,同时,将控制器中的多段文件名也改为“stuPhoto”
测试:
在debug窗口可以看到:
在网页可以看到:
图片并没有在网页列表中显示,在mysql中可以看到:
后面的字段是图片。
接下来是图片怎么在网页显示出来
将图片放在姓名上面,
修改list_student.jsp:
原来:
现在:
效果:
调小一点:
现在暂时都用默认图片,那么怎么把图片从数据库取出来呢?
到Dao层进行一定处理
在StudentDao添加:
byte[] getPicByStuNo(int stuNo);
接下来就是写映射文件
在StudentMapper.xml添加:
<select id="getPicByStuNo" parameterType="int" resultType="byte[]">
select stu_pic
from tbl_student
where stu_no=#{stuNo}
</select>
传进来的是整型参数,传出去都是字节数组,
在Service层进行修改:
在StudentService添加:
byte[] loadStuPhoto(int stuNo);
在StudentServiceImpl实现一下:
@Override
public byte[] loadStuPhoto(int stuNo) {
return stuDao.getPicByStuNo(stuNo);
}
回到springmvc,回到控制器:
要加载某个学生的照片,在专门写一个方法,需要HTTPResponse来做这个事情,在参数上写就好:
@GetMapping("/students/{stuNo}/photo")
public String loadStuPic(@PathVariable int stuNo,HttpServletResponse response) throws Exception{
byte[] data = stuService.loadStuPhoto(stuNo);
//MIME类型,在正式发送之前,要告诉他我发送的是什么东西。MIME是网页上的规范化文件识别机制,前面是大类型(image),后面是小类型(jpeg)。然后他就做好准备了,准备接收二进制数据,接收完二进制之后就以图片的方式去理解,就显示出图片了。
response.setContentType("image/jpeg");
ServletOutputStream sos=response.getOutputStream();
sos.write(data);
sos.flush();
sos.close();
return null;
}
HTTPResponse是通向浏览器的一个通道。
如果是这样的路径就会取到这个学生的照片
测试:
访问:http://localhost:8080/stuinfo/students/110/photo
来到StudentMapper,添加resultMap:
修改,并添加:
创建了一个resultMap,这个resultMap返回回来的类型是一个Map,整个resultMap的ID叫picMap。
要查的是“stu_pic”字段,用这个org.apache.ibatis.type.BlobTypeHandler转化器去转,转完之后以imgBytes为key存到java.util.Map里面去。
大致为:
<select id="getPicByStuNo" parameterType="int" resultMap="picMap">
select stu_pic
from tbl_student
where stu_no=#{stuNo}
</select>
<!--
mediumblob -> byte[] 利用BlobTypeHandler,自定义转换器
type="java.util.Map" 返回的类型是key/value键值对存储的map
column="stu_pic" 取的字段
property="imgBytes" map中的key
-->
<resultMap type="java.util.Map" id="picMap">
<id column="stu_pic" property="imgBytes" typeHandler="org.apache.ibatis.type.BlobTypeHandler"/>
</resultMap>
修改StudentDao:
修改StudentServiceImpl:
@Override
public byte[] loadStuPhoto(int stuNo) {
return (byte[])stuDao.getPicByStuNo(stuNo).get("imgBytes");
}
测试:
要将mediumblob转成byte[] 但是不能自动转 ,所以利用Mybatis提供的类型转换器 BlobTypeHandler,如果以后的转化Mybatis没有提供就要自定义转换器。就是“MyBatis自定义类型处理器 TypeHandler”
type="java.util.Map" 返回的类型是key/value键值对存储的map
column="stu_pic" 取的字段
property="imgBytes" map中的key
在插入时可以直接在旁边写上类型转换器:
结果转换就不行了,要写resultMap了。
我们原来的返回值是一个我们自定的类型(Student),但是返回一张照片的话是什么类型?什么类型都不是,所以使用resultMap后返回值是(picMap),是key/value键值对存储的map:
现在还要考虑没有图的情况
在StudentServiceImpl中修改:
@Override
public byte[] loadStuPhoto(int stuNo) {
Map picMap = stuDao.getPicByStuNo(stuNo);
if(picMap!=null)
return (byte[])picMap.get("imgBytes");
else {
return null;
}
}
在控制器中修改:
@GetMapping("/students/{stuNo}/photo")
public String loadStuPic(@PathVariable int stuNo,HttpServletResponse response) throws Exception{
//这样就得到了数据了
byte[] data = stuService.loadStuPhoto(stuNo);
//数据怎么带回去是一个问题
if(data!=null) {
//给response设置一下返回什么东西
response.setContentType("image/jpeg");
//从response身上得到一个输出流
ServletOutputStream sos = response.getOutputStream();
sos.write(data);
sos.flush();
sos.close();
}else {
System.out.println("no image");
}
//上面已经把数据输出去了,就没有必要return了
return null;
}
在list_student中修改:
在list的位置:
<img src="/stuinfo/students/${stu.stuNo}/photo" width="100px" height="100px"/>
拼装路径。
效果:
新增:
那么 ,没有图的怎么办呢?
在没有图片时,采取的方法是使用默认图片:
对控制器部分修改:
@GetMapping("/students/{stuNo}/photo")
public String loadStuPic(@PathVariable int stuNo,HttpServletRequest request,HttpServletResponse response) throws Exception{
//这样就得到了数据了
byte[] data = stuService.loadStuPhoto(stuNo);
if(data==null || data.length==0){//去取数据,发现什么都没取到,说明是没东西的。将默认图片替换进去。下面再去读是data是一定有东西的。
String defaultPicPath = request.getRealPath("/")+"resources/pics/default.png"; //获取网址“pics/default.png”,真实磁盘路径。“request.getRealPath("/")”是获取当前web应用程序所对应的根路径所对应的物理路径
FileInputStream fis =new FileInputStream(defaultPicPath);//创建一个文件输入流,把文件读取出来
data=new byte[fis.available()];//根据文件体积大小,创建字节数组
fis.read(data);//从fis中读出数据存到data中来,data就拥有了默认图片的数据
fis.close();
}
//数据怎么带回去是一个问题
//给response设置一下返回什么东西
response.setContentType("image/jpeg");
//从response身上得到一个输出流
ServletOutputStream sos = response.getOutputStream();
sos.write(data);
sos.flush();
sos.close();
//上面已经把数据输出去了,就没有必要return了
return null;
}
data的数据获取方式只有两个,无论怎样都有数据:
效果:
第二种方法是在网页端判断是否有图片。
在修改学生时,显示图片:
在update_student添加:
<div class="form-group">
<label>学生照片:</label><br>
<img class="my-3" src="/stuinfo/students/${stu.stuNo}/photo" width="100px" height="100px"/>
<input type="file" class="form-control-file" name="stuPhoto"><!-- 用EL表达式来回填的 -->
</div>
测试:
增加修改完图片的预览:
在update_student开头添加:
function previewImage(file){
//获得这个图片
var img = document.getElementById('picImg');
//确认file有文件,且第一个文件不为空,做相关的事情
if (file.files && file.files[0])
{
//准备一个文件读取器对象,并告诉它文件读取完毕之后要做什么。
var reader = new FileReader();
//成功读取了图片信息后,把读取结果赋予
//FileReader.readAsDataURL()
//开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的字符串以表示所读取文件的内容。
reader.onload = function(evt){
img.src= evt.target.result;
console.log("read ok! img src get value!"+evt.target.result);
}
console.log("start to read!")
reader.readAsDataURL(file.files[0]);
}
else{
img.src="/stuinfo/resources/pics/default.png";
// alert("no upload file!");
}
}
修改图片显示,大致如下:
<div class="form-group">
<label>学生照片:</label><br>
<img id="picImg" class="my-3" src="/stuinfo/students/${stu.stuNo}/photo" width="100px" height="100px"/>
<input type="file" class="form-control-file" name="stuPhoto" onchange="previewImage(this)"><!-- 用EL表达式来回填的 -->
</div>
测试:
现在只是预览,还没有上传上去,接下来,实现上传:
PUT提交不支持,要加上多段,没有多段解析器处理不来:
Mapper那边存的时候是自动处理的。
修改update_student:
enctype="multipart/form-data"
修改控制器:
@PutMapping("/students/{stuNo}")
public String updateStudent(Student stu, @PathVariable int stuNo,MultipartFile stuPhoto) throws Exception{
if(stuPhoto!=null)
stu.setStuPic(stuPhoto.getBytes());
stuService.updateStudent(stu);
return "redirect:/students";
}
测试:
在多个页面都要用到预览的脚本,那就将其做成外挂的模式:
在静态资源文件夹resources下,新建js文件夹,在其下新建common.js,将重复的脚本复制进来:
在list_student.jsp添加:
<script type="text/javascript"
src="/stuinfo/resources/js/common.js"></script>
来到修改界面:
添加:
删除原来的脚本。
测试:
点的到:
测试预览OK。在多段提交时,过滤器没有办法把post提交转换成put提交。
现在把update_student.jsp的put提交拿掉:
回到学生控制器:
将修改的PUT提交给为post提交:
检查没有发现提交方式与网址和这个方法相同的情况。没有冲突。
测试填充数据能不能修改:
改了。
但是现在如果只是修改名字,照片不改:
原因:
如果没有选择照片,就是空。
来到控制器:
修改判断条件,判断是有东西的我才往里面塞。
否则就是用户并没有去修改照片,认为照片没有问题,直接用数据库里的照片就好了。再把数据库的照片取出来填上。最理想的方式是
:在修改时没有更新图片时,sql的:
这个部分就不生成。就不会修改照片了。这就是动态sql语句。
@PostMapping("/students/{stuNo}")
public String updateStudent(Student stu, @PathVariable int stuNo,MultipartFile stuPhoto) throws Exception{
if(stuPhoto.getBytes()!=null&&stuPhoto.getBytes().length>0)
stu.setStuPic(stuPhoto.getBytes());
else {
stu.setStuPic(stuService.loadStuPhoto(stuNo));
}
stuService.updateStudent(stu);
return "redirect:/students";
}
测试没有修改的情况下是OK的。