项目----基于Java servlet构建的图片服务器(附代码)

目录

背景需求

整体架构

准备阶段

基础设施搭建:

数据库设计

表的设计

准备实体类

数据库操作工具类

创建DBUtil类

类的实现代码

json工具类

创建WebUtil类

功能实现 

实现图片上传接口

创建ImageServlet类

创建ImageDao类

根据md5值查重

 实现获取图片列表接口

 获取图片内容接口

创建ImageShow类

再在imageDao中完成selectOne 

删除图片接口 

在ImageServlet中重写doDelete

在ImageDao中实现delete方法 

项目展示 

背景需求

图床 , 解决 github / 博客中插入图片的问题。

整体架构

核心就是一个HTTP服务器,提供对图片的增删改查功能,同时搭配简单的页面辅助完成图片上传、展示的功能。

准备阶段

基础设施搭建:

maven项目

  • 前端技术:ajax,vue(前端js框架),jquery(只用到了这个框架提供的ajax函数来发请求)
  • 后端技术:Servlet,JDBC,junit(单元测试),Commons-fileupload(文件上传框架),Commons-codec(生成md5),jackson(json序列化),lombok

数据库设计

表的设计

drop database if exists image_system;
create database image_system character set utf8mb4;
use image_system;

create table image_table(
    image_id int primary key auto_increment comment '主键id,自增',
    image_name varchar(50) comment '图片名称',
    size bigint comment '图片大小',
    upload_time datetime comment '图片上传日期',
    md5 varchar(128) comment '用于校验图片唯一',
    content_type varchar(20) comment '数据类型',
    path varchar(1024) comment '图片所在路径:相对路径'
);

关于md5:

这是一种常见字符串 hash 算法,用于唯一性验证 , 具有三个特性:
  • 不管源字符串多长, 得到的最终 md5 值都是固定长度
  • 源字符串稍微变化一点点内容, md5 值会变化很大(降低冲突概率)
  •  通过原字符串很容易计算得到 md5 , 但是根据 md5 推导出原字符串很难(几乎不可能).

准备实体类

把数据库的表转为类,字段转为成员变量。

public class ImageTable {
    private Integer imageId;
    private String imageName;
    private Long size;
    private java.util.Date uploadTime;
    private String md5;
    private String contentType;
    private String path;
}

数据库操作工具类

创建DBUtil类

这个类主要包含三个方法

// 获取单例
public static DataSource getDataSource() { } 
// 获取链接
public static Connection getConnection() { } 
// 关闭链接
public static void close(Connection c, Statement s, 
 ResultSet re) {}

类的实现代码

public class DBUtil {
    //双重校验锁的线程安全的单例模式
    private static volatile DataSource DS;

    private static DataSource getDataSource(){
        if(DS==null){
            synchronized (DBUtil.class){  //synchronized要么修饰方法,要么传个对象,此处修饰方法显然不太合适
                if(DS==null){
                    MysqlDataSource dataSource=new MysqlDataSource();
                    dataSource.setURL("jdbc:mysql://localhost:3306/image_system");
                    dataSource.setUser("root");
                    dataSource.setPassword("123456");
                    dataSource.setUseSSL(false);
                    dataSource.setCharacterEncoding("utf8");
                    DS=dataSource;
                }
            }
        }
        return DS;
    }
    public static Connection getConnection() {
        try {
            return getDataSource().getConnection();
        } catch (SQLException e) {
            throw new RuntimeException("获取数据库连接失败",e);
        }
    }
    public static void close(Connection c, Statement s, ResultSet re){
    //释放的顺序是先re,s,c
        try {
            if(re!=null) re.close();
            if(s!=null) s.close();
            if(c!=null) c.close();
        } catch (SQLException e) {
            throw new RuntimeException("释放数据库资源失败",e);
        }
    }
}

json工具类

创建WebUtil类

public class WebUtil {

    private static final ObjectMapper M=new ObjectMapper();

    static {
//        设置序列化日期格式
        DateFormat df= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        M.setDateFormat(df);
    }
    //    json序列化
    public static void serialize(HttpServletResponse resp,Object o){
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        try {
            String json=M.writeValueAsString(o);
            resp.getWriter().write(json);
        } catch (IOException e) {
//            捕获到异常自行处理
            e.printStackTrace();
            resp.setStatus(500);
        }
    }
//    json反序列化
    public static <T> T deserialize(HttpServletRequest res,Class<T> clazz){
        try {
            return M.readValue(res.getInputStream(),clazz);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("反序列化失败",e);
        }
    }

}

关于json的序列化与反序列化:

  • 序列化:将响应的java对象转化为json字符串
  • 反序列化:将请求的json格式数据,转化为java对象

功能实现 

实现图片上传接口

假设服务端本地路径前缀为C:/XXX(完整的路径为,本地路径前缀+路径后缀),路径后缀可以是一个随机值也可以使用md5值。前端展示的路径为

<img v-bind:src="'imageShow?imageId=' + image.imageId" style="height:200px; width:200px">

那我们后端就需要提供imageShow的接口,解析imageId,找到文件在服务端本地的路径,然后把二进制数据设置到响应体。

如何解析imageId?

  • 可以通过图片id在数据库找到图片的path,设置path为路径后缀,加上路径前缀就可以找到了

前端代码

imageUpload(){
                if(!app.uploadImage) {
                    alert("选择图片后上传");
                    return;
                }
                let data = new FormData();
                data.append("uploadImage", app.uploadImage);
                $.ajax({
                    url: "image",
                    type: "post",
                    processData: false,
                    contentType: false,
                    data: data,
                    // context: this,
                    success: function(data, status) {
                        if(data.ok){
                            app.getImages();
                        }else{
                            alert(data.msg);
                        }
                        // alert("上传成功");
                    },

借助抓包工具可以看到: 

 后端代码为

创建ImageServlet类

将图片保存在本地硬盘和数据库中

@WebServlet("/image")   //抓包看路径
@MultipartConfig
public class ImageServlet extends HttpServlet {
    //服务端保存在本地硬盘的路径前缀
    public static final String LOCAL_PATH_PREFIX="D:/IMG";
    //图片上传接口
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        请求数据:uploadImage=图片数据(抓包查看)
        Part p=req.getPart("uploadImage");

//        1.保存在服务器本地硬盘:路径前缀+后缀(后缀设置为MD5值)
//        根据上传的图片生成MD5值
        String md5= DigestUtils.md5Hex(p.getInputStream());
        p.write(LOCAL_PATH_PREFIX+"/"+md5);
        
//        2.保存在数据库
//        保存要插入数据库的数据
        ImageTable image =new ImageTable();
//        设置图片名称
        image.setImageName(p.getSubmittedFileName());
//        设置图片大小
        image.setSize(p.getSize());
//        设置上传日期
        image.setUploadTime(new java.util.Date());
//        设置MD5
        image.setMd5(md5);
//        设置数据类型
        image.setContentType(p.getContentType());
//        设置图片路径
        image.setPath("/"+md5);


    }
}

  

点击上传后,我们可以看到本地的D:/IMG路径下出现

创建ImageDao类

插入数据库图片数据

public class ImageDao {

//    保存图片数据:插入    JDBC操作
    public static int insert(ImageTable image) {
        Connection c=null;
        PreparedStatement ps=null;
        try {
            c= DBUtil.getConnection();
            String sql="insert into image_table values(null,?,?,?,?,?,?)";
            ps=c.prepareStatement(sql);
//            替换占位符
            ps.setString(1,image.getImageName());
            ps.setLong(2,image.getSize());
            Long time=image.getUploadTime().getTime();
            ps.setTimestamp(3,new Timestamp(time));
            ps.setString(4,image.getMd5());
            ps.setString(5,image.getContentType());
            ps.setString(6,image.getPath());
//            执行插入操作并返回
            return ps.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("数据库保存图片出错",e);
        }finally {
            DBUtil.close(c,ps,null);
        }
    }
}

再在ImageServlet中添加以下代码

//        插入数据库图片数据
        int n= ImageDao.insert(image);
        Map<String,Object> map=new HashMap<>();
        map.put("ok",true);//不返回错误,出错了Tomcat会直接报500状态码
//        转为json字符串
        WebUtil.serialize(resp,map);

    }

在数据库中执行select * from image_table 可以看到

可以看到上传两次后,数据库里有两个一样的值,这时就需要进行判断是否重复。如果重复就不再上传。 

根据md5值查重

//        根据md5值查重
        ImageTable imageTable=ImageDao.selectByMd5(md5);
        if(imageTable!=null){  //图片已经存在
            Map<String,Object> data=new HashMap<>();
            data.put("ok",true);
            data.put("msg","图片重复");
            WebUtil.serialize(resp,data);
            return;
        }

在ImageDao中写selectByMd5 

public static ImageTable selectByMd5(String md5) {
        Connection c=null;
        PreparedStatement ps=null;
        ResultSet rs=null;
        try {
            c=DBUtil.getConnection();
            String sql="select * from image_table where md5=?";
            ps=c.prepareStatement(sql);
//            替换占位符
            ps.setString(1,md5);
//            查询返回的结果集
            rs=ps.executeQuery();
            while (rs.next()){
                ImageTable imageTable=new ImageTable();
                imageTable.setImageId(rs.getInt("image_id"));
                imageTable.setImageName(rs.getString("image_name"));
                imageTable.setPath(rs.getString("path"));
                return imageTable;
            }
//            没有查到数据返回null
            return null;

        } catch (SQLException e) {
            throw new RuntimeException("根据md5查询图片失败",e);
        }finally {
            DBUtil.close(c,ps,rs);
        }
    }

现在我们上传重复图片就会看到

  

 实现获取图片列表接口

前端代码

getImages() {
                $.ajax({
                    url: "image",
                    type: "get",
                    context: this,
                    success: function(data, status) {
                        this.images = data;
                        $("#app").resize();
                    }
                })
            },

在ImageServlet类中重写doGet方法

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        查询数据库所有图片并返回List<ImageTable>
        List<ImageTable> images=ImageDao.selectAll();
//        返回响应
        WebUtil.serialize(resp,images);
    }

在ImageDao类中添加selectAll

public static List<ImageTable> selectAll() {
        Connection c=null;
        PreparedStatement ps=null;
        ResultSet rs=null;
        try {
            c=DBUtil.getConnection();
            String sql="select * from image_table";
            ps=c.prepareStatement(sql);
//            查询返回的结果集
            rs=ps.executeQuery();
            List<ImageTable> images=new ArrayList<>();
            while (rs.next()){
                ImageTable imageTable=new ImageTable();
                imageTable.setImageId(rs.getInt("image_id"));
                imageTable.setImageName(rs.getString("image_name"));
                images.add(imageTable);
            }
            return images;

        } catch (SQLException e) {
            throw new RuntimeException("获取图片列表出错",e);
        }finally {
            DBUtil.close(c,ps,rs);
        }
    }

刷新可以看到出现了图片列表

 获取图片内容接口

​<img v-bind:src="'imageShow?imageId=' + image.imageId" style="height:200px; width:200px">

通过这个代码,能够确定,获取图片的请求为:GET/imageShow?imageId=1,响应体为图片的二进制数据。

创建ImageShow类

在这个类我们要完成以下几个内容

  1. 获取请求数据:获取图片id
  2. 根据图片id,在数据库查询图片数据
  3. 返回响应:读取本地图片文件,把二进制数据设置到响应体

代码:

@WebServlet("/imageShow")
public class ImageShowServlet extends HttpServlet {

//    获取图片内容接口
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        1.获取请求数据
        String imageId=req.getParameter("imageId");
//        2.根据图片id在数据库查询图片数据
        ImageTable imageTable = ImageDao.selectOne(Integer.parseInt(imageId));
//        3.返回响应,读取本地图片,把二进制数据设置到响应体
        String path=ImageServlet.LOCAL_PATH_PREFIX+imageTable.getPath();
//        读取这个路径的图片
        File pic=new File(path);
        byte[] data= Files.readAllBytes(pic.toPath());
//        把图片二进制数据写入响应正文
        resp.getOutputStream().write(data);
    }
}

再在imageDao中完成selectOne 

public static ImageTable selectOne(int id) {
        Connection c=null;
        PreparedStatement ps=null;
        ResultSet rs=null;
        try {
            c=DBUtil.getConnection();
            String sql="select * from image_table where image_id=?";
            ps=c.prepareStatement(sql);
//            替换占位符
            ps.setInt(1,id);
//            查询返回的结果集
            rs=ps.executeQuery();
            while (rs.next()){
                ImageTable imageTable=new ImageTable();
                imageTable.setImageId(rs.getInt("image_id"));
                imageTable.setImageName(rs.getString("image_name"));
                imageTable.setPath(rs.getString("path"));
                return imageTable;
            }
//            没有查到数据返回null
            return null;

        } catch (SQLException e) {
            throw new RuntimeException("获取图片内容失败",e);
        }finally {
            DBUtil.close(c,ps,rs);
        }
    }

删除图片接口 

前端代码

remove(imageId) {
                $.ajax({
                    url:"image?imageId=" + imageId,
                    type:"delete",
                    context: this,
                    success: function(data, status) {
                        app.getImages();
                        alert("删除成功");
                    }
                })
            }

根据前端代码可知:删除图片的请求为DELETE/image?imageId=1

在ImageServlet中重写doDelete

//删除图片接口
    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        1.获取请求数据
        String imageId=req.getParameter("imageId");
        Integer id=Integer.parseInt(imageId);
//        2.删除本地文件(根据id找到path,拼接路径)
        ImageTable imageTable=ImageDao.selectOne(id);
        String path=LOCAL_PATH_PREFIX+imageTable.getPath();
        File pic=new File(path);
        pic.delete();
//        3.删除数据库图片数据
        int n=ImageDao.delete(id);
//        4.返回响应
        Map<String,Object> data=new HashMap<>();
        data.put("ok",true);
        WebUtil.serialize(resp,data);

    }

在ImageDao中实现delete方法 

//根据图片id删除数据
    public static int delete(Integer id) {
        Connection c=null;
        PreparedStatement ps=null;
        try {
            c=DBUtil.getConnection();
            String sql="delete from image_table where image_id=?";
//            替换占位符
            ps=c.prepareStatement(sql);
            ps.setInt(1,id);
            return ps.executeUpdate();
        } catch (SQLException e) {
            throw  new RuntimeException("删除图片失败",e);
        }finally {
            DBUtil.close(c,ps,null);
        }
    }

项目展示 

上传

删除

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值