存储图片的服务器
一、项目背景
1. 项目意图
有没有很熟悉的感觉嘞?没错,就是写博客时候插入图片的界面啦!
接下来就让我们模拟一下这个功能的实现过程吧!
2. 项目描述
基于Servlet+Tomcat实现一个HTTP服务器,然后用这个服务器来存储图片,每个图片提供一个唯一的url,根据该url把图片展示到网页上。同时将图片属性存在数据库中,图片属性中的路径对应磁盘上存的图片内容。
2.1 核心功能
- 上传一个图片(数据库中存储该图片属性,硬盘上存储具体图片)
- 查看图片内容和属性
- 删除图片
- 通过网页展示图片
2.2 核心技术
- 简单的Web服务器设计开发能力(Servlet)
- 使用数据库(MySQL),通过JDBC操作MySQL
- 数据库设计(根据实际场景设计数据库表结构)
- 前后端交互的API的设计(基于HTTP协议)
- 使用Java中的Gson这个库操作JSON数据
- 测试HTTP服务器,通过Postman工具进行测试
- Servlet的使用
- 基于md5进行校验
- 使用HTML CSS JavaScript技术构建一个简单的网页
- 软件测试的基本思想和方法
二、服务器设计
1. 数据库设计
- 数据库中存储的 图片 的属性(元信息),
- 图片正文,以文件的形式直接存在磁盘上的,
- 数据库中就记录一个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:指数据访问层,这里面的代码围绕数据展开。
- 先创建 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();
}
}
}
- 创建实体类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()
}
- 创建实体类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无法推测出原字符串。