欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

图片服务器设计文档

程序员文章站 2024-03-21 20:38:10
...
           **基于Java Servlet 构建的图片服务器**

一.项目概述

1.1项目背景

用过的很多app中,每次去更换头像都会让你从本地相册中选择图片进行更换,还有前几天在学习通上传实验截图,也是点击后选择本地图片进行上传,还有博客的插入图片功能,还有等等等很多地方都用到了此功能,所以这个图片上传功能应用好广啊,俗话说,学以致用,我也想自己实现一个图片服务器,来让大家分享自己的各种有趣的,好玩的照片,美食,美景,美人都来进到我的服务器中吧。
如果感兴趣,那让我们往下接着看。
图片服务器设计文档

1.2项目介绍

1.实现一个web程序,通过url进入到图片服务器首页,直接展示所有人上传过的图片。
2.选择上传就可以从本地文件或者相册中去选择图片,点击提交后,页面会展现出你提交的图片。
3.对单张图片进行删除,会提示用户删除成功。
4.每张图片可以单击放大,左右滑动来浏览。

这就是这个简陋的图片服务器的一个介绍。

二.项目设计

2.1项目整体架构

核心就是一个 HTTP 服务器, 提供对图片的增删改查能力,同时搭配简单的页面辅助完成图片上传/展示功能。需要用到tomcat,IDEA,MySQL。
后面还会通过打war包的形式将项目部署在linux上,更好的展现项目功能。

2.2项目涉及知识点

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

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较为简单。

  1. 获取到图片相关的元信息(Image对象), 并写入数据库
    a) 创建 factory 对象和 upload 对象
    b) 使用 upload 对象解析请求
    c) 对请求信息进行解析, 转换成 Image 对象
    d) 将 Image 对象写入数据库中
  2. 获取到图片内容, 写入到磁盘中
  3. 设置返回的响应结果
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

相关标签: 项目