Bootstrap

我的图库(图床)

一、项目背景

1. 项目意图

在这里插入图片描述
有没有很熟悉的感觉嘞?没错,就是写博客时候插入图片的界面啦!
接下来就让我们模拟一下这个功能的实现过程吧!

2. 项目描述

基于Servlet+Tomcat实现一个HTTP服务器,然后用这个服务器来存储图片,每个图片提供一个唯一的url,根据该url把图片展示到网页上。同时将图片属性存在数据库中,图片属性中的路径对应磁盘上存的图片内容。

2.1 核心功能

  1. 上传一个图片(数据库中存储该图片属性,硬盘上存储具体图片)
  2. 查看图片内容和属性
  3. 删除图片
  4. 通过网页展示图片

2.2 核心技术

  1. 简单的Web服务器设计开发能力(Servlet)
  2. 使用数据库(MySQL),通过JDBC操作MySQL
  3. 数据库设计(根据实际场景设计数据库表结构)
  4. 前后端交互的API的设计(基于HTTP协议)
  5. 使用Java中的Gson这个库操作JSON数据
  6. 测试HTTP服务器,通过Postman工具进行测试
  7. Servlet的使用
  8. 基于md5进行校验
  9. 使用HTML CSS JavaScript技术构建一个简单的网页
  10. 软件测试的基本思想和方法

二、服务器设计

1. 数据库设计

  1. 数据库中存储的 图片 的属性(元信息),
  2. 图片正文,以文件的形式直接存在磁盘上的,
  3. 数据库中就记录一个path就对应到磁盘上的文件
    数据库中表的具体设计如下:
create table image_table
(
  imageId     int auto_increment primary key,
  imageName   varchar(50)   null,
  size        int           null,
  uploadTime  varchar(50)   null,
  contentType varchar(50)   null,
  path        varchar(1024) null,
  md5         varchar(1024) null
);

2. 服务器API设计(前后端交互接口设计)

2.1 JSON字符串

  • JSON:一种数据组织的格式。格式键值对的结构。
  • JSON只是一个数据格式,和编程语言无关(出身与JavaScript)
  • 此处使用JSON完成数据序列化,方便进行网络传输
  • Gson:Google搞的一个开源的JSON解析库 (快)

2.2 HTML上传文件

  • 文件路径:webapp/WEB-INF/upload.html
  • 文件内容:
<form id="upload-form" action="upload.php" method="post" enctype="multipart/form-data" >
   <input type="file" id="upload" name="upload" /> <br />
   <input type="submit" value="Upload" />
</form>

2.3 设计前后端交互API(HTTP协议具体要构造成的格式)

  • 接口一:新增图片
    客户端上传图片构造一个如下的请求,
    服务器端也要按照这个格式来解析。
请求:
POST/image
Content-Type:multipart/form-data;
[正文内容 包含图片自身的一些信息]
-------

[图片正文的二进制内容]
响应: 上传成功                上传失败
HTTP/1.1 200 OK               HTTP/1.1 200 OK
{                             {
    "ok":true                      "ok":false,
}                                   "reason":"具体的失败原因"
                              }
  • 接口二:查看所有图片信息(数据库中属性信息)
请求:
GET/image
响应:获取成功返回Json数组                   获取失败返回空数组               
HTTP/1.1 200 OK                            HTTP/1.1 200 OK               
[                                          []                    
    {                             
        imaged:1,
        imageName: "1.png",
        contentType: "image/png",
        size:1000,
        uploadTime:"20200222",
        path:"./data/1.png",
        md5: "[md5值]"                   
    },
    {
        ...
    }                                   
]                              
  • 接口三:查看指定图片信息(数据库中属性信息)
请求:
GET/image?imageId=[具体的图片id]
响应:获取成功返回Json字符串                  获取失败返回空数组               
HTTP/1.1 200 OK                             HTTP/1.1 200 OK               
{                                           {
    imaged:1,                                    "ok":false,
    imageName: "1.png",                          "reason":"具体的失败原因"
    contentType: "image/png",                }           
    size:1000,
    uploadTime:"20200222",
    path:"./data/1.png",
    md5: "[md5值]"                    
}                              
  • 接口四:删除指定图片
请求:
DELETE/image?imageId=[具体的图片id]
响应: 删除成功                删除失败
HTTP/1.1 200 OK               HTTP/1.1 200 OK
{                             {
    "ok":true                      "ok":false,
}                                   "reason":"具体的失败原因"
  • 接口五:查看指定图片内容
请求: 
GET /imageShow?imageId=[具体的图片id]
响应: 查看成功                                     查看失败
HTTP/1.1 200 OK                                   HTTP/1.1 200 OK
content-type: image/png                           {
[图片的二进制内容]//[响应 body 中为 图片内容 数据]        "ok":false,
                                                       "reason":"具体的失败原因"
                                                   }

三、源码开发

1.封装数据库操作(DAO层)

dao:指数据访问层,这里面的代码围绕数据展开。

  1. 先创建 DBUtil类封装一下获取数据库连接的过程
private static final String URL = "jdbc:mysql://127.0.0.1:3306/数据库名?characterEncoding=utf8&useSSL=true" 
private static final String USERNAME = "root"; 
private static final String PASSWORD = "";

private static volatile DataSource dataSource = null; //Java内置的类,通过此类可以操作数据库
// 通过这个方法来创建DataSource的实例
public static DataSource getDataSource(){  // 单例模式,DataSource这个对象只需要一个(注意线程安全问题)
    if(dataSource == null){
        synchronized(DBUtil.class){
            if(dataSource == null){
                dataSource = new MysqlDataSource();
                MysqlDataSource tmpDataSource = (MysqlDataSource)dataSorce;
                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();
        }
    }
} 
  1. 创建实体类Image,对应到一个图片对象(包含图片的相关属性)
public class Image{
    private int imageId;
    private String imageName;
    private int size;
    private String uploadTime;
    private String contentType;
    private String path;
    private String md5;
    //实现get和set方法
    //为调试方便加打印方法toString()
}
  1. 创建实体类ImageDao,是Image对象的管理器,借助这个类完成Image对象的增删改查操作。
    这里就用到了JDBC操作。
  • 插入图片
  • 查数据库中所有图片的信息
  • 根据imageId查找指定的图片信息
  • 根据imageId删除指定的图片
insert操作:
1.获取数据库连接
Connection connection = DBUtil.getConnection();
2.创建SQL语句
String sql = "SQL语句(?是占位符)";
PrepareStatement satement = connecton.prepareStatement(sql);
statement.setString(1,参数1);
statement.setInt(2,参数2);
...
3.执行SQL语句
int ret = statement.executeUpdate();// 返回受影响行数
根据受影响行数判断插入是否成功。
// common包放自定制异常类JavaImageServerException
4.关闭连接和statement对象
关闭应该放在finally{}代码块里!!!
DBUtil.close(connection,statement,null);

select操作:
比insert操作多一步处理结果集
方法是:ResultSet resultSet = statemen.executeQuery();// 返回的是结果集
while(resultSet.next()){
    Image image = new Image();
    image.setImageId(resultSet.getInt("imageId"));
    ...
}
return images;

2. api包中处理请求生成响应

基于Servlet+Tomcat搭建一个HTTP服务器:Servlt处理请求生成响应

  • ImageServlet类继承HTTPServlet:重写方法
1.上传图片         POST    测试用浏览器
2.查看所有图片信息  GET     测试用浏览器
3.查看指定图片信息  GET
4.删除指定图片      DELETE  测试用postman--
实现上传图片(文件),用到一个第三方库 fileupload,在maven库中搜索
对应JSON格式的响应,需要吧content-type设定成指定格式resp.setContentType("application/json; charset=utf-8");
  • ImageShowServlet类:查看图片内容

四、项目测试

1. 单元测试

在test目录下新建ImageDAoTest对数据库的操作进行测试。

public class ImageDaoTest {
    private ImageDao imageDao = new ImageDao();

    @Test
    public void insert() {
        Image image = new Image();
        image.setImageName("1.png");
        image.setSize(100);
        image.setUploadTime("20200227");
        image.setContentType("image/png");
        image.setPath("./data/1.png");
        image.setMd5("112233");
        imageDao.insert(image);
    }

    @Test
    public void selectAll() {
        List<Image> images = imageDao.selectAll();
        System.out.println(images);
    }

    @Test
    public void selectOne() {
        Image image = imageDao.selectOne(1);
        System.out.println(image);
    }

    @Test
    public void delete() {
        imageDao.delete(6);
    }
}

2. api接口测试

接口测试使用postman进行测试。
用postman对HTTP协议的GET/POST/DELETE方法进行测试。

3. 手工测试

手工测试测试点如下。
在这里插入图片描述

五、项目扩展

1. 优化磁盘存储空间

将两张同样内容的照片在磁盘上只做一份存储。通过MD5实现。

  • 上传图片的时候,先判定下该图片MD5在数据库中是否存在;
  • 如果已经存在就不把图片内容写到磁盘上,如果不存在则写入磁盘。
    获取MD5:
image.setMd5(DigestUtils.md5Hex(fileItem.get()));

带来新问题——删除时需要判断数据库中是否还存在该MD5的数据,存在就不删除磁盘数据,不存在即可删除。

2. 防盗链机制

防止图片链接被其他人使用。
通过一定的机制限制其他人使用图片,否则图片使用的人太多服务器会挂掉。
实现方式:白名单方式。

  • 可以判定当前请求referer字段(HTTP请求协议中的header的部分),是不是在我们代码中指定的白名单中,如果是才允许访问。(referer表示当前请求的上个页面地址。)
  • 在代码中用一个hashSet存一下允许的referer,展示图片的时候判断一下是否在hashSet中存在即可。
//判断是否在白名单之中
        String referer = req.getHeader("Referer");
        if (!whiteList.contains(referer)) {
            resp.setContentType("application/json; charset=utf-8");
            resp.getWriter().write("{ \"ok\": false, \"reason\": \"未授权的访问\" }");
            return;
        } 

MD5的特点:
1.不管原串多长,得到的MD5值是固定长度
2.原串哪怕变动一点点,MD5值变动很大
3.计算MD5值的过程很简单,但是通过MD5无法推测出原字符串。

;