有时候写博客时需要插入图片,提示可选择图片链接,于是就想设计一个存储图片的服务器,用来保存我们需要的图片,并且每一张图片对应唯一的URL地址,用户可直接使用URL将图片在网页上上传,方便我们使用。
数据存储模块,一方面在数据库中存储图片的属性信息,另一方面,将图片的正文信息以文件形式直接存储到磁盘上,所以数据库中记录一个 path 对应到磁盘上的文件。
建表语句:
CREATE TABLE `image_table` ( `imageId` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT, `imageName` varchar(50) DEFAULT NULL, `size` int(11) DEFAULT NULL, `uploadTime` varchar(50) DEFAULT NULL, `contentType` varchar(50) DEFAULT NULL, `path` varchar(1024) DEFAULT NULL, `md5` varchar(1024) DEFAULT NULL )md5是什么?(MD5_百度百科) 这是一种常见的字符串 hash 算法,具有三个特性:
不管源字符串多长,得到的 md5 都是固定的长度;源字符串稍微变化一点点内容,md5 值会发生很大的改变;计算 md5 值的过程很简单,但是通过 md5 值几乎无法推测出源字符串。查看表结构(desc image_table;)如下:
Json 是一种常见的数据格式组织方式,源于 JavaScript ,是一种键值对风格的数据格式。Java 中可以使用 Gson 库来完成 Json 的解析和构造。
在 Maven 中新增 Gson 的依赖:
<dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.2</version> </dependency>创建一个单例类辅助创建连接,其中 URL 为数据库连接字符串,用户名和密码都是固定的。
private static final String URL = "jdbc:mysql://127.0.0.1:3306/java_image_server?characterEncoding=utf8&useSSL=true"; private static String USERNAME ="root"; private static String PASSWORD ="";这个类主要包含三个方法:
//这是一个获取单例的方法 public static DataSource getDataSource(){ } //获取链接 public static Connection getConnection() { } //关闭链接 public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) { }类的实现代码:
public class DBUtil { private static final String URL = "jdbc:mysql://127.0.0.1:3306/java_image_server?characterEncoding=utf8&useSSL=true"; private static String USERNAME ="root"; private static String PASSWORD =""; //③加 volatile ,保持属性是内存可见的,使第一个线程进行操作后,其他线程可以及时看到更新 private static volatile DataSource dataSource=null; //线程安全问题:①加锁 ②双重判断 ③加volatile public static DataSource getDataSource(){ //通过这个方法创建 DataSource 的实例 if(dataSource==null){//②双重判断(加锁操作是一种比较耗时、低效的操作,双重判断就是希望不要频繁的操作) synchronized (DBUtil.class){//①加锁 if(dataSource==null){ dataSource=new MysqlDataSource(); MysqlDataSource tmpDataSource=(MysqlDataSource)dataSource; tmpDataSource.setURL(URL); tmpDataSource.setUser(USERNAME); tmpDataSource.setPassword(PASSWORD); } } } return dataSource; } //获取数据库连接对象 public static Connection getConnection() { try { return getDataSource().getConnection(); } catch (SQLException e) { e.printStackTrace(); } return null; } //进行关闭操作(顺序很重要,先打开的后关闭) public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) { try { if(resultSet!=null){ resultSet.close(); } if(statement!=null){ statement.close(); } if(connection!=null){ connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } }测试一下 insert 方法:
public static void main(String[] args) {//用于简单的测试 //1、测试插入数据 Image image=new Image(); image.setImageName("1.png"); image.setSize(100); image.setUploadTime("20200820"); image.setContentType("image/png"); image.setPath("./data/1.png"); image.setMd5("11223344"); ImageDao imageDao=new ImageDao(); imageDao.insert(image); }在数据库中查看是否插入成功。
测试一下selectAll 方法:
public static void main(String[] args) {//用于简单的测试 //2、测试查找所有图片信息 ImageDao imageDao=new ImageDao(); List<Image> images=imageDao.selectAll(); System.out.println(images); }测试一下 selectOne 方法:
public static void main(String[] args) {//用于简单的测试 //3、测试查找指定图片信息 ImageDao imageDao=new ImageDao(); Image image=imageDao.selectOne(1); System.out.println(image); }测试一下 delete 方法:
public static void main(String[] args) {//用于简单的测试 //4、测试删除指定图片 ImageDao imageDao=new ImageDao(); imageDao.delete(1); }在数据库中查看是否删除成功。
首先在项目根目录下创建一个 servlet 包,在这个包中创建两个 Servlet 类,一个用来完成图片的增删改查 (ImageServlet) ,一个用来展示图片的详细内容 (ImageShowServlet) 。
同时要记得把这个类加到 web.xml 中,其中类名要写完整的带包的名字。
<servlet> <servlet-name>ImageServlet</servlet-name> <servlet-class>api.ImageServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ImageServlet</servlet-name> <url-pattern>/image</url-pattern> </servlet-mapping>该方法对应上传图片,这里需要用到 Commons FileUpload, 可以在 Maven 仓库中找到这个包, 并且使用 maven 下载。
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>用 upload.html 实现上传:
<html> <head> </head> <body> <form id="upload-form" action="image" method="post" enctype="multipart/form-data" > <input type="file" id="upload" name="upload" /> <br /> <input type="submit" value="Upload" /> </form> </body> </html>实现 doPost 方法:
/** * 上传图片 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // resp.setContentType("text/html;charset=utf-8");//让浏览器以 UTF-8 的方式解析 // resp.setStatus(200); // resp.getWriter().write("hello"); //1、获取图片的属性信息,并且存入数据库 //(1)需要创建一个 factory 对象和 upload 对象,这是为了获取图片属性做的准备工作(固定的逻辑) FileItemFactory factory=new DiskFileItemFactory(); ServletFileUpload upload=new ServletFileUpload(factory); //(2)通过 upload 对象进一步解析请求(解析 HTTP 请求中奇怪的 body 中的内容) //FileItem 就代表一个上传的文件对象。 // 理论上来说,HTTP 支持一个请求中同时上传多个文件 List<FileItem> items=null; try { items=upload.parseRequest(req); } catch (FileUploadException e) { //出现异常说明解析出错 e.printStackTrace(); //告诉客户端具体的错误 resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write("{\"ok\":false,\"reason\": \"请求解析失败\"}"); return; } //(3)把 FileItem 中的属性提取出来,转换成 Image 对象,才能保存到数据库中 FileItem fileItem=items.get(0);// 当前只考虑一张图片的情况 Image image=new Image(); image.setImageName(fileItem.getName()); image.setSize((int)fileItem.getSize()); // 手动获取一下当前的日期,并转换成格式化日期,yyyyMMdd==>年月日 SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyyMMdd"); image.setUploadTime(simpleDateFormat.format(new Date())); image.setContentType(fileItem.getContentType()); //计算 MD5 image.setMd5(DigestUtils.md5Hex(fileItem.get())); // 自己构造一个路径来保存,引入时间戳是为了让文件路径能够唯一 //image.setPath("./image/"+System.currentTimeMillis()+"_"+image.getImageName()); image.setPath("./image/"+image.getMd5()); // 存到数据库中 ImageDao imageDao=new ImageDao(); //看看数据库中是否存在相同的 MD5 值的图片,不存在,返回null Image existImage=imageDao.selectByMd5(image.getMd5()); imageDao.insert(image); //2、获取图片的内容信息,并且写入到磁盘文件 if (existImage==null) { File file=new File(image.getPath()); try { fileItem.write(file); } catch (Exception e) { e.printStackTrace(); //告诉客户端具体的错误 resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write("{\"ok\":false,\"reason\": \"写磁盘失败\"}"); return; } } //3、给客户端返回一个结果 // resp.setContentType("application/json;charset=utf-8"); // resp.getWriter().write("{\"ok\"}:true"); resp.sendRedirect("index.html"); }验证该方法,可以使用刚写的 upload.html ,上传一张图片,检查服务器响应是否正确,数据库是否写入成功,图片文件是否上传成功。
这里分两种情况,一个是获取所有图片信息,一个是获取单个图片信息,根据请求中是否带有 imageId 参数来决定。
/** * 查看图片属性:既能查看所有,也能查看指定图片 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //HttpServletRequest req —— 请求(方法,URL,各种header,body),包含了请求中的所有信息 //HttpServletResponse resp —— 响应(状态码,各种header,body),要生成的结果就放到里面去 //当前这个 doGet 方法就是要根据请求,生成响应 //在网页上显示一个 hello world ,应该修改 “响应”(响应的body部分) // resp.setStatus(200); // resp.getWriter().write("hello");//这个代码就是把 hello 这个字符串放到 http 响应的 body 中了 //考虑到查看所有图片属性和查看指定图片属性 //通过 URL 中是否带有 imageId 参数来进行区分 //存在 imageId 查看指定图片属性,否则就查看所有图片属性 String imageId=req.getParameter("imageId");//得到的是 String 类型的数据,如果 URL 中不存在 imageId 那么返回 null if(imageId==null||imageId.equals("")){//不存在 imageId 或者 imageId 为空字符串 //查看所有图片属性 selectAll(req,resp); }else{ //查看指定图片属性 selectOne(imageId,resp); } }修改 web.xml :
<servlet> <servlet-name>ImageShowServlet</servlet-name> <servlet-class>api.ImageShowServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ImageShowServlet</servlet-name> <url-pattern>/imageShow</url-pattern> </servlet-mapping>直接在网上搜索免费的网页模板,将其解压缩,拷贝到项目的 webapp 目录中。删除模板中不需要的部分,保留自己所需要的部分。
(参考 Vue 文档)
创建 Vue 对象:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })构造数据:
修改 js 代码:
var app = new Vue({ el:'#app', data: { images: [ { imageId: 1, imageName: "1.png", contentType: "image/png", md5: "aabbccdd", }, { imageId: 2, imageName: "2.png", contentType: "image/png", md5: "aabbccdd", } ] }, methods: { }, });修改 html 代码,和数据关联:
使用 v-bind:src 把图片的 src 通过 imageShow 接口获取到使用 {{image.imageName}} 表示图片标题 <div class="am-g am-g-fixed blog-fixed blog-content" id="app"> <figure data-am-widget="figure" class="am am-figure am-figure-default " data-am-figure="{ pureview: 'true' }"> <div id="container"> <div v-for="image in images"> <img style="width: 200px;height: 200px" v-bind:src="'imageShow?imageId='+image.imageId"> <h3>{{image.imageName}}</h3> </div> </div> </figure> </div>从服务器获取数据:
在 method 中新增获取所有图片的方法:
getImages(){ $.ajax({ url:"image", type:"get", context:this, success:function(data,status) { //此处的代码在浏览器收到响应后,才会执行到 //参数中的 data 就相当于收到的 HTTP 响应中的 body 部分 this.images=data; $('#app').resize(); } }) } //页面加载时调用 app.getImages();部署到服务器上,测试效果。
当前的上传请求会返回一个 JSON 格式的数据,而我们更需要的是直接能看到上传的效果,所以修改上传接口的响应,直接返回一个 302 响应,重定向回主页。
修改 ImageServlet.doPost ,在上传成功代码最后,加上一个重定向。
resp.sendRedirect("index.html");图片下面新增删除按钮:
<button style="width: 100%" v-on:click="remove(image.imageId)" class="am-btn am-btn-success">删除</button>实现事件处理函数:
remove(imageId){ $.ajax({ url:"image?imageId=" + imageId, type:"delete", context:this, success:function (data,status) { this.getImages(); //弹出对话框 alert("删除成功!"); } }) }验证删除效果。
阻止点击事件冒泡:
此时发现个问题, 点击删除按钮之后, 会触发预览图片效果。这是因为 JavaScript 的事件冒泡机制导致的.,一个标签接受到的事件会依次传给父级标签。此处需要阻止 click 事件冒泡,Vue 中使用 v-on:click.stop 即可。
<button style="width: 100%" v-on:click.stop="remove(image.imageId)" class="am-btn am-btn-success">删除</button>通过 HTTP 中的 refer 字段判定是否是指定网站请求图片,修改 ImageShowServlet.doGet 方法。
新增属性:
static private HashSet<String> whiteList=new HashSet<>(); static { whiteList.add("http://127.0.0.1:8085/java_image_server/index.html"); }新增以下逻辑:
String referer=req.getHeader("Referer"); if(!whiteList.contains(referer)){ resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write("{\"ok\":false,\"reason\":\"未授权的访问\"}"); return; }整体思路:
修改上传图片代码, 使用 md5 作为文件名;修改 DAO 层代码, 在 DAO 层实现一个 selectByMD5 方法, 根据 MD5 来查找数据库中的图片信息;修改上传图片代码, 存储文件时先判定, 该 md5 对应的文件是否存在, 存在就不必写磁盘了;修改删除图片代码, 先删除数据库记录, 删除完毕后, 看数据库中是否存在相同 md5 的记录. 如果不存在, 就删除磁盘文件。实现计算 MD5:
修改 pom.xml ,引入依赖
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.14</version> </dependency>修改 ImageServlet.doPost 方法,实现计算 MD5。
image.setMd5(DigestUtils.md5Hex(fileItem.get())); image.setPath("./image/"+image.getMd5());修改 ImageDao :
新增方法 selectByMD5:
//通过 MD5 查找数据库内容 public Image selectByMd5(String md5){ //1、获取数据库连接 Connection connection=DBUtil.getConnection(); //2、构造 SQL 语句 String sql="select * from image_table where md5=?"; PreparedStatement statement=null; ResultSet resultSet=null; //3、执行 SQL 语句 try { statement=connection.prepareStatement(sql); statement.setString(1,md5); resultSet=statement.executeQuery(); //4、处理结果集 if(resultSet.next()){//查找结果只有一条,所以用 if / while 都可以 Image image=new Image(); image.setImageId(resultSet.getInt("imageId")); image.setImageName(resultSet.getString("imageName")); image.setSize(resultSet.getInt("size")); image.setUploadTime(resultSet.getString("uploadTime")); image.setContentType(resultSet.getString("contentType")); image.setPath(resultSet.getString("path")); image.setMd5(resultSet.getString("md5")); return image; } } catch (SQLException e) { e.printStackTrace(); }finally { //5、关闭连接 DBUtil.close(connection,statement,resultSet); } return null; }根据 MD5 决定写入文件:
修改 ImageServlet.doPost 方法,如果该 MD5 值的文件不存在, 才真的写入磁盘;如果该 MD5 值的文件存在,则直接使用原来的文件,不必再写一次磁盘。
ImageDao imageDao=new ImageDao(); //看看数据库中是否存在相同的 MD5 值的图片,不存在,返回null Image existImage=imageDao.selectByMd5(image.getMd5()); imageDao.insert(image); //2、获取图片的内容信息,并且写入到磁盘文件 if (existImage==null) { File file=new File(image.getPath()); try { fileItem.write(file); } catch (Exception e) { e.printStackTrace(); //告诉客户端具体的错误 resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write("{\"ok\":false,\"reason\": \"写磁盘失败\"}"); return; } } //3、给客户端返回一个结果 // resp.setContentType("application/json;charset=utf-8"); // resp.getWriter().write("{\"ok\"}:true"); resp.sendRedirect("index.html");根据 MD5 决定删除文件:
多个图片对应一个文件,删除任何一个图片,都会导致文件被删除,该怎么办呢?
其实通过 selectByMd5 值对应的图片在数据库中是否存在,如果不存在这个 MD5 ,才真正删除磁盘文件。
整体框架: