项目核心知识点
- 简单的Web服务器设计开发能力(Servlet)
- 使用数据库(MySQL)JDBC操作MySQL
- 数据库设计(根据实际场景设计数据库表结构)
- 前后端交互的API的设计(基于HTTP协议)
- 认识JSON数据格式,学习使用Java中的Gson这个库操作JSON数据
- 学习测试HTTP服务器,Postman
- 使用HTML CSS JavaScript技术构建一个简单的网页
项目功能:
实现一个服务器完成对图片的上传,查看(属性/内容),删除操作。(重点)
同时也要实现一个简单的页面来展示当前的图片(了解)
一、服务器设计
1.数据库设计
-
数据库中存储的图片的属性(元信息)
-
图片正文,以文件的形式直接存在磁盘上的
-
数据库中就记录一个path对应到磁盘上的文件
-
校验和: 通过一个更短的字符串,来验证整体数据是否正确,短的字符串是根据原串内容通过一定的规则来计算出来的
-
md5(字符串哈希算法):图片的md5校验和
create database java_image_server;
create table image_table(
imageId int not null primary key auto_increment,
imageName varchar(50),
size int,
uploadTime varchar(50),
contentType varchar(50),
path varchar(1024),
md5 varchar(1024));
2.服务器API设计(前后端交互接口设计)
a)JSON 一种数据组织的格式。格式键值对结构。
使用JSON完成数据的序列化,方便进行网络传输
Gson:google搞得一个开源的JSON的JSON解析库
b)文件上传操作在HTML中是如何完成的
文件上传在HTTP协议中是如何进行的
c)设计前后端交互API
- 新增图片
请求:
POST /image
Content-Type:multipart/form-data;
//正文内容:包含图片自身的一些信息
//图片正文的二进制内容
响应:
//上传成功
HTTP/1.1 200 OK
{
"ok":true
}
//上传失败
HTTP/1.1 200 OK
{
"ok":false,
"reason":"具体的失败原因"
}
- 查看所有图片属性
请求:
GET /image
响应:
HTTP/1.1 200 OK
[
{
imageId:1,
imageName:"1.png",
contentType:"image/png",
size:10000,
uploadTime:"20200404",
path:"./data/1.png"
md5:"11223344",
},
{
.......
}
]
- 查看指定图片属性
请求:
GET /image?imageId=[具体的数值]
响应:
HTTP/1.1 200 OK
{
imageId:1,
imageName:"1.png",
contentType:"image/png",
size:10000,
uploadTime:"20200404",
path:"./data/1.png"
md5:"11223344",
}
- 删除指定图片
请求:
DELETE /image?imageId=[具体的图片id]
服务器实现代码的时候就可以判定方法,如果是DLETE方法,就执行删除操作
删除也不一定非得用DELETE方法。例如:
GET /image?imageId=xxx&delete=1
- 查看指定图片内容
请求:
GET /imageShow?imageId=[具体的图片id]
二、源码开发
1.数据库操作
-
先创建DBUtil封装一下获取数据库连接的过程
-
dao数据访问层。这里面的类围绕着数据操作展开
-
Image对应到一个图片对象(包含图片的相关属性)
-
ImageDao Image对象的管理器。借助这个类完成Image对象的增删改查操作
ImageDao有个selectAll方法,查找出所有数据库中的数据。如果数据库中内容有个几千条,这样查ok;如果数据库有几亿条,这样查就会非常低效,很可能直接把数据库或者应用程序搞挂
更科学的做法可以指定一些其他筛选条件插入图片
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.getMad5());
//3.执行SQL语句
int ret=statement.executeUpdate();
if(ret!=1){
//程序出现问题,抛出一个异常
throw new JavaImageServerException("插入数据库出错!");
}
} catch (SQLException e) {
e.printStackTrace();
}catch (JavaImageServerException e) {
e.printStackTrace();
}finally {
//4.关闭连接和statement对象
DBUtil.close(connection,statement,null);
}
}
查看所有图片信息
public List<Image> selectAll(){
List<Image> images=new ArrayList<Image>();
Connection connection=DBUtil.getConnection();
String sql="select * from image_table";
PreparedStatement statement=null;
ResultSet resultSet=null;
try {
statement=connection.prepareStatement(sql);
resultSet=statement.executeQuery();
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.setPath(resultSet.getString("path"));
image.setMad5(resultSet.getString("md5"));
image.setContentType(resultSet.getString("contentType"));
images.add(image);
}
return images;
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(connection,statement,resultSet);
}
return null;
}
根据imageId查找指定的图片信息
public Image selectOne(int imageId){
Connection connection=DBUtil.getConnection();
String sql="select * from image_table where imageId=?";
PreparedStatement statement=null;
ResultSet resultSet=null;
try {
statement=connection.prepareStatement(sql);
statement.setInt(1,imageId);
resultSet=statement.executeQuery();
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.setPath(resultSet.getString("path"));
image.setMad5(resultSet.getString("md5"));
image.setContentType(resultSet.getString("contentType"));
return image;
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(connection,statement,resultSet);
}
return null;
}
public void delete(int imageId){
Connection connection=DBUtil.getConnection();
String sql="delete from image_table where imageId=?";
PreparedStatement statement=null;
try {
statement=connection.prepareStatement(sql);
statement.setInt(1,imageId);
int ret=statement.executeUpdate();
if(ret!=1){
throw new JavaImageServerException("删除数据库操作失败");
}
} catch (SQLException e) {
e.printStackTrace();
}catch (JavaImageServerException e) {
e.printStackTrace();
}finally {
//4.关闭连接和statement对象
DBUtil.close(connection,statement,null);
}
}
根据Md5查找图片
public Image selectMd5(String md5){
Connection connection=DBUtil.getConnection();
String sql="select * from image_table where md5=?";
PreparedStatement statement=null;
ResultSet resultSet=null;
try {
statement=connection.prepareStatement(sql);
statement.setString(1,md5);
resultSet=statement.executeQuery();
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.setPath(resultSet.getString("path"));
image.setMad5(resultSet.getString("md5"));
image.setContentType(resultSet.getString("contentType"));
return image;
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
DBUtil.close(connection,statement,resultSet);
}
return null;
}
}
受查异常和非受查异常
出现异常之后,处理的具体措施
1.当前接触过的大部分都是打印调用栈
2.让程序直接终止
3.监控报警通知程序员
- 基于Servlet来搭建服务器
a)安装Servlet(maven)
b)创建一个类,继承HttpServlet父类并且重写这个父类中的一些重要方法
按照现有设定,图片存储的文件路径./image/文件名
如果有两个图片,内容不同,但是名字相同,此时就可能出现上传失败的情况(在路径中加上时间戳)
时间戳:以1970年1月1日0时0分0秒作为基准值计算当时时刻和基准时刻的描述/毫秒数之差
3.上传图片
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取图片的属性信息,并且存入数据库
// a)需要创建一个factory对象和upload对象
FileItemFactory factory=new DiskFileItemFactory();
ServletFileUpload upload=new ServletFileUpload(factory);
//b)通过upload对象进一步解析
//FileItem代表一个上传的文件对象
List<FileItem> items;
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());
SimpleDateFormat format=new SimpleDateFormat("yyyyMMdd");
image.setUploadTime(format.format(new Date()));
image.setContentType(fileItem.getContentType());
image.setMad5(DigestUtils.md5Hex(fileItem.get()));
image.setPath("./image/"+image.getMad5());
//存到数据库中
ImageDao imageDao=new ImageDao();
Image existImage=imageDao.selectMd5(image.getMad5());
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("lw-img.html");
}
4.删除图片:
1).先获取到请求中的imageId
2).创建ImageDao对象,查看到该图片对象对应的相关属性
3).删除数据库中的记录
4).删除本地磁盘文件
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
String imageId=req.getParameter("imageId");
if(imageId==null||imageId.equals("")){
resp.setStatus(200);
resp.getWriter().write("{\"ok\":false,\"reason\":\"请求解析失败\"}");
return;
}
ImageDao imageDao=new ImageDao();
Image image=imageDao.selectOne(Integer.parseInt(imageId));
if(image==null){
resp.setStatus(200);
resp.getWriter().write("{\"ok\":false,\"reason\":\"在数据库中不存在\"}");
return;
}
imageDao.delete(Integer.parseInt(imageId));
Image existImage=imageDao.selectMd5(image.getMad5());
if(existImage==null){
File file=new File(image.getPath());
file.delete();
resp.setStatus(200);
resp.getWriter().write("{\"ok\":true}");
}
}
5.页面主题展示图片预览
<form id="upload-form" action="image" method="post" enctype="multipart/form-data" class="am-topbar-form am-topbar-right am-form-inline">
<div class="am-form-group">
<input type="file" id="upload" name="upload" class="am-form-field am-input-sm" >
<input type="submit" value="Upload" style="height: 41px"/>
</div>
</form>
三、项目优化
1.简单的防盗链机制
可以判定当前请求的referer字段(HTTP请求协议中的header的部分),是不是在我们代码中指定的白名单中,如果是,才允许访问
referer表示当前请求的上个页面的地址
在代码中用一个hashSet存一下允许的Referer就可以
防盗链其他方式:https://www.cnblogs.com/wangyongsong/p/8204698.html
2.优化磁盘存储空间
应用到MD5
如果两个图片内容完全一样,就在磁盘上只存一份文件就可以了
通过MD5就能判定两个图片内容是否一样的
图片文件虽然是二进制数据,但本质上也是字符串,针对图片内容计算MD5
如果两个图片内容相同,得到的MD5一定是相同的
反之,近似的认为MD5相同,原图片内容一定相同
MD5的特点:
1.不管原串多长,得到的MD5值是固定长度
2.原串哪怕变动一个字节,MD5值就会变动很大
3.计算MD5值得过程很简单,但是通过MD5值无法推测出原字符串的(应用在密码学)
实现思路:上传图片的时候,先判定下新图片的MD5在数据库中是否存在,如果存在,就不把图片内容写到磁盘上,如果不存在,才存磁盘
1)改上传代码逻辑,磁盘文件名用md5值来表示
2)修改ImageDao,新增一个接口,能够按照MD5查找数据库内容
3)再次修改上传图片逻辑,根据MD5判断,当前图片是否要写磁盘
3. 支持图片处理功能(例如返回指定大小的图片)