1 项目背景
有时候写博客时需要插入图片,提示可选择图片链接,于是就想设计一个存储图片的服务器,用来保存我们需要的图片,并且每一张图片对应唯一的URL地址,用户可直接使用URL将图片在网页上上传,方便我们使用。
2 数据库设计
数据存储模块,一方面在数据库中存储图片的属性信息,另一方面,将图片的正文信息以文件形式直接存储到磁盘上,所以数据库中记录一个 path 对应到磁盘上的文件。
建表语句:
CREATE TABLE `image_table` (
`imageId` int(11) NOT NULL PRIMARY KEY 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
)
md5是什么?(MD5_百度百科)
这是一种常见的字符串 hash 算法,具有三个特性:
- 不管源字符串多长,得到的 md5 都是固定的长度;
- 源字符串稍微变化一点点内容,md5 值会发生很大的改变;
- 计算 md5 值的过程很简单,但是通过 md5 值几乎无法推测出源字符串。
查看表结构(desc image_table;)如下:
3 服务器API设计
3.1 Json
Json 是一种常见的数据格式组织方式,源于 JavaScript ,是一种键值对风格的数据格式。Java 中可以使用 Gson 库来完成 Json 的解析和构造。
在 Maven 中新增 Gson 的依赖:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
3.2 新增图片
请求:
POST /image
Content-Type: application/x-www-form-urlencoded
...[正文内容,包含图片自身的一些信息]...
...[图片正文的二进制内容]...
响应:
上传成功:
HTTP/1.1 200 OK
{
"ok": true,
}
上传失败:
HTTP/1.1 200 OK
{
"ok": false,
"reason":"具体失败原因"
}
3.3 查看所有图片属性
请求:
GET /image
响应:
获取成功:
HTTP/1.1 200 OK
[
{
"imageId": 1,
"imageName": "1.png",
"contentType": "image/png",
"md5": "[md5值]"
...
},
{
...
},
...
]
获取失败:
HTTP/1.1 200 OK
{
"ok": false,
"reason":"具体失败原因"
}
3.4 查看指定图片属性
请求:
GET /image?imageId=1
响应:
获取成功:
HTTP/1.1 200 OK
{
"imageId": 1,
"imageName": "1.png",
"contentType": "image/png",
"md5": "[md5值]",
...
}
获取失败:
HTTP/1.1 200 OK
{
"ok": false,
"reason":"具体失败原因"
}
3.5 删除指定图片
请求:
DELETE /image?imageId=1
响应:
删除成功:
HTTP/1.1 200 OK
{
"ok": true
}
删除失败:
HTTP/1.1 200 OK
{
"ok": false,
"reason":"具体失败原因"
}
3.6 查看指定图片内容
请求:
GET /imageShow?imageId=1
响应:
响应成功:
HTTP/1.1 200 OK
content-type: image/png
[响应 body 中为 图片内容 数据]
响应失败:
HTTP/1.1 200 OK
{
"ok": false,
"reason":"具体失败原因"
}
4 封装数据库操作
4.1 创建 DBUtil 类
创建一个单例类辅助创建连接,其中 URL 为数据库连接字符串,用户名和密码都是固定的。
private static final String URL = "jdbc:mysql://127.0.0.1:3306/java_image_server?characterEncoding=utf8&useSSL=true";
private static String USERNAME ="root";
private static String PASSWORD ="";
这个类主要包含三个方法:
//这是一个获取单例的方法
public static DataSource getDataSource(){ }
//获取链接
public static Connection getConnection() { }
//关闭链接
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) { }
类的实现代码:
public class DBUtil {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/java_image_server?characterEncoding=utf8&useSSL=true";
private static String USERNAME ="root";
private static String PASSWORD ="";
//③加 volatile ,保持属性是内存可见的,使第一个线程进行操作后,其他线程可以及时看到更新
private static volatile DataSource dataSource=null;
//线程安全问题:①加锁 ②双重判断 ③加volatile
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();
}
}
}
4.2 创建 Image 类
//对应到一个图片对象(包含图片的相关属性)
public class 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 void setImageId(int imageId) {
this.imageId = imageId;
}
public String getImageName() {
return imageName;
}
public void setImageName(String imageName) {
this.imageName = imageName;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public String getUploadTime() {
return uploadTime;
}
public void setUploadTime(String uploadTime) {
this.uploadTime = uploadTime;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
@Override
public String toString() {
return "Image{" +
"imageId=" + imageId +
", imageName='" + imageName + '\'' +
", size=" + size +
", uploadTime='" + uploadTime + '\'' +
", contentType='" + contentType + '\'' +
", path='" + path + '\'' +
", md5='" + md5 + '\'' +
'}';
}
}
4.3 创建 ImageDao 类
public class ImageDao {
public boolean insert(Image image) {
return false;
}
public Image[] selectAll() {
return null;
}
public Image selectOne(int imageId) {
return null;
}
public boolean delete(int imageId) {
return false;
}
}
4.3.1 实现 ImageDao.insert 方法
/**
* 把 image 对象插入到数据库中
* @param image
*/
public void insert(Image image){
//1、获取数据库连接
Connection connection=DBUtil.getConnection();
//2、创建并拼装 SQL 语句
//imageId 是自增主键,数据库会自动地生成值; ?表示占位符,在后面会被替换成相关的属性
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("插入数据库出错!");
//throw 之后程序会直接走到 catch 处,所以关闭连接操作必须放到 finally 中,否则会出问题
}
} catch (SQLException | JavaImageServerException e) {
e.printStackTrace();
}finally {
//4、关闭连接和 statement 对象
DBUtil.close(connection,statement,null);
}
}
测试一下 insert 方法:
public static void main(String[] args) {//用于简单的测试
//1、测试插入数据
Image image=new Image();
image.setImageName("1.png");
image.setSize(100);
image.setUploadTime("20200820");
image.setContentType("image/png");
image.setPath("./data/1.png");
image.setMd5("11223344");
ImageDao imageDao=new ImageDao();
imageDao.insert(image);
}
在数据库中查看是否插入成功。
4.3.2 实现 ImageDao.selectAll 方法
/**
* 查找数据库中的所有图片的信息
* @return
*/
public List<Image> selectAll(){
List<Image> images=new ArrayList<>();
//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;
}
测试一下selectAll 方法:
public static void main(String[] args) {//用于简单的测试
//2、测试查找所有图片信息
ImageDao imageDao=new ImageDao();
List<Image> images=imageDao.selectAll();
System.out.println(images);
}
4.3.3 实现 ImageDao.selectOne 方法
/**
* 根据 imageId 查找指定图片信息
* @param imageId
* @return
*/
public Image selectOne(int imageId){
//1、获取数据库连接
Connection connection=DBUtil.getConnection();
//2、构造 SQL 语句
String sql="select * from image_table where imageId=?";
PreparedStatement statement=null;
ResultSet resultSet=null;
//3、执行 SQL 语句
try {
statement=connection.prepareStatement(sql);
statement.setInt(1,imageId);
resultSet=statement.executeQuery();
//4、处理结果集
if(resultSet.next()){//查找结果只有一条,所以用 if / while 都可以
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;
}
测试一下 selectOne 方法:
public static void main(String[] args) {//用于简单的测试
//3、测试查找指定图片信息
ImageDao imageDao=new ImageDao();
Image image=imageDao.selectOne(1);
System.out.println(image);
}
4.3.4 实现 ImageDao.delete 方法
/**
* 根据 imageId 删除指定图片
* @param imageId
*/
public void delete(int imageId){
//1、获取数据库连接
Connection connection=DBUtil.getConnection();
//2、拼装 SQL 语句
String sql="delete from image_table where imageId=?";
PreparedStatement statement=null;
//3、执行 SQL 语句
try {
statement=connection.prepareStatement(sql);
statement.setInt(1,imageId);
int ret=statement.executeUpdate();
if(ret!=1){
throw new JavaImageServerException("删除数据库操作失败");
}
} catch (SQLException | JavaImageServerException e) {
e.printStackTrace();
}finally {
//4、关闭连接
DBUtil.close(connection,statement,null);
}
}
测试一下 delete 方法:
public static void main(String[] args) {//用于简单的测试
//4、测试删除指定图片
ImageDao imageDao=new ImageDao();
imageDao.delete(1);
}
在数据库中查看是否删除成功。
5 实现Servlet
首先在项目根目录下创建一个 servlet 包,在这个包中创建两个 Servlet 类,一个用来完成图片的增删改查 (ImageServlet) ,一个用来展示图片的详细内容 (ImageShowServlet) 。
5.1 创建 ImageServlet
public class ImageServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws
ServletException, IOException {
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws
ServletException, IOException {
super.doPost(req, resp);
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws
ServletException, IOException {
super.doDelete(req, resp);
}
}
同时要记得把这个类加到 web.xml 中,其中类名要写完整的带包的名字。
<servlet>
<servlet-name>ImageServlet</servlet-name>
<servlet-class>api.ImageServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ImageServlet</servlet-name>
<url-pattern>/image</url-pattern>
</servlet-mapping>
5.1.1 实现 ImageServlet.doPost
该方法对应上传图片,这里需要用到 Commons FileUpload, 可以在 Maven 仓库中找到这个包, 并且使用 maven 下载。
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
用 upload.html 实现上传:
<html>
<head>
</head>
<body>
<form id="upload-form" action="image" method="post" enctype="multipart/form-data" >
<input type="file" id="upload" name="upload" /> <br />
<input type="submit" value="Upload" />
</form>
</body>
</html>
实现 doPost 方法:
/**
* 上传图片
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// resp.setContentType("text/html;charset=utf-8");//让浏览器以 UTF-8 的方式解析
// resp.setStatus(200);
// resp.getWriter().write("hello");
//1、获取图片的属性信息,并且存入数据库
//(1)需要创建一个 factory 对象和 upload 对象,这是为了获取图片属性做的准备工作(固定的逻辑)
FileItemFactory factory=new DiskFileItemFactory();
ServletFileUpload upload=new ServletFileUpload(factory);
//(2)通过 upload 对象进一步解析请求(解析 HTTP 请求中奇怪的 body 中的内容)
//FileItem 就代表一个上传的文件对象。
// 理论上来说,HTTP 支持一个请求中同时上传多个文件
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;
}
//(3)把 FileItem 中的属性提取出来,转换成 Image 对象,才能保存到数据库中
FileItem fileItem=items.get(0);// 当前只考虑一张图片的情况
Image image=new Image();
image.setImageName(fileItem.getName());
image.setSize((int)fileItem.getSize());
// 手动获取一下当前的日期,并转换成格式化日期,yyyyMMdd==>年月日
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/"+System.currentTimeMillis()+"_"+image.getImageName());
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.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");
}
验证该方法,可以使用刚写的 upload.html ,上传一张图片,检查服务器响应是否正确,数据库是否写入成功,图片文件是否上传成功。
5.1.2 实现 ImageServlet.doGet
这里分两种情况,一个是获取所有图片信息,一个是获取单个图片信息,根据请求中是否带有 imageId 参数来决定。
/**
* 查看图片属性:既能查看所有,也能查看指定图片
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//HttpServletRequest req —— 请求(方法,URL,各种header,body),包含了请求中的所有信息
//HttpServletResponse resp —— 响应(状态码,各种header,body),要生成的结果就放到里面去
//当前这个 doGet 方法就是要根据请求,生成响应
//在网页上显示一个 hello world ,应该修改 “响应”(响应的body部分)
// resp.setStatus(200);
// resp.getWriter().write("hello");//这个代码就是把 hello 这个字符串放到 http 响应的 body 中了
//考虑到查看所有图片属性和查看指定图片属性
//通过 URL 中是否带有 imageId 参数来进行区分
//存在 imageId 查看指定图片属性,否则就查看所有图片属性
String imageId=req.getParameter("imageId");//得到的是 String 类型的数据,如果 URL 中不存在 imageId 那么返回 null
if(imageId==null||imageId.equals("")){//不存在 imageId 或者 imageId 为空字符串
//查看所有图片属性
selectAll(req,resp);
}else{
//查看指定图片属性
selectOne(imageId,resp);
}
}
5.1.3 实现 doSelectAll
private void selectAll(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json;charset=utf-8");
//1、创建一个 ImageDao 对象,并查找数据库
ImageDao imageDao=new ImageDao();
List<Image> images=imageDao.selectAll();
//2、把查找到的结果转成 JSON 格式的对象,并写回到 resp 对象
Gson gson=new GsonBuilder().create();
// jsonData 就是一个 json 格式的字符串了,就和之前约定的格式是一样的了
// 重点体会下面这行代码,这个方法的核心,gson 帮我们做了大量的格式转换工作
// 只要之前的相关字段都约定成统一的命令,下面的操作就可以一步到位地完成整个转换
String jsonData=gson.toJson(images);
resp.getWriter().write(jsonData);
}
5.1.4 实现 doSelectOne
private void selectOne(String imageId, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json;charset=utf-8");
//1、创建一个 ImageDao 对象
ImageDao imageDao=new ImageDao();
Image image=imageDao.selectOne(Integer.parseInt(imageId));//当前得到的 imageId 还是 String 类型,还应该转换一下它的类型
//2、使用 gson 把查到的数据转换成 json 格式,并写回给响应对象
Gson gson=new GsonBuilder().create();
String jsonData=gson.toJson(image);
resp.getWriter().write(jsonData);
}
5.1.5 实现 ImageServlet.doDelete
/**
* 删除指定图片
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json;charset=utf-8");
//1、先获取请求中的 imageId
String imageId=req.getParameter("imageId");
if(imageId==null||imageId.equals("")){
resp.setStatus(200);
resp.getWriter().write("{\"ok\":false,\"reason\":\"解析请求失败\"}");
return;
}
//2、创建 ImageDao 对象,查看到该图片对象对应的相关属性(为了得到图片对应的文件路径)
ImageDao imageDao=new ImageDao();
Image image=imageDao.selectOne(Integer.parseInt(imageId));
if(imageId==null){
//此时请求中传入的 imageId 在数据库中不存在
resp.setStatus(200);
resp.getWriter().write("{\"ok\":false,\"reason\":\"imageId 在数据库中不存在\"}");
return;
}
//3、删除数据库中的记录
imageDao.delete(Integer.parseInt(imageId));
//4、删除本地磁盘文件
File file=new File(image.getPath());
file.delete();
resp.setStatus(200);
resp.getWriter().write("{\"ok\":true}");
}
5.2 创建 ImageShowServlet
/**
* 查看指定图片内容
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1、解析出 imageId
String imageId=req.getParameter("imageId");
if(imageId==null||imageId.equals("")){
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write("{\"ok\":false,\"reason\":\"imageId 解析失败\"}");
return;
}
//2、根据 imageId 查找数据库,得到对应图片的属性信息(需要知道图片存储的路径)
ImageDao imageDao=new ImageDao();
Image image=imageDao.selectOne(Integer.parseInt(imageId));
//3、根据路径打开文件,读取其中的内容,写入到响应对象中
resp.setContentType(image.getContentType());
File file=new File(image.getPath());
// 由于图片是二进制文件,应该使用字节流的方式读取文件
OutputStream outputStream=resp.getOutputStream();
FileInputStream fileInputStream=new FileInputStream(file);
byte[] buffer=new byte[1024];
while(true){
int len=fileInputStream.read(buffer);
if(len==-1){
//文件读取结束
break;
}
//此时已经读到一部分数据,放到 buffer 里,把 buffer 中的内容写到响应对象中
outputStream.write(buffer);
}
fileInputStream.close();
outputStream.close();
}
修改 web.xml :
<servlet>
<servlet-name>ImageShowServlet</servlet-name>
<servlet-class>api.ImageShowServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ImageShowServlet</servlet-name>
<url-pattern>/imageShow</url-pattern>
</servlet-mapping>
6 写前端页面
6.1 使用 HTML 模板
直接在网上搜索免费的网页模板,将其解压缩,拷贝到项目的 webapp 目录中。删除模板中不需要的部分,保留自己所需要的部分。
6.2 使用 Vue.js
创建 Vue 对象:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
6.3 实现展示图片
构造数据:
修改 js 代码:
var app = new Vue({
el:'#app',
data: {
images: [
{
imageId: 1,
imageName: "1.png",
contentType: "image/png",
md5: "aabbccdd",
},
{
imageId: 2,
imageName: "2.png",
contentType: "image/png",
md5: "aabbccdd",
}
]
},
methods: {
},
});
修改 html 代码,和数据关联:
- 使用 v-bind:src 把图片的 src 通过 imageShow 接口获取到
- 使用 {{image.imageName}} 表示图片标题
<div class="am-g am-g-fixed blog-fixed blog-content" id="app">
<figure data-am-widget="figure" class="am am-figure am-figure-default " data-am-figure="{ pureview: 'true' }">
<div id="container">
<div v-for="image in images">
<img style="width: 200px;height: 200px" v-bind:src="'imageShow?imageId='+image.imageId">
<h3>{{image.imageName}}</h3>
</div>
</div>
</figure>
</div>
从服务器获取数据:
在 method 中新增获取所有图片的方法:
getImages(){
$.ajax({
url:"image",
type:"get",
context:this,
success:function(data,status) {
//此处的代码在浏览器收到响应后,才会执行到
//参数中的 data 就相当于收到的 HTTP 响应中的 body 部分
this.images=data;
$('#app').resize();
}
})
}
//页面加载时调用
app.getImages();
部署到服务器上,测试效果。
6.4 完善上传功能
当前的上传请求会返回一个 JSON 格式的数据,而我们更需要的是直接能看到上传的效果,所以修改上传接口的响应,直接返回一个 302 响应,重定向回主页。
修改 ImageServlet.doPost ,在上传成功代码最后,加上一个重定向。
resp.sendRedirect("index.html");
6.5 实现删除图片
图片下面新增删除按钮:
<button style="width: 100%" v-on:click="remove(image.imageId)" class="am-btn am-btn-success">删除</button>
实现事件处理函数:
remove(imageId){
$.ajax({
url:"image?imageId=" + imageId,
type:"delete",
context:this,
success:function (data,status) {
this.getImages();
//弹出对话框
alert("删除成功!");
}
})
}
验证删除效果。
阻止点击事件冒泡:
此时发现个问题, 点击删除按钮之后, 会触发预览图片效果。这是因为 JavaScript 的事件冒泡机制导致的.,一个标签接受到的事件会依次传给父级标签。此处需要阻止 click 事件冒泡,Vue 中使用 v-on:click.stop 即可。
<button style="width: 100%" v-on:click.stop="remove(image.imageId)" class="am-btn am-btn-success">删除</button>
7 后续扩展点
7.1 实现基于白名单方式的防盗链
通过 HTTP 中的 refer 字段判定是否是指定网站请求图片,修改 ImageShowServlet.doGet 方法。
新增属性:
static private HashSet<String> whiteList=new HashSet<>();
static {
whiteList.add("http://127.0.0.1:8085/java_image_server/index.html");
}
新增以下逻辑:
String referer=req.getHeader("Referer");
if(!whiteList.contains(referer)){
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write("{\"ok\":false,\"reason\":\"未授权的访问\"}");
return;
}
7.2 基于 MD5 实现相同内容图片只存一张
整体思路:
- 修改上传图片代码, 使用 md5 作为文件名;
- 修改 DAO 层代码, 在 DAO 层实现一个 selectByMD5 方法, 根据 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.14</version>
</dependency>
修改 ImageServlet.doPost 方法,实现计算 MD5。
image.setMd5(DigestUtils.md5Hex(fileItem.get()));
image.setPath("./image/"+image.getMd5());
修改 ImageDao :
新增方法 selectByMD5:
//通过 MD5 查找数据库内容
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;
//3、执行 SQL 语句
try {
statement=connection.prepareStatement(sql);
statement.setString(1,md5);
resultSet=statement.executeQuery();
//4、处理结果集
if(resultSet.next()){//查找结果只有一条,所以用 if / while 都可以
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;
}
根据 MD5 决定写入文件:
修改 ImageServlet.doPost 方法,如果该 MD5 值的文件不存在, 才真的写入磁盘;如果该 MD5 值的文件存在,则直接使用原来的文件,不必再写一次磁盘。
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.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");
根据 MD5 决定删除文件:
多个图片对应一个文件,删除任何一个图片,都会导致文件被删除,该怎么办呢?
其实通过 selectByMd5 值对应的图片在数据库中是否存在,如果不存在这个 MD5 ,才真正删除磁盘文件。
8 最终效果及总结
整体框架: