目录
背景需求
整体架构
核心就是一个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类
在这个类我们要完成以下几个内容
- 获取请求数据:获取图片id
- 根据图片id,在数据库查询图片数据
- 返回响应:读取本地图片文件,把二进制数据设置到响应体
代码:
@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);
}
}
项目展示
上传
删除