一、项目背景
图床, 解决 github / 博客中插入图片的问题。
二、整体架构
核心就是一个 HTTP 服务器, 提供对图片的增删改查能力.
同时搭配简单的页面辅助完成图片上传/展示。需要实现的功能/接口:
- 显示图片列表;
- 显示图片内容;
- 上传图片;
- 删除图片;
三、技术分析
- 因为存在删除、上传的操作,所以前端页面需要是动态网页。实现动态网页的技术有三种:(1)servlet返回字符串拼接的html内容;(2)模板技术(3)ajax动态响应动态生成html内容。本次项目使用第三种方式。
- 图片的信息,包括:名称,Http路径,图片重复标识(为了避免上传的图片重复而存在的唯一标识,类似于hashcode字段)。信息需要存储在数据库中,返回在图片列表里,数据类型是List< T >。
- 图片的数据:在页面初始化的时候就由浏览器自动发ajax请求,展示出来。
四、业务设计
- 数据库设计:考虑包括的字段
- 接口设计:考虑请求方法,请求数据格式;响应数据
- 开发环境:前端(ajax,vue,jquery);后端:servlet,JDBC,commons-codec(apache提供做加密的依赖包),jackson,lombok。
五、详细设计
1.数据库设计
-- 准备表
create table image_info(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 'md5值,用于校验图片唯一',content_type varchar(50) comment '数据类型,上传图片时包含在报文中的 ',path varchar(1024) comment'图片路径:相对路径');
2.实体类设计
在数据库建立好后,需要根据字段实现实体类,因为JDBC操作及返回http响应都需要使用实体类;
//lombok注解,加上以后,自动在编译的class文件中生成这些方法
@Getter
@Setter
@ToString
public class ImageInfo {private Integer imageId;private String imageName ;private long size;private java.util.Date uplaodeTime;private String md5;private String contentType;private String path;
}
3.前端设计
UI框架需要自己在网上找来使用,这里只做涉及到前后端交互的接口。
- 采用了VUE框架(可以让数据和DOM元素双向绑定),同时使用了jQuery中封装好的ajax函数来渲染页面和发送请求数据。VUE版本:2.6.12
- 前端的执行流程:
-
访问index.html,页面初始化就发送Ajax请求获取图片列表信息。 对于返回的响应(这里要求返回imageID和imageName两个字段)通过vue的绑定渲染到相应的位置。从而显示图片列表。
-
有了图片列表,浏览器自动根据<img src=" "来发送请求获取图片内容。
-
上传图片功能的实现:利用vue绑定提交事件函数,提交form表单。事件函数:
-
删除图片:利用vue绑定删除事件函数即可。事件函数:
<!-- content srart --><div class="am-g am-g-fixed blog-fixed blog-content"><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"><--这里使用了v-bind:src来拼接images数组里的内容--》<img v-bind:src="'imageShow?imageId=' + image.imageId" style="height:200px; width:200px"><h3 style="margin-left: auto; margin-right: auto;">{{image.imageName}}</h3><button style="width:100%" class="am-btn am-btn-success" v-on:click.stop="remove(image.imageId)">删除</button></div></div></figure></div>
</div>
<script>var app = new Vue({el:'#app',data: {images: [],uploadImage: ''},methods: {getImages() {$.ajax({url: "image",type: "get",context: this,//响应码200时执行,将服务端响应的data赋值给images对象,随后便通过vue的绑定修改相应的dom,以后便可以渲染到页面上,success: function(data, status) {this.images = data;$("#app").resize();}})},changeImage(event){app.uploadImage = event.target.files[0];},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("上传成功");},error: function (err, textStatus, throwable) {console.error(JSON.stringify(err))}})},remove(imageId) {$.ajax({url:"image?imageId=" + imageId,type:"delete",context: this,success: function(data, status) {app.getImages();alert("删除成功");}})}},});app.getImages();
</script>
4.API设计
(1)常用功能封装
- 封装数据库连接池
public class DBUtil {//双重校验锁方式,封装数据库连接池(单例模式)//1.volatile修饰静态变量//2.两个if判断,中间进行synchronized加锁操作private static volatile DataSource DS;private static DataSource getDtaSource(){if(DS==null){synchronized (DBUtil.class){if(DS==null){MysqlDataSource dataSource=new MysqlDataSource();dataSource.setURL("jdbc:mysql://localhost:3306/image_system");dataSource.setUser("root");dataSource.setPassword("666666");dataSource.setUseSSL(false);dataSource.setCharacterEncoding("utf8");DS=dataSource;}}}return DS;}//提供一个开放的连接方式public static Connection getConnection(){try {return getDtaSource().getConnection();} catch (SQLException e) {throw new RuntimeException("获取数据库连接失败",e);}}@Testpublic void testGetConnection(){System.out.println(getConnection());}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 e) {throw new RuntimeException("释放数据库资源出错",e);}}
}
- 序列化与反序列化操作封装
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) throws IOException {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);
// Map<String,Object> body=new HashMap<>();
// body.put("ok",false);
// body.put("msg","json序列化失败");
// resp.getWriter().write(M.writeValueAsString(body));}}//反序列化public static <T> T deserialize(HttpServletRequest req,Class<T> clazz){try {return M.readValue(req.getInputStream(),clazz);} catch (IOException e) {throw new RuntimeException("反序列化失败",e);}}
}
(2)针对路径问题的考虑
- 上传需要考虑到的图片路径的问题:上传到数据库时,需要保存Path;图片本身是要保存在本地硬盘上,也涉及到路径;显示文件内容时,前端也需要路径< img src=“xxxx”;因此需要综合考虑这些路径之间的关系。
- 对于上传到数据库中保存的路径信息字段名为path(是服务端自定义的,这里是一个md5值),但是数据库中并不保存完整路径,完整路径=本地路径前缀+自定义后缀
- 前端显示的路径由
<img v-bind:src="'imageShow?imageId=' + image.imageId">
决定 - 后端servlet需要提供imageShow的接口:通过解析imageID,找到文件在本地的路径(完整路径),然后把二进制数据写入响应体。其中解析方法是:1,通过Id在数据库中找到对应的数据(包含path字段)2.拼接上前缀就可以找到图片在本地的真实路径;
(3)上传接口设计
上传的流程:提出上传请求(前端)-》接收请求(后端)-》上传数据(后端)-》图片保存在服务端硬盘(后端)-》图片信息添加到数据库中(后端)-》返回响应(后端)-》接收响应(前端)-》渲染(前端)
接口实现的步骤:
- 获取请求数据:获取图片Part对象
- 保存上传图片到本地
- 保存图片信息在数据库
- 构建响应数据
上传数据使用到的工具:@MultipartConfig注解
@WebServlet("/image")
@MultipartConfig
public class imageServlet extends HttpServlet {public static final String LOCAL_PATH_PREFIX="D:\\Develop\\Java_Develop\\图片服务器\\TMP";//图片上传接口@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setCharacterEncoding("utf-8");//请求数据:uploadImage=图片数据Part p=req.getPart("uploadImage");//TODO 先验证MD5值//1.保存在服务端本地:文件名为md5名String md5= DigestUtils.md5Hex(p.getInputStream());//根据md5值查找数据ImageInfo imageInfo=imageDao.selectByMd5(md5);if(imageInfo!=null){//已经存在图片信息,说明上传重复Map<String,Object> data=new HashMap<>();data.put("ok",false);data.put("msg","上传图片重复");WebUtil.serialize(resp,data);return;}p.write(LOCAL_PATH_PREFIX+"/"+md5);//2.保存在数据库//先构造数据库保存的信息ImageInfo image=new ImageInfo();image.setImageName(p.getSubmittedFileName());image.setSize(p.getSize());image.setUplaodeTime(new java.util.Date());image.setMd5(md5);image.setContentType(p.getContentType());image.setPath("/"+md5);//插入数据库数据int n= imageDao.insert(image);Map<String,Object> data=new HashMap<>();data.put("ok",true);WebUtil.serialize(resp,data);}
(4)获取图片列表接口设计
这一部分只是获取到图片的信息,图片本身还没有获取到。针对这个接口,servlet需要做的是:查询所有图片的数据,多行数据转换为List< ImageInfo >,并设置到响应体返回。
//获取图片列表接口;@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//查询数据库所有图片并返回List<ImageInfo> images=imageDao.selectAll();//返回响应WebUtil.serialize(resp,images);}
(5)获取图片内容接口
在前端:<img v-bind:src="'imageShow?imageId=' + image.imageId">
会在页面初始化时自动发出请求获取图片内容。请求:“GET” imageShow?imageId=n;
而servlet响应体设置为图片的二进制数据:使用了文件工具类Files的readAllByte(Path)方法,以二进制返回一个路径下的所有数据, byte[] data=Files.readAllBytes(pic.toPath());
.其中Path对象可以根据File对象来转换。
//获取图片内容接口@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.获取请求数据:获取图片IdString imageId =req.getParameter("imageId");//2.根据图片Id,在数据库查询图片数据ImageInfo imageInfo= imageDao.selectOne(Integer.parseInt(imageId));//3.返回响应String path=imageServlet.LOCAL_PATH_PREFIX+imageInfo.getPath();//读取这个路径的文件File pic=new File(path);byte[] data=Files.readAllBytes(pic.toPath());//把二进制图片数据写入到响应正文resp.getOutputStream().write(data);}
}
(6)删除图片接口
这一部分在前端的设计是:
因此在后端设计中是根据图片ID来删除。在从本地删除pic.delete();
的同时,也要从数据库中删除,这里封装了函数imageDao.delete(id);
。
存在问题:本地文件若存储在C盘,ideal是没有权限删除文件的,pic.delete();
会返回false;本项目没有考虑到删除失败的问题。
//删除接口@Overrideprotected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.获取图片idString imageId=req.getParameter("imageId");Integer id=Integer.parseInt(imageId);//2.删除本地文件,先查询ImageInfo imageInfo=imageDao.selectOne(id);String path=LOCAL_PATH_PREFIX+imageInfo.getPath();File pic=new File(path);//删除文件pic.delete();//3.删除数据库中数据int n=imageDao.delete(id);//4.返回响应数据dataMap<String ,Object>data=new HashMap<>();data.put("ok",true);WebUtil.serialize(resp,data);}
5.项目优化
目前存在着一些问题:
1.重复图片上传的验证
对于数据库来说即使是重复的图片仍能上传到数据库,产生了新的数据,所以会降低性能;对于本地文件来说,是会覆盖掉,所以本地问题不大。
解决方案:校验Md5值,即在保存之前先根据md5值在数据库中查找一遍,如果有查找到数据,就不再保存。
ImageInfo imageInfo=imageDao.selectByMd5(md5);if(imageInfo!=null){//已经存在图片信息,说明上传重复Map<String,Object> data=new HashMap<>();data.put("ok",false);data.put("msg","上传图片重复");WebUtil.serialize(resp,data);return;}
2.图片防盗链
很多网站会在html中的src中引用别人的图片,如果仅想被白名单网站引用,就要设置防盗链。
解决方案:根据请求报文中Referer字段的值来判断这是哪个网页发起的请求,同时决定要不要让他访问。
在图片显示servlet里加上白名单列表:
//白名单列表:如果放云服务器是需要改地址private static final List<String> WHITE_LIST= Arrays.asList("http://localhost:8080/java_image_server/","http://localhost:8080/java_image_server/index.html");
同时在doGet方法中加上:
//防盗链String referer=req.getHeader("Referer");//不在白名单中if(!WHITE_LIST.contains(referer)){resp.setStatus(403);return;}