引言:在QQ中更换头像时,点击上传本地照片时,看到下面这个窗口有了想法,自己是否也可以做出类似的图片服务器呢?(当然自己的肯定没有这个高大上~~~~~~)

一. 项目背景

  • 现在很多网页都可以见到上传图片的功能,假如我们上传一张本地图片后,网页就会显示我们刚刚所上传的图片,比如博客上传一张图、个人信息提交头像页面等等。那么这背后的原理是什么呢?
  • 其实当我们浏览网页的时候,本质上是从 对端服务器 获取资源,浏览器在获得这些资源之后进行解析和渲染,呈现在我们用户面前的就是绚丽多彩的页面了。
  • 一般来讲这些资源文档主要有三种格式:HTML、CSS、JS。HTML相当于网页的骨架,CSS相当于网页的衣服,用来规定网页的样式,比如字体大小以及排版等等,而JavaScript则主要负责一些动态的逻辑,比如在网页上按下一个按键后会显示什么等等。

二.涉及到的知识点

  1. 简单的 Web 服务器设计能力
  2. Java 操作 MySQL 数据库
  3. 数据库设计
  4. 响应风格 API
  5. gson 的使用
  6. HTTP 协议的理解
  7. Servlet 的使用
  8. 基于 md5 进行校验
  9. 软件测试的基本思想和方法

三.整体实现功能

核心就是实现一个 HTTP 服务器,实现对图片的增删查功能,同时搭配简单的页面辅助完成对图片的上传,显示,阅览功能。(实现一个 HTTP 服务器,然后用这个服务器来存储图片,针对每个图片提供一个唯一的 url,有了这个 url 之后就可以借助它把图片展示到其他网页上。)

  • 项目模块划分:
      本项目的结构主要分为两个部分,数据存储模块和服务器模块。使用MySQL存储图片的属性信息,将图片内容保存到本地磁盘,服务器向外提供诸如上传图片、获取单个图片的属性信息、获取所有图片的属性信息、和删除图片等API接口。

四.数据库设计


(1)imageId:图片的 id ,设置为自增主键
(2)imageName:图片名称
(3)size:图片大小
(4)uploadTime:图片的上传时间
(5)contentType:图片的类型(image/jpg,image/png等)
(6)path:图片在磁盘上的存储路径
(7)md5:一种计算校验和的算法(主要用来判断两张图片是否内容相同,即是否两张图片一样)

什么是 md5?
这是一种常见的字符串 hash 算法, 具有三个特性:

  1. 不管源字符串多长, 得到的最终 md5 值都是固定长度
  2. 源字符串稍微变化一点点内容, md5 值就会变化很大(降低冲突概率)
  3. 通过原字符串很容易计算得到 md5 值,但是根据 md5 推导出原字符串很难(几乎不可能)
  4. md5 常用于哈希算法,加密算法,校验和

五.服务器API设计

Json 是一种常见的数据格式组织方式,源于 JavaScript , 是一种键值对风格的数据格式。Java 中可以使用 Gson 库来完成 Json 的解析和构造. 在 Maven 中新增 对 Gson 库的依赖就可以使用 Json 了

<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.2</version>
</dependency>

简单实验(创建一个 TestGson 类):

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;import java.util.HashMap;class Hero {public String name;public String skill1;public String skill2;public String skill3;public String skill4;
}public class TestGson {public static void main(String[] args) {//        HashMap<String, Object> hashMap = new HashMap<>();
//        hashMap.put("name", "曹操");
//        hashMap.put("skill1", "剑气");
//        hashMap.put("skill2", "三段跳");
//        hashMap.put("skill3", "加攻击并吸血");
//        hashMap.put("skill4", "加攻速");Hero hero = new Hero();hero.name = "曹操";hero.skill1 = "剑气";hero.skill2 = "三段跳";hero.skill3 = "加攻击并吸血";hero.skill4="加攻速";// 通过 map 转成 JSON 结构的字符串// 1. 创建一个 gson 对象Gson gson = new GsonBuilder().create();// 2. 使用 toJson 方法把键值对结构转成 JSON 字符串String str = gson.toJson(hero);System.out.println(str);  //{"name":"曹操","skill1":"剑气","skill2":"三段跳","skill3":"加攻击并吸血","skill4":"加攻速"}}
}

1.新增图片(上传一个图片文件)

写一个简单的 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>

样式展示:

直接抓包,可以看到上传图片的 Http 请求的样式:

2. 设置各种响应格式

json 其最大的优点在于可以减少由于格式的错误导致程序异常,引用这个类可以自动严格按照JSON语法规则(syntax rules)创建JSON text。

(1)上传图片

假设上传图片的 HTTP 请求:

POST /image
Content-Type: application/x-www-form-urlencoded------WebKitFormBoundary5muoelvEmAAVUyQB
Content-Disposition: form-data; name="filename"; filename="图标.jpg"
Content-Type: image/jpeg......[图片正文].....

a)上传图片成功后的 HTTP 响应

HTTP/1.1 200 OK
{"ok": true
}

b)上传图片失败后的 HTTP 响应

HTTP/1.1 200 OK
{"ok": false,"reason":"失败原因"
}

(2)查看所有图片的属性信息

假设查看所有图片的属性信息的请求格式为:

GET /image

a)成功后的 HTTP 响应

HTTP/1.1 200 OK
[{"imageId": 1,"imageName": "1.png","contentType": "image/png","md5": "[md5值]"......},{"imageId": 1,"imageName": "1.png","contentType": "image/png","md5": "[md5值]"......},{"imageId": 1,"imageName": "1.png","contentType": "image/png","md5": "[md5值]"......}
]

b)失败后的 HTTP 响应

HTTP/1.1 200 OK
{"ok": false,"reason":"失败原因"
}

(3)查看指定图片的属性信息

假设请求格式为:

GET /image?imageId=10

a)成功后的 HTTP 响应

HTTP/1.1 200 OK
{"imageId": 1,"imageName": "1.png","contentType": "image/png","md5": "[md5值]"......
}

b)失败后的 HTTP 响应

HTTP/1.1 200 OK
{"ok": false,"reason":"失败原因"
}

(4)删除图片

假设请求格式为:

DELETE /image?imageId=10

a)成功后的 HTTP 响应

HTTP/1.1 200 OK
{"ok": true
}

b)失败后的 HTTP 响应

HTTP/1.1 200 OK
{"ok": false,"reason":"失败原因"
}

(5)查看指定图片的内容

假设请求为:

GET /imageShow?imageId=10

a)成功后的 HTTP 响应

HTTP/1.1 200 OK
content-type: image/png [响应 body 中为 图片的内容数据]

b)失败后的 HTTP 响应

HTTP/1.1 200 OK
{"ok": false,"reason":"失败原因"
}

六.创建一个JavaWeb 项目

(1)Maven 是什么?

  • Maven是 Apache 下的一个纯 Java 开发的开源项目,是一个项目构建和管理的工具;它提供了帮助管理、 构建、文档、报告、依赖、发布、分发的方法。可以方便的编译代码、进行依赖管理、管理二进制库等等。Maven是专门用于构建和管理Java相关项目的工具

(2)使用 Maven 的好处?

  • maven 的目标是完成项目构建解决一切繁琐事宜。我们具体关注它的以下功能:
    *提供一个标准的项目工程目录
    *提供项目描述
    *提供强大的版本管理工具
    *可以分阶段的进行构建过程
    *提供了丰富的插件库使用

七.封装数据库操作

dao 包下封装对数据库的相关操作

1.创建 DBUtil 类::创建一个单例类来辅助创建连接,关闭连接

private static final String URL = "jdbc:mysql://127.0.0.1:3306/java_image_server?characterEncoding=utf8&useSSL=true";              //数据库连接字符串
private static final String USERNAME = "root";    //用户名
private static final String PASSWORD = "123456";  //密码

类包含的三个方法:

// 这是一个获取单例的方法
public static DataSource getDataSource() { }
// 获取数据库链接
public static Connection getConnection() { }
// 关闭链接
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {}

类的实现代码:

package dao;import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;/***  与数据库相关操作有关*/
public class DBUtil {private static final String URL = "jdbc:mysql://127.0.0.1:3306/java_image_server?characterEncoding=utf8&useSSL=true";private static final String USERNAME = "root";private static final String PASSWORD = "123456";private static volatile DataSource dataSource = null;  //管理数据源/** (单例模式)*  通过这个方法来创建一个 DataSource 的实例* @return*/public static DataSource getDataSource() {if (dataSource == null) {   //双重校验锁// 此处的 synchronized 其实就是给当前这一小块加锁// synchronized 要么修饰一个方法,要么得传个对象,此处修饰整个方法的话锁的粒度太大synchronized (DBUtil.class) {if (dataSource == null) {dataSource = new MysqlDataSource();MysqlDataSource tmpDataSource = (MysqlDataSource) dataSource;tmpDataSource.setURL(URL);tmpDataSource.setUser(USERNAME);tmpDataSource.setPassword(PASSWORD);}}}return dataSource;}/***  这个方法用来获取一个数据库的连接对象* @return*/public static Connection getConnection() {try {return getDataSource().getConnection();} catch (SQLException e) {e.printStackTrace();}return null;}/***  这个方法用来关闭连接* @param connection* @param statement* @param resultSet*/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();}}
}

2.创建 Image 类

package dao;/***  一个 Image 对象:和数据库表的设计相对应*/
public class Image {private int imageId;private String imageName;private int size;private String uploadTime;    //图片上传时间private String contentType;   //图片的格式:image.jpg/image.png 等等private String path;           //图片应该往磁盘写的位置private String md5;            //校验和机制(用于判断两张图片的内容是否相同)public int getImageId() {return imageId;}public void setImageId(int imageId) {this.imageId = imageId;}public String getImageName() {return imageName;}public void setImageName(String imageName) {this.imageName = imageName;}public int getSize() {return size;}public void setSize(int size) {this.size = size;}public String getUploadTime() {return uploadTime;}public void setUploadTime(String uploadTime) {this.uploadTime = uploadTime;}public String getContentType() {return contentType;}public void setContentType(String contentType) {this.contentType = contentType;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}public String getMd5() {return md5;}public void setMd5(String md5) {this.md5 = md5;}@Overridepublic String toString() {return "Image{" +"imageId=" + imageId +", imageName='" + imageName + '\'' +", size=" + size +", uploadTime='" + uploadTime + '\'' +", contentType='" + contentType + '\'' +", path='" + path + '\'' +", md5='" + md5 + '\'' +'}';}
}

3.创建 ImageDao 类

package dao;
public class ImageDao {//把一个 image 对象插入到数据库中 public void insert(Image image){}//查找数据库中所有图片的属性信息,并存储在 List 中public List<Image> selectAll(){return null;   }//根据 imageId 查找指定图片的属性信息public Image selectOne(int imageId){return null;}//根据 imageId 删除指定图片public void delete(int imageId){}//按照 md5 的值来查找数据库中的 “图片”(图片的 md5 值是唯一的)public Image selectByMd5(String md5){return null;}
}

类的实现代码:

package dao;import common.JavaImageServerException;import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;/***  用来和数据库打交道的一个类*/public class ImageDao {/*** 把一个 Image 对象插入到数据库中*/public void insert(Image image){// 1.获取数据库连接Connection connection=DBUtil.getConnection();// 2.创建并拼装 SQL 语句String sql="insert into image_table values(null,?,?,?,?,?,?)";  // imageId 是自增主键,不需要赋值PreparedStatement statement=null;try {statement=connection.prepareStatement(sql);statement.setString(1,image.getImageName());statement.setInt(2,image.getSize());statement.setString(3,image.getUploadTime());statement.setString(4,image.getContentType());statement.setString(5,image.getPath());statement.setString(6,image.getMd5());// 3.执行 SQL 语句int ret=statement.executeUpdate();    //插入一条记录,影响到的行数应该为1if(ret!=1){//说明程序插入失败了,需要抛出一个异常throw new JavaImageServerException("插入数据库出错");}} catch (SQLException | JavaImageServerException e) {e.printStackTrace();}finally {// 4.关闭连接和statement对象(无论插入是否成功,都需要关闭连接)DBUtil.close(connection,statement,null);}}/*** 查找数据库中所有图片的属性信息,并存储在 List 列表中*/public List<Image> selectAll(){List<Image> images=new ArrayList<>();// 1.获取数据库连接Connection connection=DBUtil.getConnection();// 2.创建并拼装 SQL 语句String sql="select * from image_table";PreparedStatement statement=null;ResultSet resultSet=null;try {// 3.执行 SQL 语句statement=connection.prepareStatement(sql);resultSet =statement.executeQuery();  //得到查询结果集// 4.处理结果集 resultSet (把结果放到我们的 List 列表当中去)while(resultSet.next()){   //结果集中还有数据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"));images.add(image);}return images;} catch (SQLException e) {e.printStackTrace();}finally {// 5.关闭连接和 statement 对象,resultSet 对象DBUtil.close(connection,statement,resultSet);}return null;   //出错的话返回空的结果集}/*** 根据 imageId 查找指定图片的属性信息* @param imageId* @return*/public Image selectOne(int imageId){// 1.获取数据库连接Connection connection=DBUtil.getConnection();// 2.创建并拼装 SQL 语句String sql="select * from image_table where imageId = ?";PreparedStatement statement=null;ResultSet resultSet=null;try {// 3.执行 SQL 语句statement=connection.prepareStatement(sql);statement.setInt(1,imageId);resultSet=statement.executeQuery();  //查询// 4.处理结果集if(resultSet.next()){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.关闭连接和statement对象DBUtil.close(connection,statement,resultSet);}return null;   //如果失败的话返回null}/*** 根据 imageId 删除指定图片* @param imageId*/public void delete(int imageId){// 1. 获取数据库连接Connection connection=DBUtil.getConnection();// 2. 拼装 SQL 语句String sql="delete from image_table where imageId = ?";PreparedStatement statement=null;try {// 3. 执行 SQL 语句statement=connection.prepareStatement(sql);statement.setInt(1,imageId);int ret=statement.executeUpdate();  // 删除一条记录,那么影响的行数应该为1if(ret!=1){throw new JavaImageServerException("删除操作出错!");}} catch (SQLException | JavaImageServerException e) {e.printStackTrace();}finally {// 4. 关闭连接DBUtil.close(connection,statement,null);}}/***  按照 md5 的值来查找数据库中的 “图片”Image对象(图片的 md5 值是唯一的)* @param md5* @return*/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;try {// 3.执行 SQL 语句statement=connection.prepareStatement(sql);statement.setString(1,md5);    //md5 在数据库中是按字符串形式存储的,所以要调用 setString() 方法resultSet=statement.executeQuery();// 4.处理结果集if(resultSet.next()){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.关闭连接和statement对象DBUtil.close(connection,statement,resultSet);}return null;}// 如果你的数据库在云服务器上, 没有在本地服务器上. 这个程序在本地直接运行就会无法访问数据库// 此处需要把这个程序部署到云服务器上执行,才能看到效果// 打一个 jar 包, 把 jar 包拷贝到云服务器上, 就可以执行了public static void main(String[] args) {// 用于进行简单的测试// 1. 测试插入数据是否正确/*  Image image = new Image();image.setImageName("1.png");image.setSize(100);image.setUploadTime("20200217");image.setContentType("image/png");image.setPath("./data/1.png");image.setMd5("11223344");ImageDao imageDao = new ImageDao();imageDao.insert(image);*/// 2.测试查找所有图片信息/*  ImageDao imageDao=new ImageDao();List<Image> images=imageDao.selectAll();System.out.println(images);*/// 3.测试根据 imageId 查找指定图片的信息/*  ImageDao imageDao=new ImageDao();Image image=imageDao.selectOne(1);System.out.println(image);*/// 4.测试删除图片/*  ImageDao imageDao=new ImageDao();imageDao.delete(1);*/}
}

七.实现 Servlet


首先创建一个 api 包,在这个包中创建两个 Servlet 类,这两个Servlet 类都继承自 HttpServlet类,然后重写了对应的 doxxx() 方法。
一个用来完成图片的增删查操作(ImageServlet类)
一个用来展示图片的详细内容(ImageShowServlet类)
注意:要把这两个Servlet 类都要配置到 web.xml 文件中,其中类名要写完整的带包名的名字

1.创建 ImageServlet 类

这个类的 doPost() 对应插入图片操作, doGet() 对应查看图片属性操作,doDelete() 对应删除图片操作。

package api;public class ImageServlet extends HttpServlet {//查看图片属性信息: 既能查看所有图片的属性信息, 也能查看指定图片的属性信息protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  }// 查看所有图片的属性信息 private void selectAll(HttpServletRequest req, HttpServletResponse resp) throws IOException { }// 查看指定图片的属性信息private void selectOne(String imageId, HttpServletResponse resp) throws IOException {    }//上传图片 ( 包括上传到数据库,包括写到磁盘对应位置 )@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}// 删除指定图片@Overrideprotected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
}

(1)实现 ImageServlet.doPost()方法

这个方法对应上传图片操作,这里需要用到 Commons FileUpload,在Maven 仓库中找到该包,然后添加到依赖中去

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

(2)实现 ImageServlet.doGet()方法

这里要分成两种情况, 一个是获取所有图片的属性信息, 一个是获取单个图片的属性信息。根据请求中是否带有 imageId 参数来决定。

类的代码实现:

package api;import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dao.Image;
import dao.ImageDao;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;public class ImageServlet extends HttpServlet {/*** 查看图片属性: 既能查看所有图片的属性信息, 也能查看指定图片的属性信息* @param req* @param resp* @throws ServletException* @throws IOException*/@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {/*  考虑到 查看所有图片属性信息 和 查看指定图片属性信息通过 URL 中是否带有 imageId 参数来进行区分存在 imageId 则查看指定图片属性信息,否则就查看所有图片属性信息例如: URL为   /image?imageId=100    那么则查看 imageId 为100的图片的属性信息如果 URL 中不存在 imageId 等号后边的值, 那么返回 null*/String imageId=req.getParameter("imageId");if(imageId==null||imageId.equals("")){//查看所有图片属性信息selectAll(req,resp);}else{//查看指定图片属性信息selectOne(imageId,resp);}}/***  查看所有图片属性信息* @param req* @param resp* @throws IOException*/private void selectAll(HttpServletRequest req, HttpServletResponse resp) throws IOException {resp.setContentType("application/json; charset=utf-8");// 1.创建一个 ImageDao 对象,并且查找数据库ImageDao imageDao=new ImageDao();List<Image> images=imageDao.selectAll();// 2.把查找到的结果转成 JSON 格式的字符串,并且传给 resp 对象Gson gson=new GsonBuilder().create();// jsonData 就是一个 json 格式的字符串了// 下面的这行代码是核心操作,gson 帮我们自动完成了大量的格式转换工作String jsonData=gson.toJson(images);// 3.写响应 (在浏览器界面就可以显示 JSON 格式的字符串了)resp.getWriter().write(jsonData);}/***  查看指定图片属性信息* @param imageId* @param resp* @throws IOException*/private void selectOne(String imageId, HttpServletResponse resp) throws IOException {resp.setContentType("application/json; charset=utf-8");// 1.创建 ImageDao 对象ImageDao imageDao=new ImageDao();Image image=imageDao.selectOne(Integer.parseInt(imageId));// 2.创建 Gson 对象,使用 gson 对象吧查到的数据转成 json 格式Gson gson=new GsonBuilder().create();String jsonData=gson.toJson(image);// 3.写响应 (在浏览器界面就可以显示了)resp.getWriter().write(jsonData);}/*** 上传图片 ( 包括上传到数据库,包括写到磁盘对应位置 )* @param req* @param resp* @throws ServletException* @throws IOException*/@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// 1.获取图片的属性信息,并且存入数据库// a)需要创建一个 factory 对象和 upload 对象,这是为了获取图片的属性信息而做的准备工作,是固定的逻辑用法FileItemFactory factory=new DiskFileItemFactory();ServletFileUpload upload=new ServletFileUpload(factory);// 理论上来说,HTTP 支持一个请求中同时上传多个文件,FileItem 就代表上传的一个文件对象,多个文件用 List 组织List<FileItem> items=null;try {// b)通过 upload 对象进一步解析请求(解析 HTTP 请求中的 body 部分的内容)items=upload.parseRequest(req);} catch (FileUploadException e) {// 出现异常说明上传图片请求解析出错了,需要告诉客户端出现的具体错误是什么e.printStackTrace();resp.setContentType("application/json; charset=utf-8");resp.getWriter().write("{ \"ok\":false,\"reason\":\"上传图片请求解析出错\" }");return;}// c)把 List<FileItem> 中的属性提取出来,转换成 Image 对象,这样才能保存到数据库当中去FileItem fileItem=items.get(0);  //我们一次只上传一个,一个图片就是一个 FileItemImage image=new Image();image.setImageName(fileItem.getName());image.setSize((int)fileItem.getSize());//手动获取一下当前日期,并转成格式化日期,yyyyMMdd => 20200218SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyyMMdd");image.setUploadTime(simpleDateFormat.format(new Date()));image.setContentType(fileItem.getContentType());   //设置图片本身的类型image.setMd5(DigestUtils.md5Hex(fileItem.get()));// md5 值是十六进制的字符串image.setPath("./image/" + image.getMd5());// 最后存到数据库中ImageDao imageDao = new ImageDao();// 还没有插入前,先看看数据库中是否存在相同的 Md5 值的图片, 不存在返回 nullImage existImage = imageDao.selectByMd5(image.getMd5());//不管怎样都往数据库中插入,往数据库中可以插入多张重复的图片,但是尽管多张,但只在磁盘写一份就行imageDao.insert(image);// 2.如果没有插入这张图片前,磁盘没有存在这张图片,即 existImage==null,获取图片的内容信息,并且写入磁盘文件if (existImage == null) {File file = new File(image.getPath());   //往磁盘的那个位置存图片try {fileItem.write(file);   //把 fileItem 对应的这个图片文件写到相应位置} 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 }");// HTTP 采用302 可以重定向 到  index.html 页面// 本来显示的是 JSON 格式的数据,但是结果不直接,需要上传成功后跳转到主页面 index.htmlresp.sendRedirect("index.html");}/*** 删除指定图片* @param req* @param resp* @throws ServletException* @throws IOException*/@Overrideprotected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("application/json; charset=utf-8");// 1.先获取到请求中的 imageIdString imageId=req.getParameter("imageId");if(imageId==null||imageId.equals("")){resp.setStatus(200);resp.getWriter().write("{ \"ok\":false,\"reason\":\"解析请求中的 imageId 出错\" }");return;}// 2.创建 ImageDao 对象,先查看该 imageId 对应的图片是否存在,若存在则要删除//  既要删除数据库中的内容,也要删除磁盘中存储的对应图片,因此要知道该 imageId 对应的图片对象的存储路径ImageDao imageDao=new ImageDao();Image image=imageDao.selectOne(Integer.parseInt(imageId));if(image==null){    //说明输入的 imageId 是错误的,数据库中没有这个 imageId 对应的图片resp.getWriter().write("{ \"ok\":false,\"reason\":\"传入的 imageId 在该数据库中没有对应值\" }");return;}// 3.走到这说明数据库中有该 imageId 对应的图片,那么就要删除数据库中的记录imageDao.delete(Integer.parseInt(imageId));// 4.删除本地磁盘文件(注意:数据库中可能有多张相同的图片,但是磁盘中只写入了一张图片,删除要注意)//   刚刚已经从数据库中删除了那张图片,若此时数据库中还存有相同的图片,那么不应该删除磁盘图片文件//   相同图片的 md5 值是相同的Image existImage=imageDao.selectByMd5(image.getMd5());// 如果删除完毕后,数据库中没有相同的图片了,那么就删除磁盘文件,否则就不删除if(existImage==null) {File file = new File(image.getPath());file.delete();resp.getWriter().write("{ \"ok\":true }");}}
}

2.实现 ImageShowServlet 类

类的实现代码:

package api;import dao.Image;
import dao.ImageDao;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;public class ImageShowServlet extends HttpServlet {static private HashSet<String> whiteList=new HashSet<>();static{whiteList.add("http://127.0.0.1:8080/java_image_server/index.html");//你想要哪个网站可以访问你的图片,在 whiteList 中添加一下即可//  http://127.0.0.1:8080/java_image_server/imageShow?imageId=22 就不可以访问你的图片}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String referer=req.getHeader("Referer");// 如果你的白名单中没有存储这个网址,那就无法访问你的图片内容if(!whiteList.contains(referer)){resp.setContentType("application/json; charset=utf-8");resp.getWriter().write("{ \"ok\":false,\"reason\":\"该网站未授权访问\" }");return;}// 1.已授权,从请求中解析出 imageIdString imageId=req.getParameter("imageId");if(imageId==null||imageId.equals("")){resp.setContentType("application/json; charset=utf-8");resp.getWriter().write("{ \"ok\":false,\"reason\":\"传入的 imageId 是错误的,解析失败\" }");return;}// 2.根据 imageId 查找数据库中对应的图片,得到对应图片的属性信息 Path// (得到 path: 需要知道图片在磁盘上的存储路径 )ImageDao imageDao=new ImageDao();Image image=imageDao.selectOne(Integer.parseInt(imageId));if(image==null){  //没有该 imageId 对应的图片resp.getWriter().write("{ \"ok\":false,\"reason\":\"传入的 imageId 是错误的,没有对应的图片\" }");return;}// 3.path 已经得到,根据路径打开图片文件,读取其中的图片内容,写入到响应对象中resp.setContentType(image.getContentType());File file=new File(image.getPath());FileInputStream fileInputStream=new FileInputStream(file); //由于图片内容是二进制文件,应该使用字节流的方式来读取文件OutputStream outputStream=resp.getOutputStream();   //往响应中写byte[] buffer=new byte[1024];while(true){int len=fileInputStream.read(buffer); //把图片内容读到 buffer 字节数组中去if(len==-1){//文件读取结束(图片内容读取结束)break;}//此时已经读到了一部分数据,放到了 buffer 里,把 buffer 中的内容要写到响应对象中outputStream.write(buffer);}fileInputStream.close();  //关闭输入流outputStream.close();     //关闭输出流}
}

两个 Servlet 类已经实现,然后配置 web.xml 文件

八.写前端页面

1.使用 HTML 模板

直接在百度上搜索 “免费网页模板”, 能找到很多免费模板网站. 可以直接基于现成的漂亮的页面进行修改。

tips: 做减法比做加法更容易.

将网页模板解压缩, 拷贝到项目的 webapp 目录中,然后删除模板中不必要的目录和内置的图片

2.基于模板进行删减

修改之后的 html 代码为 (只截取修改部分的关键代码):

(1)head 标签的修改的

(2)页面的选择文件和提交按钮

注意:为了让 “提交按钮” 和前面的选择文件一样高, 这里加了一个style=“height:41px”
此处我们直接使用 style 属性调整样式, 这是一个简单粗暴的做法. 事实上专业的前端开发很少这样做, 因为代码的可维护性不好~~

(3)页面 Logo 的修改

(4)使用 Vue

Vue 是 JS 的框架,他做的核心工作是把页面显示的内容和 JS 中的代码相互关联到一起,修改 JS 中的变量就能很方便的影响到页面的显示情况。

基本用法:

var 声明这是一个变量,构造方法是使用了 JSON 形式的{} ,el 属性表示我要把当前这个对象关联上某一个 html 的标签(把 app 对象关联到 html 中一个 id=“app” 的标签)

例如:

添加的 JS 代码:

<script>var app=new Vue({// e1 属性表示我要把当前这个对象关联到某一个 html 的标签上(把 app 对象关联到 html 中的一个 id="app 的标签)el: '#app',data: {author:"Miao-hu",images: [    //一个集合数组]},methods:{// 请求格式:   GET /imagegetImages(){$.ajax({   // ajax 是在JS中构造 http 请求发送给服务器的一种实现方式url:"image",type:"get",context:this,success:function (data,status) {//此处的代码在浏览器收到响应之后,才会执行到//参数中的 data 就相当于收到的 http 响应中的 body 部分this.images=data;$('#app').resize();   //触发浏览器,让它自动调整大小,让图片正确显示}})},// 请求格式:   DELETE /image?imageId=remove(imageId){$.ajax({url:"image?imageId="+imageId,type:"delete",context:this,success:function (data,status) {this.getImages();   //只要删除图片,就重新获取一下服务器上剩余的图片,保证页面显示的图片数量正常alert("删除图片成功!");   //弹出一个对话框}})}}})app.getImages();         //项目只要访问 upload.html 就自动调用该方法
</script>


第一步: id=“app” 把 JS 代码和 该标签绑定
第二步:使用 v-bind:src 把图片的内容 通过 ImageShowServlet 接口获取到.
第三步:使用 {{image.imageName}} 表示图片的名称
第四步:v-for 能够循环访问一个数据


从服务器获取所有图片的数据:
在 methods 中新增获取所有图片数据的方法:

注意:

只要访问 index.html 文件就要加载服务器中的所有图片,所以只要访问 index.html 文件,就要调用该方法

(5)完善上传功能

当前的上传请求成功后会返回一个 JSON 格式的数据. 而我们需要的是直接能看到上传的效果,因此应该修改响应,直接重定向到 index.html 页面观察即可。
修改上传接口中的响应, 直接返回一个 302 响应, 重定向回主页即可。

修改 ImageServlet.doPost()方法

(6)实现删除图片

保证删除按钮的宽度和图片的宽度一样:style=“width:100%”

事件处理函数:

出现异常:
此时发现个问题, 点击删除按钮之后, 会触发预览图片效果。这是因为 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>

此程序的事件冒泡机制:div 标签中有 3 个字标签
①点击 div 就会触发一个点击事件,div 就会处理这个点击事件并显示预览图
②点击 button 删除按钮的时候也会触发一个点击事件,触发删除操作
③点击 cilck 就会先被 button 处理,进行删除操作,然后顺着 button 的父标签,传递给 div 标签,然后 div 再处理一次,触发预览图效果

九.项目拓展点

1.实现基于白名单方式的简单防盗链

通过 HTTP 请求中的 Refer 字段判定是否是指定网站请求服务器图片,未授权网站就无法访问图片内容。

新增属性:

修改 ImageShowServlet.doGet ()方法:


2.基于 MD5 实现相同内容的图片在磁盘上只能存一份

整体思路:

●修改 dao 层代码,在 dao 层实现一个selectByMd5()的方法,根据 Md5 值来查找数据库中的图片信息(是否存在多张相同图片).
●修改上传图片代码,在磁盘上存储文件时先判定,该 md5 对应的图片是否已经存在于磁盘上了,若存在就不必再往磁盘写了.
●修改删除图片代码,先删除数据库记录,删除完毕后,看数据库中是否还存在刚刚相同的 md5 的记录.如果不存在,就删除磁盘文件.

在这里计算 md5 值我们引入依赖:

<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.14</version>
</dependency>
 /***  按照 md5 的值来查找数据库中的 “图片”Image对象(图片的 md5 值是唯一的)* @param md5* @return*/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;try {// 3.执行 SQL 语句statement=connection.prepareStatement(sql);statement.setString(1,md5);    //md5 在数据库中是按字符串形式存储的,所以要调用 setString() 方法resultSet=statement.executeQuery();// 4.处理结果集if(resultSet.next()){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.关闭连接和statement对象DBUtil.close(connection,statement,resultSet);}return null;}



十.效果展示图


图片服务器项目编写思路相关推荐

  1. 图片服务器项目编写设计

    待到秋来九月八,我花开后百花杀 项目背景 项目所涉及知识点 服务器设计 数据库设计 服务器API(前后端交互接口)设计 涉及知识点 正式设计接口功能(约定HTTP协议格式) 编写源代码开发 数据库操作 ...

  2. 图片服务器项目测试用例

    单元测试 这里只对imageDAO层进行了单元测试 public class ImageDAOTest {@Testpublic void queryCountByMd5() {ImageDAO im ...

  3. 课程设计题目:图片服务器项目的全面测试

    源包地址:(32条消息) 软件测试课程设计或者大作业-图形服务器-软考文档类资源-CSDN文库 目录  注意:很多图都上传不上去,懒得上传了,自己打开源包里面啥都有 一.课程设计内容 1 二.课程设计 ...

  4. 图片服务器项目+测试总结

    图片服务器项目: 网盘百度链接:https://pan.baidu.com/s/1YIGnhy6QxoLBzf4CGjtM2A 提取码:1111 项目分析+代码 1.项目分析 过程分析:请求:浏览器- ...

  5. 图床/图片服务器项目详细设计

    项目目录 1.前言 1.1 项目背景 1.2 项目意义 1.3 涉及的知识点 2.总体设计 2.1 需求规定 2.1.1 软件功能的规定 2.1.1.1 系统功能 2.1.1.2 数据流图 2.1.1 ...

  6. Nginx 独立图片服务器的搭建

    为什么需要独立图片服务器? 如果你留心的话,可以发现,现在主流的网站都是有单独的图片服务器的,例如,人人网的为rrimg,淘宝的为taobaocdn,下面还有很多的二级域名. 独立的图片服务器有诸多好 ...

  7. Nginx独立图片服务器搭建教程

    Nginx独立图片服务器搭建教程 发布时间:2014-06-04编辑:脚本学堂 本文介绍了nginx独立图片服务器的搭建与配置教程,有需要的朋友参考下. 首先,为什么需要独立图片服务器? 现在主流的网 ...

  8. 零基础编写图片服务器(1)

    1)在我们真正进行WEB开发的时候,编写代码之前,我们都要做两件非常重要的设计:数据库设计+前后端交互API接口的设计+JDBC操作+WEB服务器开发能力,我们还要实现一个简单的页面来进行展示当前的图 ...

  9. javaweb项目创建图片服务器

    javaweb项目,创建tomcat图片服务器,用于保存用户上传的头像等图片信息.前期项目的新建和正常访问的调试不再说明,本文章仅讲解如何创建一个图片服务器. 1.WebContent下新建imgFi ...

最新文章

  1. Web容器启动中执行某个Java类
  2. 安装最新版git,git升级
  3. 获取电脑的唯一识别码_教你如何知道自己的电脑能够装黑苹果
  4. Java基础篇:封装、继承、多态三大特性
  5. hdfs mv命令_如何HDFS mv命令工作
  6. php在苹果手机上传不了图片大小,ThinkPHP后台上传图片无默认尺寸解决方法
  7. [EffectiveC++]item34:区分接口继承和实现继承
  8. vCenter 升级错误 VCSServiceManager 1603
  9. Vue 2.x 文件夹目录
  10. 项目方案-标书-文档等等编写规范
  11. java学习之流程控制
  12. 各种类型变量的定义以及赋值
  13. VC 定位窗体常用方法
  14. koa2 从入门到进阶之路 (五)
  15. split函数python_python有split函数吗
  16. oracle12c性能测试,Oracle12c IMO 测试
  17. 电脑W ndoWs未能启动怎么办,电脑出现windows未能启动怎么办
  18. 猪猪视频显示没有服务器,猪猪小视频软件
  19. 讲座 | lidar目标检测------图森未来CTO王乃岩
  20. 新生儿的一类(免费)疫苗(截止2019年)

热门文章

  1. 我们采访了这些游戏大神,这是他们给新人的建议
  2. 【金猿技术展】一种分布式 HTAP 数据库上基于索引的数据任意分布方法——为 HTAP 数据库实现 Collocation 优化...
  3. SQLite 的简单介绍
  4. SqlSugar-C#中数据库存储的框架
  5. U8g2菜单界面+按键操作在线模拟
  6. [4G5G专题-95]:MAC层- 调度 - 无线资源调度概述
  7. 设置font-family时需要注意的问题
  8. GOPS现场 | 大规模团队如何实现Jenkins的集中管理——对话龙智技术顾问
  9. centos8安装nexus
  10. mingw64安装和环境变量配置教程