图片服务器设计文档
**基于Java Servlet 构建的图片服务器**
文章目录
一.项目概述
1.1项目背景
用过的很多app中,每次去更换头像都会让你从本地相册中选择图片进行更换,还有前几天在学习通上传实验截图,也是点击后选择本地图片进行上传,还有博客的插入图片功能,还有等等等很多地方都用到了此功能,所以这个图片上传功能应用好广啊,俗话说,学以致用,我也想自己实现一个图片服务器,来让大家分享自己的各种有趣的,好玩的照片,美食,美景,美人都来进到我的服务器中吧。
如果感兴趣,那让我们往下接着看。
1.2项目介绍
1.实现一个web程序,通过url进入到图片服务器首页,直接展示所有人上传过的图片。
2.选择上传就可以从本地文件或者相册中去选择图片,点击提交后,页面会展现出你提交的图片。
3.对单张图片进行删除,会提示用户删除成功。
4.每张图片可以单击放大,左右滑动来浏览。
这就是这个简陋的图片服务器的一个介绍。
二.项目设计
2.1项目整体架构
核心就是一个 HTTP 服务器, 提供对图片的增删改查能力,同时搭配简单的页面辅助完成图片上传/展示功能。需要用到tomcat,IDEA,MySQL。
后面还会通过打war包的形式将项目部署在linux上,更好的展现项目功能。
2.2项目涉及知识点
- 简单的Web服务器设计能力
- Java 操作 MySQL 数据库
- 数据库设计
- 响应风格 API
- gson 的使用
- 加深对HTTP 协议的理解
- Servlet 的使用
- 基于 md5 进行校验
- 软件测试的基本思想和方法
2.3 项目具体实现
2.3.1 数据库的设计
使用mysql,创建一个表来存放图片的属性信息,包括:id,图片名,图片大小,上传时间,图片类型,它的路径,md5(后面优化会用到)。
CREATE TABLE image_table (
imageId int(11) NOT NULL 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,
PRIMARY KEY (imageId)
)
2.3.2 JDBC建立数据库连接
选择用单例模式的饿汉,将JDBC连接专门封装到一个类里面,方便后面调用。选择单例模式是因为这个类只能创建一个实例,节省资源。
千万不能忘了close方法,里面的关闭的顺序不能有错,遵循先打开的后关闭。
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/image_picture?characterEncoding=utf8&&useSSL=true";
private static final String USERNAME="root";
private static final String PASSWORD="mysql";
//private static final String PASSWORD="LMJian521";
//private static final String PASSWORD="";
/**
* DataSource创建的connection既有基本实现,也有连接池实现(可以复用,DataSource帮我们实现了复用机制
*/
private static DataSource dataSource=null;
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();
}
}
}
2.3.3 封装一个类对象表示图片
面向对象编程,首先就是如何抽象为对象,一张图片代表一个对象,所以用一个类来描述它的属性,也就是数据库表里面的属性,顺序必须一一对应。
这里属性都是private,封装起到了保护作用,外部只能通过调用get,set方法来进行操作。
package dao;
public class Image {
/**
* image保存图片的属性
*/
private int imageId;
private String imageName;
private int size;
private String uploadTime;
private String contentType;
private String path;
private String md5;
public int getImageId() {
return imageId;
}
public String getImageName() {
return imageName;
}
public int getSize() {
return size;
}
public String getUploadTime() {
return uploadTime;
}
public void setImageId(int imageId) {
this.imageId = imageId;
}
public void setImageName(String imageName) {
this.imageName = imageName;
}
public void setSize(int size) {
this.size = size;
}
public void setUploadTime(String uploadTime) {
this.uploadTime = uploadTime;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public void setPath(String path) {
this.path = path;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getContentType() {
return contentType;
}
public String getPath() {
return path;
}
public String getMd5() {
return md5;
}
@Override
public String toString() {
return "Image{" +
"imageId=" + imageId +
", imageName='" + imageName + '\'' +
", size=" + size +
", uploadTime='" + uploadTime + '\'' +
", contentType='" + contentType + '\'' +
", path='" + path + '\'' +
", md5='" + md5 + '\'' +
'}';
}
}
2.3.4 实现对数据库的增删查改
基本思路:
1.获取数据库连接:调用封装好的connection类
2.拼装sql语句
3.执行sql语句
4.处理结果集
5.关闭连接
public class ImageDao {
/**
* 把image对象插入到数据库中
* @param image
*/
public void insert(Image image) {
//1.获取数据库连接
Connection connection=DBUtil.getConnection();
//2.创建并拼装SQL语句
String sql="insert into image_table values(null,?,?,?,?,?,?)";
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();
if(ret!=1){
//程序出现问题需要抛出异常
throw new JavaImageServerException("插入数据库出错");
}
} catch (SQLException e) {
e.printStackTrace();
} catch (JavaImageServerException e) {
e.printStackTrace();
}finally {
DBUtil .close(connection,statement,null);
}
//4.关闭连接和statement对象
}
/**
* 查找数据库中所有图片的信息
* @return
*/
public List<Image> selectAll() {
List<Image>images=new ArrayList<Image>();
//1.获取数据库连接
Connection connection=DBUtil.getConnection();
//2.构造sql语句
String sql="select *from image_table";
PreparedStatement statement=null;
ResultSet resultSet=null;
//3.执行sql语句
try {
statement=connection.prepareStatement(sql);
resultSet=statement.executeQuery();
//4.处理结果集
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.关闭连接
DBUtil.close(connection,statement,resultSet);
}
return null;
}
/**
* 根据imageId查找指定的图片
* @param imageId
* @return
*/
/**
* 根据imageId删除指定图片
* @param imageId
*/
}
这里因为篇幅原因就不全部上传代码了,需要的下面有Git链接。
2.3.5 实现servlet服务
通过HTTP的req,resp来实现通信,需要继承HttpServlet覆写里面的doget,dopost,dodelete方法来实现。
相信大家对HTTP都不陌生,get方法是用来获取信息的,post是用来提交数据的,delete是用来删除指定图片的。
这里详细说下post方法,因为get较为简单。
- 获取到图片相关的元信息(Image对象), 并写入数据库
a) 创建 factory 对象和 upload 对象
b) 使用 upload 对象解析请求
c) 对请求信息进行解析, 转换成 Image 对象
d) 将 Image 对象写入数据库中 - 获取到图片内容, 写入到磁盘中
- 设置返回的响应结果
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取图片的属性信息, 并且存入数据库
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
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;
}
// c) 把 FileItem 中的属性提取出来, 转换成 Image 对象, 才能存到数据库中
// 当前只考虑一张图片的情况
FileItem fileItem = items.get(0);
Image image = new Image();
image.setImageName(fileItem.getName());
image.setSize((int)fileItem.getSize());
// 手动获取一下当前日期, 并转成格式化日期, yyyyMMdd => 20200218
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/" + 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对象中的内容保存到某个指定的文件中
//最主要的用途是把上传的文件内容保存在本地文件系统中。
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");
}
delete方法步骤:
1. 获取请求中的 imageId
// 2. 创建 ImageDao 对象, 查找对应的 Image 对象
// 3. 删除数据库中的数据
// 4. 删除磁盘上的文件
// 5. 写回响应
web.xml就是告诉服务器tomcat你的项目有哪些服务信息
- 从Servlet的第一次请求开始(因为这时Servlet对象还没有创建),先执行new的操作(构造方法),再调用init()进行初始化。接着等待请求的到来,一旦有请求到来,service()方法被调用,根据请求类型决定调用doGet()/doPost()。只要有请求到来就会重复service()–>doGet()/doPost()/doDelete()这一过程。
- 当服务器重启或者关闭时,destroy()方法被调用,进行销毁Servlet对象工作
2.3.6 实现前端页面渲染
首先声明下自己前端并没有怎么接触,完全就是照猫画虎做出来的。
1.从百度下载HTML模板,进行修改。
修改过程:
- 文件上传和提交按钮: 把 input 标签及其父 div 拷贝一份. 原来的 input 标签 type 改成 file, 增加name=“filename”. 新的 input 标签 type=“submit”
- 修改 form 标签属性, 新增 method=“POST” enctype=“multipart/form-data"action=”/java_image_server/image".
2.通过修改js代码,发起ajax请求来渲染页面。
methods: {
// GET /image
getImages() {
$.ajax({
url: "image",
type: "get",
context: this,
success: function(data, status) {
// 此处的代码在浏览器收到响应之后, 才会执行到
// 参数中的 data 这就相当于收到的 HTTP 响应中的 body 部分
this.images = data;
$('#app').resize();
}
})
},
三.项目优化
3.1基于白名单的防盗链机制
通过 HTTP 中的 refer 字段判定是否是指定网站请求图片,
3.2基于 MD5 实现相同内容图片只存一份
整体思路
1.修改上传图片代码, 使用 md5 作为文件名.
2.修改 DAO 层代码, 在 DAO 层实现一个 selectByMD5 方法, 根据 MD5 来查找数据库中的图片信息.
3.修改上传图片代码, 存储文件时先判定, 该 md5 对应的文件是否存在, 存在就不必写磁盘了.
4.修改删除图片代码, 先删除数据库记录, 删除完毕后, 看数据库中是否存在相同 md5 的记录. 如果不存在, 就删除磁盘文件.
引入依赖包:
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
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);
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. 关闭链接
DBUtil.close(connection, statement, resultSet);
}
return null;
}
// 3. 删除数据库中的记录
imageDao.delete(Integer.parseInt(imageId));
// 4. 删除本地磁盘文件
File file = new File(image.getPath());
Image existImage = imageDao.selectByMd5(image.getMd5());
if (existImage == null) {
file.delete();
resp.setStatus(200);
resp.getWriter().write("{ \"ok\": true }");
}
修改doPost方法,如果该 MD5 值的文件不存在, 才真的写入磁盘.
如果该 MD5 值的文件存在, 则直接使用原来的文件, 不必再写一次磁盘
image.setMd5(DigestUtils.md5Hex(fileItem.get()));
// 自己构造一个路径来保存, 引入时间戳是为了让文件路径能够唯一
image.setPath("./image/" + image.getMd5());
片的内容信息, 并且写入磁盘文件
if (existImage == null) {
File file = new File(image.getPath());//创建指定路径的文件
try {
//将FileItem对象中的内容保存到某个指定的文件中
//最主要的用途是把上传的文件内容保存在本地文件系统中。
fileItem.write(file);
} catch (Exception e) {
e.printStackTrace();
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": false, \"reason\": \"写磁盘失败\" }");
return;
}
四.总结评估
1.细节决定成败,逻辑走天下。设计思路很重要
2.编码过程中需要不断的测试,写一点测一点,这样方便后期问题排查
3.项目部署问题:项目部署及问题排查
4.测试文档:图片服务器测试文档
五.效果展示
Git源码
https://github.com/ZYuY/Mystudy/tree/master/java_image_serve
上一篇: Hello,World
推荐阅读
-
图片服务器设计文档
-
生成pdf文档 插入签名图片
-
创建一个简单的图片服务器
-
【响应式Web设计】读书笔记 - 弹性布局与响应式图片(五) - 7
-
浅谈企业应用架构(二) 博客分类: 架构乱弹 企业应用网络应用应用服务器设计模式框架
-
《互联网程序设计(Java)》——课程笔记6:多用户服务器程序设计
-
Lucene-2.0学习文档(1) 博客分类: 全文检索 lucene搜索引擎全文检索多线程应用服务器
-
设计文档CheckList 博客分类: java; 企业应用
-
设计文档CheckList 博客分类: java; 企业应用
-
JavaMail核心类定义和理解及代码(二) 博客分类: 功能模块实例 应用服务器设计模式SUN工作Blog