背景需求
- 解决GitHub或者是博客里面插入图片的问题。
项目技术点
- Java操作MySql数据库
- 数据库的设计
- Restful风格API(https://baike.baidu.com/item/RESTful/4406165?fr=aladdin)
- Http协议
- Servlet的使用
- 基于md5进行校验
- 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开发
- @WebServlet("/") 注解
- Servlet类要继承HttpServlet
- 重写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; //后面的代码不能执行了
}