1)现在我们还剩下最后一个API设计,叫做查看文件的内容:
package API; import Dao.Image; import Dao.OperateImage; import com.fasterxml.jackson.databind.ObjectMapper; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.Scanner; @WebServlet("/ShowImage") public class ShowImageServlet extends HttpServlet { static class Response{ public int OK; public String reason; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1解析出ImageID ObjectMapper objectMapper=new ObjectMapper(); String ImageID=req.getParameter("ImageID"); if(ImageID==null||ImageID.equals("")) { Response response=new Response(); response.OK=3; response.reason="当前我们前端传递的参数有问题,我们无法进行查询一张图片看到具体信息的操作"; resp.setContentType("application/json;charset=utf-8"); String html = objectMapper.writeValueAsString(response); resp.getWriter().write(html); return; } //2根据ImageID来进行查询数据库,从而得到图片的属性信息(主要是我们想知道图片存储的路径); OperateImage operateImage=new OperateImage(); Image image= null; try { image = operateImage.SelectOne(Integer.parseInt(ImageID)); } catch (SQLException throwables) { throwables.printStackTrace(); } if(image==null||image.equals("")) { DeleteImageServlet.Response response=new DeleteImageServlet.Response(); response.OK=5; response.response="数据库中没有ImageID对应的图片信息,我们无法进行查看图片操作"+ImageID; resp.setContentType("application/json;charset=utf-8"); String html= objectMapper.writeValueAsString(response); resp.getWriter().write(html); return; } //3根据路径打开文件,读取到其中的响应数据,因为咱们的图片属于二进制,所以我们要按照字节流的方式来进行读取 InputStream inputStream=new FileInputStream(image.getContentPath()); StringBuilder stringBuilder=new StringBuilder(); while(true) { int len=inputStream.read(); if(len==-1) { break; } stringBuilder.append((char)len); } inputStream.close(); resp.setContentType(image.getContentType()); resp.getWriter().write(stringBuilder.toString()); } }
2)如果我们这时候把图片上传到图片服务器上面,我们就可以在其它网站上面通过url来进行引用这个图片了
3)resp.setContentType(image.getContentType());设置服务器返回的HTTP数据响应报文是一个图片类型,要不然你给浏览器返回大量的图片二进制数据,浏览器就无法进行识别了
2.实现前端页面,让我们的网页变得更好看
rm java_server* -rf
一:我们为了实现页面的上传:
1)把搜索框改成了文件上传按钮
2)新增了一个提交按钮
3)修改了form属性,新增action(后端服务器路径),method,enctype
二:发现一个小问题,代码中访问List<FileItem>,发现了数组下标越界,也就是出现了空指针异常,说明list数组是空,那么说明并没有包含与文件相关的内容,form表单写的不太对,一定要加上name属性
三:比如说现在有一个div标签:
<div>
<input type="submit" value="提交">
</div>
现在我们想要调整这一块input按钮的高度,那么就可以改一下
<input type="submit" style="height:60px">
四)循环展示图片:原来代码是这样的,目标把网络上显示的这些预览图片替换成服务器展示的图片,循环进行渲染;
只需要把img标签中的src属性改成服务器存放的文件路径的地址就可以了
从network里面去看加载的属性,比如说去查看src的属性加载是否正确
a标签会自动根据src中的属性向后端发送一个HTTP请求
我们把代码改成这样:
1)这样就可以显示一张图片了,但是我们当前代码里面只有一个img标签,但是实际上我们有多张图片,实际上也就应该有多个img标签,每一个img标签都有一个src属性,里面存放的就是对一个图片的路径
2)所以我们在这里面的做法就是需要获取到服务器上面所有图片的url,就是之前我们在后端写的获取到所有图片的属性信息,ImageServlet,需要先通过JS来进行获取到所有图片属性,再来分别加载每一个图片
3)此时我们应该是用Vue JS的框架来进行更方便的编写代码,引入vue,直接引入这一段文件即可,直接粘贴到head标签里面就可以了,此时我们的vue就下载安装成功了,自动会向服务器下载一些第三方的JS,在后面的代码中就可以直接使用
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
4)我们的JS的变量类型都是在初始化的时候进行自动推导的,var声明这是一个变量
vue框架的用法:
Vue vue=new Vue({}),里面的参数只有一个,那就是一个JSON格式的数据
1)el:表示我要把这个Vue对象关联到哪一个html标签上面,假设说el:"#app"表示把vue这个变量关联到id属性是app的这个HTML标签上;
2)data:里面是一个键值对,关联的变量名以及具体的值
在浏览器的控制台上面通过vue对象.data属性中间的键就可以得到里面的值,并且还可以进行修改里面的值,当我们进行修改vue对象.data属性的值的时候,对应的HTML页面中关联的属性也会发生变化;
3)methods代表着vue对象中的方法
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body > <div id="User">{{username}}</div> //最终这里面的username会被替换成vue中的username的值 <script> var app=new Vue({ el:"#User",//获取到里面的ID属性,指定选中的html标签 data:{ username:"李佳伟", } }); </script> </body> </html>
1)我们在浏览器的控制台上面输入app.username,就可以显示信息了,会自动替换div中的uaername信息
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body > <div id="User"> <div>{{username}}</div> <div>{{password}}</div> <div>{{number}}</div> </div> <!-- //最终这里面的username会被替换成vue中的username的值 --> <script> var app=new Vue({ el:"#User",//获取到里面的ID属性,指定选中的html标签 data:{ username:"李佳伟", password:"12503487", number:"生命在于运动" } }); </script> </body> </html>
2)上面还可以进行相应替换,所以说咱们vue所做的核心工作就是把页面HTML显示的内容和咱们JS中显示的内容紧紧的关联到一起,咱们修改对应的JS的变量就可以很方便的影响到页面的显示情况;
咱们还可以在控制随意修改,一敲回车就可以了,咱们的{{username}}这种写法就叫做差值表达式,里面的变量会自动替换成vue变量的内容,不需要手动干预,而是有框架自动完成的
1)我们首先先从vue对象中构造一组写死的images数据,我们先进行借助这些写死的数据来进行循环渲染页面
1)这里面div里面的v-for可以循环访问一组数据
2)这里面的v-bind是把某一个数据绑定到html标签上面的某一个属性
3)此时如果我们在标签内部使用Vue对象中的数据,就要使用差值表达式
4)如果说在标签属性里面使用Vue对象的数据,比如说src属性,不需要使用使用差值表达式,但是需要搭配Vue中的命令,v:bind;
5)v-on:绑定某一个事件的处理函数,点击鼠标,双击,右键,按下某个键,调整传功窗口大小,html在收到事件之后会进行一定的响应
<script> var app=new Vue({ el:"#container", data:{ images:[ { imageID:11, contentType:"image.png", contentPath:"images/1.png", imageName:"1.png", md5:"aabbcc" }, { imageID: 12, contentType:"image.png", contentPath:"images/2.png", imageName:"2.png", md5:"aabbcc" } ] } })
<div class="am-g am-g-fixed blog-fixed blog-content"> <figure data-am-widget="figure" class="am am-figure am-figure-default " data-am-figure="{ pureview: 'true' }"> <div id="container"> <!--这里面的image就是循环访问vue中的images里面的元素,类似于JAVA中的for each循环,images就相当于是一个数组,里面的images元素就相当于是一个image对象--> <div v-for="image in images"> <img v-bind:src="'ShowImage?ImageID='+image.imageID"> <h3>{{image.imageName}}</h3> </div> </div> </figure>
最终代码:
1)接下来我们可以通过浏览器中的JS代码请求服务器,获取到服务器上面有那些图片,把这个数据作为Vue渲染的依据
先让浏览器发送ajax请求从服务器获取到这个images数据,再进行渲染HTML中的元素
2)此处的代码是在浏览器在收到响应之后,才会执行到,参数中的data就是相当于是HTTP请求中的body部分;
var app=new Vue({ el:"#container", data:{ images:[]//最终会有ajax将data中的一个个的images数组赋值给images对象 }, methods:{ getImages(){//用来向服务器来进行获取数据,约定的的请求 $.ajax({ url:"Image", type:"GET", context:this, success:function(data,status){//此时的代码在浏览器收到响应之后,才会执行到,data就相当于是HTTP响应中的body部分 this.images=data; $("#container").resize(); } }); }, remove(ImageID){ $.ajax({ url:"Image?ImageID="+ImageID, method:"delete", context:this, success:function(data,status){ //再次尝试获取数据 this.getImages(); //弹出对话框 alert("删除成功"); } }) } } }); app.getImages();
需要进行渲染的源代码:
<div id="container"> <!--这里面的image就是循环访问vue中的images里面的元素 类似于JAVA中的for each循环,images就相当于是一个数组, 里面的images元素就相当于是一个image对象--> <div v-for="image in images"> <!--通过v-bind把vue某一个数据关联上html标签上面的一个属性 --> <img v-bind:src="'ShowImage?ImageID='+image.imageID"> <h3>{{image.imageName}}</h3> <!--通过v-on来给我们的按钮关联上一个点击事件,下面的vue代码会自动调用vue中的methods中的函数--> <button style="width:100% background-color=green" v-on:click="remove(image.imageID)"> 删除图片</button> </div> </div>
咱们此时的div预览图如下所示:
1)咱们点击div就会触发一个点击事件,div就会处理这个点击事件并显示预览图
2)咱们点击button也是触发一个点击事件,出发删除操作,div就会处理这个点击事件并显示预览图,
3)所以说点击button也是触发一个点击事件,触发删除操作
但是这个触发操作会顺着父亲的html标签结构向父亲的标签传递给div
div也会在进行处理一次;
4)这就是HTML事件冒泡机制,所以说我们要阻止事件冒泡: v-on:click.stop="";
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="user"> <div v-for="user in users"> <a v-bind:href="user.userhref">{{user.userhrefname}}</a> <div>{{user.username}}</div> <div>{{user.password}}</div> <input type="button" v-on:click="remove(user.userID)" v-bind:value="user.uservalue"> </div> </div> <script> var vue=new Vue({ el:"#user", data:{ users:[{ userID:1, username:"zhangsan", password:"12503487", userhref:"http://www.baidu.com", userhrefname:"百度网址", uservalue:"点击跳转到百度网页" }, { userID:2, username:"zhangsan", password:"12503487", userhref:"http://www.baidu.com", userhrefname:"搜狗网志", uservalue:"点击转到搜狗主页" } ] }, methods:{ remove(userID){ alert(userID); } } } ) </script> </body> </html>
有了上面的基本知识之后,我们就可以通过浏览器中的JS代码请求我们的后端服务器,来进行获取到服务器上面都有那些图片,把这个数据作为Vue渲染的依据(Vue中的images数组)
1)我们上面的vue中的el是根据ID来进行决定绑定哪一个html标签
2)我们上面中的vue的data是用来存放我们将要渲染的数据
3)我们下面的vues中的methods是用来存放我们的方法,这个方法用于给后端的Http服务器发送请求
4)下面咱们的ajax是构造HTTP请求发送给服务器的一种方式
项目扩展:
简单的图片防盗链机制:
1)因为你的图片链接可能会被其他人使用,我们在图片上面复制链接地址,再用浏览器打开就会能够访问到我们服务器上面的图片了
2)我们要通过一定的机制来限制其他人来进行使用图片,否则图片用的人太多,你的服务器就挂了
3)我们可以在后端代码中判断当前请求的referer字段是否来自我们指定的白名单里面,如果是,才可以进行访问,referer字段是请求协议的header头部分,表示当前请求的上个页面的地址,我们在代码中使用HashSet来进行存一下允许的Referer就可以,判断展示图片的时候看看是否在HashSet中存在即可
我们可以在获取图片具体内容的时候加上这些代码:
String ImageID=req.getParameter("ImageID"); ObjectMapper objectMapper=new ObjectMapper(); OperateImage operateImage=new OperateImage(); Response response=new Response(); HashSet<String> hashSet=new HashSet<>(); hashSet.add("http://127.0.0.1:8080/Java100/index1.html"); String referer=req.getHeader("Referer"); if(!hashSet.contains(referer)){ response.state=3; resp.setContentType("application/json;charset=utf-8"); System.out.println(referer); response.reason="您当前没有权限进行访问"; System.out.println("生命在于运动"); String html=objectMapper.writeValueAsString(response); resp.getWriter().write(html); return; }
优化磁盘存储空间:保证上传图片到磁盘里面的这个操作在插入图片到数据库之前
1)在这里面我们要应用到MD5,如果两个图片的内容完全一样,那么我们在磁盘上面只存一份图片就可以了,让两个图片在数据库中的保存路径是一样的
2)图片文件虽然是二进制数据,但是本质上也是字符串,我们需要针对图片内容计算MD5,如果说两个图片内容相同,得到的MD5一定相同的,反之,近似地认为MD5相同,那么原图片内容一定相同
图片内容相同,那么MD5值一定相同,如果图片内容相同
那么MD5的值一定不同,是一个16进制数字
3)理论上是有可能两个图片内容不同,MD5相同,但是实际上出现概率极低,这是因为MD5自身算法上设计所引起的特性
MD5的特点:
1)不管原串多长,得到的MD5是一个固定长度
2)原串哪怕变动一点点,哪怕变一个字节,那么MD5的就会变动很大
3)计算MD5值的过程很简单,但是通过MD5值是无法推测出原来的字符串的,应用到了MD5
实现思路:
在我们上传图片的时候,我们先进行判断一下新图片的MD5的值是否在数据库中已经存在,如果已经存在了,那么就不把图片内容写到磁盘上面,如果不存在MD5值,就写到磁盘里面
具体操作:
1)修改上传代码的逻辑,磁盘文件名用MD5来进行表示,就是说现根据文件内容来进行获取MD5,文件路径的后缀修改成MD5;
2)引入第三方库来进行计算一下MD5,直接在中央仓库里面进行搜索codec;
但是向数据库插入记录是必要的,无论根据MD5的值在数据库中是否查到记录,都应该向数据库中插入这个图片记录
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.14</version> </dependency>
list=upload.parseRequest(req); //3.创建Image对象 FileItem fileItem= list.get(0); Image image=new Image(); image.setImageName(fileItem.getName()); image.setImageSize((int)fileItem.getSize()); image.setContentType(fileItem.getContentType()); image.setMd5(DigestUtils.md5Hex(fileItem.get())); image.setContentPath("./image/"+image.getMd5()); //设置时间 SimpleDateFormat format=new SimpleDateFormat("yyyyMMdd"); image.setImageTime(new Timestamp(System.currentTimeMillis())); //4.将image对象插入到数据库里面 OperateImage operateImage=new OperateImage(); operateImage.InsertImage(image);
3)修改OperateImage,新增一个数据库接口,能够按照MD5的值来进行查找内容
public Image SelectByMD5(String md5) throws SQLException { Connection connection=ConnectionMYSQL.GetConnection(); String SQL="select * from Image where md5=?"; PreparedStatement statement=connection.prepareStatement(SQL); statement.setString(1,md5); ResultSet resultSet= statement.executeQuery(); while(resultSet.next()){ Image image=new Image(); image.setContentType(resultSet.getString("ContentType")); image.setImageSize(resultSet.getInt("ImageSize")); image.setImageID(resultSet.getInt("ImageID")); image.setContentPath(resultSet.getString("ContentPath")); image.setMd5(resultSet.getString("md5")); image.setImageName(resultSet.getString("ImageName")); image.setImageTime(resultSet.getTimestamp("ImageTime")); ConnectionMYSQL.Close(connection,statement,resultSet); return image; } return null; }
4)继续修改上传图片的逻辑,根据MD5来进行判定当前图片是否要写磁盘,原来的逻辑是无论如何都要进行修改图片,现在先进行查看这个MD5是否存在,再决定是否要写图片,MD5已经存在,就不需要写了
//这个POST方法用于上传图片 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.为了获取图片属性,写几个类 Response response=new Response(); ObjectMapper objectMapper=new ObjectMapper(); FileItemFactory factory=new DiskFileItemFactory(); ServletFileUpload upload=new ServletFileUpload(factory); //2.解析body内容 List<FileItem> list=null; try { list=upload.parseRequest(req); //3.创建Image对象 FileItem fileItem= list.get(0); Image image=new Image(); image.setImageName(fileItem.getName()); image.setImageSize((int)fileItem.getSize()); image.setContentType(fileItem.getContentType()); image.setMd5(DigestUtils.md5Hex(fileItem.get())); image.setContentPath("./image/"+image.getMd5()); //设置时间 SimpleDateFormat format=new SimpleDateFormat("yyyyMMdd"); image.setImageTime(new Timestamp(System.currentTimeMillis())); //4.将image对象插入到数据库里面 OperateImage operateImage=new OperateImage(); //5.将图片写到文件里卖弄 //看看数据库中是否存在具有相同的MD5值的图片,如果不存在,数据库操作直接就返回null Image exists= operateImage.SelectByMD5(image.getMd5()); System.out.println(exists); if(exists==null) { File file = new File(image.getContentPath()); System.out.println(file.getAbsoluteFile()); fileItem.write(file); resp.setContentType("application/json;charset=utf-8"); response.state = 0; response.reason = "将文件上传到指定目录成功"; //String html=objectMapper.writeValueAsString(response); } operateImage.InsertImage(image); resp.sendRedirect("index1.html"); } catch (FileUploadException | SQLException e) { resp.setContentType("application/json;charset=utf-8"); response.state=0; response.reason="解析body内容失败"; String html= objectMapper.writeValueAsString(response); resp.getWriter().write(html); e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } }
但是这里面又会出现问题
删除咋办?多个数据库记录对应着一个文件,删除任何一张图片,那么都会导致文件被删除掉了,删除逻辑的调整方式就是通过selectByMd5看看当前的md5的值对应的图片在数据库中是否存在,如果不存在这个md5才可以真正的的删除磁盘文件;
但是对于数据库中的表是自己写的
1)点击settings,搜索maven,全部勾中
2)确认settings文件是否存在,如果不存在,那么没有配置国内源,settings.xml
3)如果已经存在settings.xml文件,检查有没有配置国内源
<mirror> <id>mirrorId</id> <mirrorOf>repositoryId</mirrorOf> <name>Human Readable Name for this Mirror.</name> <url>http://my.repository.com/repo/path</url> </mirror>
1)如果配置了国内元,重新下载外部Jar包,还是下载不下来,是因为你本地仓库存储的jar包是不完整的,保存了下载了一半的外部jar包,再去下载maven不知道完整性,
2)把本地仓库的jar包全部删除掉(Local reposity)
还不能解决:重复前面的三个流程,多试几遍,一定会成功