【项目】——图片服务器

背景需求

  • 解决GitHub或者是博客里面插入图片的问题。

项目技术点

  1. Java操作MySql数据库
  2. 数据库的设计
  3. Restful风格API(https://baike.baidu.com/item/RESTful/4406165?fr=aladdin)
  4. Http协议
  5. Servlet的使用
  6. 基于md5进行校验
  7. json的使用

项目框架

在这里插入图片描述

项目简介


数据库设计

创建文件init.sql

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

drop table if exists `image_table`;
create table `image_table`(
                          image_id int not null primary key auto_increment,
                          image_name varchar(50),
                          size bigint,
                          upload_time varchar(50),
                          md5 varchar(128),
                          content_type varchar(50) comment '图片类型',
                          path varchar(1024) comment '图片所在路径');

封装数据库操作

创建包结构

org.example.dao

创建Util类

这个类下面主要放一些公用的方法,其中数据库连接,用户名,密码等都是固定的,但是注意密码要写自己的密码,不要写错了

private static final ObjectMapper M = new ObjectMapper();   //创建比较耗时,所以弄成全局唯一的

    //数据库连接池
    private static final MysqlDataSource DS = new MysqlDataSource();
    static {
        DS.setURL("jdbc:mysql://localhost:3306/image_system");
        DS.setUser("root");
        DS.setPassword("111111");  //设置成自己的额数据库的密码
        DS.setUseSSL(false);
        DS.setCharacterEncoding("utf-8");
    }

这个类主要包含四个方法

    //序列化json字符串
    public static String serialize(Object o) {}

    //获取数据库连接
    public static Connection getConnection () {}
 
    //关闭数据库连接
    public static void close(Connection c, Statement s, ResultSet rs) {}

    //关闭数据库连接的重载方法
    public static void close(Connection c, Statement s) {}

类的实现代码

public class Util {

    private static final ObjectMapper M = new ObjectMapper();   //创建和销毁比较耗时,所以弄成全局唯一的

    //数据库连接池
    private static final MysqlDataSource DS = new MysqlDataSource();
    static {
        DS.setURL("jdbc:mysql://localhost:3306/image_system");
        DS.setUser("root");
        DS.setPassword("111111");
        DS.setUseSSL(false);
        DS.setCharacterEncoding("utf-8");
    }

    /**
     * 把java对象序列化为json字符串      Servlet响应输出的body需要json字符串
     */
    public static String serialize(Object o) {
        try {
            return M.writeValueAsString(o);   
        } catch (JsonProcessingException e) {
            throw new RuntimeException("系列化java对象失败"+o,e);   
        }
    }

    /**
     * 注意使用java.sql包下面的Connection
     * 数据库连接
     */
    public static Connection getConnection () {
        try {
            return DS.getConnection();
        } catch (SQLException throwables) {
            throw new RuntimeException("连接数据库失败!"+throwables);
        }
    }

    /**
     * 数据库关闭
     * @param c
     * @param s
     * @param rs
     */
    public static void close(Connection c, Statement s, ResultSet rs) {
        try {
            if (rs != null)  rs.close();
            if (s != null) s.close();
            if (c != null) c.close();
        } catch (SQLException throwables) {
            throw new RuntimeException("释放数据库资源失败!"+ throwables);
        }
    }

    /**
    * 重载方式
    * 有时候去操作数据库的时候用不到 ResultSet ,不需要进行关闭
    */
    public static void close(Connection c, Statement s) {
        close(c,s,null);
    }

}

创建Image类

注意Image类下面的字段的名称一定不要和数据库里面的名称设置的相同,这样会导致前端解析响应的时候不知道解析哪个,会将代码写死!!!!(我在这里卡住了好久,一直都没有想出来问题所在的原因)

public class Image {
    private Integer imageId;   //图片id
    private String imageName;    //图片名字
    private Long size;    //图片大小
    private String uploadTime;    //图片的上传时间
    private String md5;     //文件的唯一校验
    private String contentType;    //文件的类型
    private String path;     //文件的路径
}

在文章的最后会说明md5的作用以及md5的获取方式

创建ImageDao类

public class ImageDAO {

    //先查找数据库有没有包含这个md5的图片
    public static int queryCount(String md5) {
        return 1;
    }


    //插入图片
    public static int insert(Image image){
        return 1;
    }

     //查询所有的图片
    public static List<Image> queryAll() {
            List<Image> list = new ArrayList<>();
            return list;
    }

    //根据id查找图片
    public static Image queryOne(int id) {
        return null;
    }

    //删除图片
    public static int delete(int id) {
        return 1;
    }

实现ImageDao.insert方法
public static int insert(Image image){
        Connection connection = Util.getConnection();
        PreparedStatement statement = null;

        try {
            String sql = "insert into image_table values(null,?,?,?,?,?,?)";   //自增的用null
            statement = connection.prepareStatement(sql);
            statement.setString(1,image.getImageName());
            statement.setLong(2,image.getSize());
            statement.setString(3,image.getUploadTime());
            statement.setString(4,image.getMd5());
            statement.setString(5,image.getContentType());
            statement.setString(6,image.getPath());

            return statement.executeUpdate();
        } catch (SQLException throwables) {
            throw new RuntimeException("新增上传图片出错",throwables);
        } finally {
            Util.close(connection,statement);
        }
    }
实现ImageDao.queryCount方法
public static int queryCount(String md5) {
        Connection connection = Util.getConnection();
        String sql = "select count(0) c from image_table where md5 = ?";
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        int num = 0;
        try {
            statement = connection.prepareStatement(sql);
            statement.setString(1,md5);
            resultSet = statement.executeQuery();
            resultSet.next();
            return resultSet.getInt("c");
        } catch (SQLException throwables) {
            throw new RuntimeException("查询上传图片md5出错:"+md5,throwables);
        } finally {
            //进行关闭操作
            Util.close(connection,statement,resultSet);
        }
    }
实现ImageDao.queryAll方法
public static List<Image> queryAll() {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            connection = Util.getConnection();
            String sql = "select * from image_table";
            statement = connection.prepareStatement(sql);
            resultSet = statement.executeQuery();
            List<Image> list = new ArrayList<>();
            while (resultSet.next()) {
                Image image = new Image();

                image.setImageId(resultSet.getInt("image_id"));
                image.setPath(resultSet.getString("path"));
                image.setImageName(resultSet.getString("image_name"));
                image.setContentType("content_type");
                image.setMd5(resultSet.getString("md5"));
                list.add(image);
            }
            return list;
        } catch (SQLException throwables) {
            throw new RuntimeException("查询所有图片出错",throwables);
        } finally {
            Util.close(connection,statement,resultSet);
        }
    }
实现ImageDao.queryOne 方法
public static Image queryOne(int id) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            connection = Util.getConnection();
            String sql = "select * from image_table where image_id = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1,id);
            resultSet = statement.executeQuery();
            Image image = null;
            while (resultSet.next()) {
                image = new Image();
                image.setImageId(resultSet.getInt("image_id"));
                image.setPath(resultSet.getString("path"));
                image.setUploadTime(resultSet.getString("upload_time"));
                image.setImageName(resultSet.getString("image_name"));
                image.setContentType("content_type");
                image.setSize(resultSet.getLong("size"));
                image.setMd5(resultSet.getString("md5"));
            }
            return image;
        } catch (SQLException throwables) {
            throw new RuntimeException("查询所有图片出错",throwables);
        } finally {
            Util.close(connection,statement,resultSet);
        }
    }
实现ImageDao.delete方法
public static int delete(int id) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = Util.getConnection();
            connection.setAutoCommit(false);   //不自动提交
            String sql = "delete from image_table where image_id = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1,id);
            int n = statement.executeUpdate();
            connection.commit();   //提交,如果有出错,就会自动回滚
            return n;
        } catch (Exception throwables) {   //所有的异常都需要回滚
            try {
                connection.rollback();
            } catch (SQLException e) {
                throw new RuntimeException("删除图片回滚失败:"+id,e);
            }
            throw new RuntimeException("删除数据库图片失败: "+id,throwables);
        } finally {
            Util.close(connection,statement);
        }
    }

实现Servlet

Servlet开发

  1. @WebServlet("/") 注解
  2. Servlet类要继承HttpServlet
  3. 重写do×××方法

创建包结构

org.example.servlet

在这个包里面创建两个Servlet类,一个类用来完成图片的增删查改功能,另一个类用来展示图片的详细内容

创建ImageServlet类

@WebServlet("/image")   //Tomcat运行的时候自动new一个下面的ImageServlet
@MultipartConfig    //文件的传输需要的注解,不写会报错
public class ImageServlet extends HttpServlet {

    public static final String IMAGE_DIR = "D://比特//image";   //自己的本地路径,用来存储图片

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }

    //展示图片
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }
}

实现Image.doPost类

这个方法对应的是上传图片
这里需要用到 Commons FileUpload, 可以在 Maven 仓库中找到这个包, 配置到pom.xml里面

    <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.4</version>
    </dependency>
@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        resp.setCharacterEncoding("utf-8");
        resp.setContentType("application/json");

        //构造要返回的响应体信息,也可以创建一个类
        Map<String,Object> map = new HashMap<>();

        try {
            //1.解析请求数据
            //因为前端的数据类型时multipart/form-data(文件上传,上传的文件可以是任意的类型)  所以不能用之前的接收方式来接受前端给到的数据了,只能用getPart的方式接收前端的数据
            Part p = req.getPart("uploadImage");   //前端的name
            //p.write("D://tupianServer");   //保存文件到服务端的路径
            Long size = p.getSize();     //获得上传文件的大小
            String contentType = p.getContentType();    //不是数据头信息里面的type,而是获取每个part(键值对)的数据格式(在body里面显示的type)
            String name = p.getSubmittedFileName();    //获取上传文件的名字

            //图片上传时间,数据库保存的是字符串,所以用日期格式化来转换
            Date date = new Date();   // java.util底下的包
            DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  //日期格式化,括号里面代表日期的格式
            String uploadTime = df.format(date);

            //md5的获得时通过part的输入流
            InputStream is = p.getInputStream();     //获取上传文件的输入流,就是数据
            String md5 = DigestUtils.md5Hex(is);     //根据输入流转md5

            //如果已上传该图片(md5相同),就不能插入数据和保存本地
            int num = ImageDAO.queryCount(md5);
            if (num >= 1) {
                //代表数据库里面有这张图片,不能插入图片了
                throw new AppException("上传图片重复");    //抛出自定义异常
            }

            //path 保存为相对路径
            p.write(IMAGE_DIR+"/"+md5);  //上传的图片名可能重复,但是md5是唯一的,这样写的图片的名字就不会重复

            //代码走到这代表这张图片在数据库里面不存在
            Image image = new Image();
            image.setMd5(md5);
            image.setSize(size);
            image.setContentType(contentType);
            image.setImageName(name);
            image.setUploadTime(uploadTime);
            image.setPath("/"+md5);
            int n = ImageDAO.insert(image);
            map.put("ok",true);
            
        }catch (Exception e) {
            e.printStackTrace();
            //resp.setStatus(500);   //只要上面出错,不论是什么错误,都返回500

            map.put("ok",false);
            if (e instanceof AppException) {
                map.put("msg", e.getMessage());
            } else {
                map.put("msg","未知错误,请联系管理员");
            }

        }

        //返回响应数据
        String s = Util.serialize(map);
        resp.getWriter().println(s);
    }

实现Servlet.doGet类

这里要分成两种情况,一个是获取所有图片信息,一个是获取单张图片信息
根据请求中是否带有ImageId参数来决定

@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        resp.setCharacterEncoding("utf-8");
        resp.setContentType("application/json");

        String id = req.getParameter("imageId");
        Object o = null;
        if (id == null) {
            //代表查询所有的图片   o = List<Image>
            o = ImageDAO.queryAll();
        } else {
            //查询指定id的图片  o = image对象
            o = ImageDAO.queryOne(Integer.parseInt(id));
        }
        String json = Util.serialize(o);
        resp.getWriter().println(json);
    }

实现Servlet.doDelete类

@Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        resp.setCharacterEncoding("utf-8");
        resp.setContentType("application/json");

        Map<String,Object> map = new HashMap<>();

        try {
            String id = req.getParameter("imageId");
            //数据库和本地文件都要删除,数据库根据id删除图片数据,

            Image image = ImageDAO.queryOne(Integer.parseInt(id));
            if (image == null) {
                //代表数据库里面没有这张图片删除失败
                throw new AppException("删除图片失败,数据库没有这张图片,请刷新重试");
            }
            int num = ImageDAO.delete(Integer.parseInt(id));
            //本地硬盘删除图片文件
            String path = IMAGE_DIR + image.getPath();
            File f = new File(path);
            f.delete();  //删除操作   删除不掉可能有两个原因1.前面用到了文件的操作,操作结束之后没有进行关闭文件  2.可能是因为文件在C盘,没有操作的权限
            //ok(resp);
            map.put("ok",true);

        } catch (Exception e) {

            map.put("ok",false);
            if (e instanceof AppException) {
                map.put("msg",e.getMessage());
            } else {
                map.put("msg","未知错误");
            }
        }

        String s = Util.serialize(map);
        resp.getWriter().println(s);
    }

实现ImageShowServlet

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

    //白名单链表
    private static final Set<String> whiteList = new HashSet<>();
    static {   //白名单防盗链,白名单允许访问的内容,还可以设计为白名单+黑名单的方式
        whiteList.add("http://localhost:8080/java_image_server/index.html");
        whiteList.add("http://localhost:8080/java_image_server/");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String referer = req.getHeader("Referer");
        if (!whiteList.contains(referer)) {
            //白名单不包含当前请求的referer,不允许访问,返回一个403(登录了没有身份权限)401(没有登录,没有访问权限)
            resp.setStatus(403);
            //还可以设置响应体数据
            return;   //后面的代码不能执行了
        }


        //解析请求数据   imageId
        String id = req.getParameter("imageId");

        //业务处理    根据id查找图片path字段   通过path找到本地图片文件
        Image image = ImageDAO.queryOne(Integer.parseInt(id));
        resp.setContentType(image.getContentType());   //图片是以二进制数据放在body里面,同时要制定头信息Content_type
        //本地图片的绝对路径
        String path = ImageServlet.IMAGE_DIR+image.getPath();
        //然后去本地读图片
        //io输入流读文件
        FileInputStream fis = new FileInputStream(path);
        //返回响应  服务器本地图片的二进制数据
        OutputStream os = resp.getOutputStream();    //现在是往body输出
        byte[] bytes = new byte[1024*8];   //大小设置大一点
        int len;
        while ((len = fis.read(bytes)) != -1) {
            //代表已经读完了
            os.write(bytes,0,len);    //输出
        }
        os.flush();   //刷新缓冲区   输入流和输出流是先存储在缓冲区里面的,不是马上写到你想让他写到的地方,所以需要刷新

        fis.close();
        os.close();     //这里不是放资源后面如果需要操作文件可能会失败

    }
}

基于md5实现相同图片只存在一份

整体思路

  • 将图片的md5值也上传到服务器
  • 先用md5的值查找该图片在数据库里面是否存在,然后判断能否上传该图片
  • 删除图片的时候,先利用md5判断数据库是否存在这个md5的图片,然后判断是否要进行删除

计算md5

首先在pom.xml里面引入依赖

<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
    <dependency>
      <groupId>commons-codec</groupId>
      <artifactId>commons-codec</artifactId>
      <version>1.13</version>
    </dependency>

计算md5

//md5的获得是通过part的输入流
  InputStream is = p.getInputStream();     //获取上传文件的输入流,就是数据
  String md5 = DigestUtils.md5Hex(is);     //根据输入流转md5

实现基于白名单方式的防盗链

通过HTTP中的referer字段判断是否是指定网站请求图片
在这里插入图片描述

//白名单链表
    private static final Set<String> whiteList = new HashSet<>();
    static {   
        //白名单防盗链,白名单允许访问的内容,还可以设计为白名单+黑名单的方式
        whiteList.add("http://localhost:8080/java_image_server/index.html");   
        whiteList.add("http://localhost:8080/java_image_server/");   //只有这两个url才可以访问
    }
String referer = req.getHeader("Referer");
        if (!whiteList.contains(referer)) {
            //白名单不包含当前请求的referer,不允许访问,返回一个403(登录了没有身份权限)401(没有登录,没有访问权限)
            resp.setStatus(403);
            //还可以设置响应体数据
            return;   //后面的代码不能执行了
        }

八种常见的防盗链方法总结及分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值