一、项目简介
图片服务器:解决项目中插入图片的问题
二、功能及场景
1.功能/接口
- 显示图片列表
- 显示图片内容
- 上传图片
- 删除图片
模拟的html要展示的图片列表:
因为存在上传和删除操作,所以列表是动态变化=>需要是动态网页
(1) servlet返回Java字符串拼接的html内容
(2) 模板技术
(3) ajax 根据响应来动态生成html内容(本项目以此方式实现)
2.图片服务器的应用场景:
常见的:专门提供图片下载浏览的网站,图床
写博客文章,可以上传图片
三、业务设计
系统设计
数据库设计
接口设计:考虑请求方法,请求路径,请求数据(格式)﹔响应数据(json,图片格式)
基础设施搭建:
maven项目
前端技术: ajax,vue(前端js框架),jquery (只用了这个框架提供的ajax函数来发请求)
后端技术: Servlet, jdbc,commons-fileupload, commons–codec(唯一性验证的框架,通过上传的图片生成md5来验证), jackson,lombok
四、数据库设计
准备图片表
-- 准备表:
-- 注意:图片表的字段,转为实体类的成员变量(名称会关联前后端接口)
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 '数据类型,上传图片时,请求数据就包含Content-Type',
path varchar(1024) comment '图片的路径: 相对路径'
);
准备实体类
数据库jdbc操作(插入,修改,查询),返回http响应数据,都需要使用实体类。
把数据库的表转为类,字段转为成员变量。
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class ImageInfo {
//主键id
private Integer imageId;
//图片名称
private String imageName;
//图片大小
private Long size;
//图片上传日期
private java.util.Date uploadTime;
//md5校验码:通过一段数据(字符串,数值,二进制)生成
private String md5;
//数据格式:http请求上传form-data数据格式时,图片字段还可以包含Content-Type
private String contentType;
//图片路径:相对路径
private String path;
}
五、API设计
常用功能封装
- 封装数据库连接池
public class DBUtil {
//封装数据库连接池(双重校验锁的线程安全的单例模式)
private static volatile DataSource DS;
private static DataSource getDataSource(){
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("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);
}
}
@Test
public 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){
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格式数据,转换为Java对象
public static <T> T deserialize(HttpServletRequest req, Class<T> clazz){
try {
return M.readValue(req.getInputStream(), clazz);
} catch (IOException e) {
throw new RuntimeException("反序列化失败", e);
}
}
}
文件上传
上传需要考虑到的图片路径的问题:上传到数据库时,需要保存Path;图片本身是要保存在本地硬盘上,也涉及到路径;显示文件内容时,前端需要路径为< img src=“xxxx”>;因此需要分析这些路径该如何设置。
- 对于上传到数据库中保存的路径信息字段名为path(是服务端自定义的,这里是一个md5值),但是数据库中并不保存完整路径——为自定义后缀,完整路径=本地路径前缀(服务端本地路径前缀)+自定义后缀
- 前端显示的路径由
<img v-bind:src="'imageShow?imageId=' + image.imageId">
决定 - 后端servlet提供/imageShow的接口:通过解析imageId, 找到文件在服务端本地的路径(完整路径),然后把二进制数据写入响应体。其中解析方法是:1,通过Id在数据库中找到对应的数据(包含path字段)2.拼接上前缀就可以找到图片在本地的真实路径;
文件上传
文件上传接口请求如下:
针对接口的请求,后端servlet进行处理返回响应,整体步骤分为以下操作:
- 获取请求数据:uploadImage=图片数据 。获取图片Part对象
- 保存图片完整路径在服务端本地硬盘: 完整路径为路径前缀+后缀(/MD5值)
- 保存图片信息在数据库
- 返回响应: {ok: boolean, msg: String} ,是根据接口所需响应设置此两个字段
//图片上传接口
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//请求数据:uploadImage=图片数据
Part p = req.getPart("uploadImage");
//1.保存在服务端本地硬盘: 完整路径为路径前缀+后缀(自己约定规则)
//我们这里简单的,用md5值作为文件名(/md5值作为后缀)
//md5操作,可以基于byte[],String,InputStream
//先根据上传的图片,生成md5值
String md5 = DigestUtils.md5Hex(p.getInputStream());
//保存在服务端本地硬盘:路径前缀(常量)+后缀(/md5值)
p.write(LOCAL_PATH_PREFIX+"/"+md5);
//2.保存在数据库
//先构造一个ImageInfo对象,来保存要插入数据库的数据
ImageInfo image = new ImageInfo();
//设置图片名称:上传的文件名
image.setImageName(p.getSubmittedFileName());
//设置图片大小:上传的文件大小
image.setSize(p.getSize());
//设置上传日期:当前日期
image.setUploadTime(new java.util.Date());
//设置md5
image.setMd5(md5);
//设置数据格式/类型: 上传的文件格式(注意,不是请求的数据格式,是form-data上传的图片字段的格式)
image.setContentType(p.getContentType());
//设置路径:设置为路径后缀(/md5值)
image.setPath("/"+md5);
//插入数据库图片数据 jdbc操作
int n = ImageDao.insert(image);
//返回响应: {ok: boolean, msg: String}
Map<String, Object> data = new HashMap<>();
data.put("ok", true);//我们不返回错误,出错就让tomcat返回500状态码
WebUtil.serialize(resp, data);
}
获取图片列表接口
返回:[{imageId: 1, imageName: “”}]
查询数据库所有图片,并返回(List),设置到响应体中
//获取图片列表接口:返回[{imageId: 1, imageName: ""}]
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//查询数据库所有图片,并返回(List<ImageInfo>)
List<ImageInfo> images = ImageDao.selectAll();
//返回响应
WebUtil.serialize(resp, images);
}
此时运行后,图片列表展示效果如下:
获取图片内容
- 请求:GET /imageShow?imageld=1
其中, imageld是获取图片列表接口响应的数据,来填充进去的 - 响应体为图片的二进制数据(响应的数据格式,可以设置,也可以不设置)
后端servlet处理get请求步骤如下:
- 获取请求数据:图片id
- 根据图片id,在数据库查询图片数据(path字段)
- 根据图片完整路径,读取本地图片,把二进制数据设置到响应体
//获取图片内容接口:GET /imageShow?imageId=1
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1.获取请求数据:获取图片id,获取queryString,是getParameter获取
String imageId = req.getParameter("imageId");
//2.根据图片id,在数据库查询图片数据(包含path字段:路径后缀)
ImageInfo imageInfo = ImageDao.selectOne(Integer.parseInt(imageId));
//3.返回响应:读取本地图片文件,把二进制数据设置到响应体
//读取本地图片:完整路径=路径前缀+路径后缀(path字段)
String path = ImageServlet.LOCAL_PATH_PREFIX+imageInfo.getPath();
//读取这个路径的文件
//读取方式一:通过FileInputStream文件输入字节流来读取(参考io操作的代码)
//读取方式二:byte[] b = Files.readAllBytes(path) 是工具方法,读取一个路径的所有数据,
//Path对象,可以通过File对象转换
File pic = new File(path);
byte[] data = Files.readAllBytes(pic.toPath());
//把图片二进制数据,写入到响应正文
//严格来说要设置响应数据格式Content-Type,但前端是<img>使用,所以没有也可以
resp.getOutputStream().write(data);
}
删除图片接口
后端处理delete请求步骤如下:
- 获取请求数据: 获取图片id
- 根据图片id查询到数据(包含path),拼接完整路径在本地删除
- 删除数据库图片数据
- 返回响应数据
六、项目优化
目前存在的问题:
1.重复图片上传:目前还是会接收并保存(降低效率)
解决方法:使用md5值,在数据库查询是否存在,如果存在就返回报错信息:上传重复图片
//md5操作,可以基于byte[],String,InputStream
//先根据上传的图片,生成md5值
String md5 = DigestUtils.md5Hex(p.getInputStream());
// 可以先验证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.图片防盗链
只提供给授权(允许)的网站使用=>防盗链
解决方法:通过http请求报文中,Referer这个请求头,可以知道,当前这个http请求,是从哪个页面发起的(上一个页面是哪个)。我们就可以根据Referer的值,来判断是否允许访问。
白名单:提供一个数组/列表,在范围内的,才允许访问
/白名单列表:本机访问本机时,是以下路径,如果放在云服务器,需要改
private static final List<String> WHITE_LIST = Arrays.asList(
"http://localhost:8080/java_image_server/"
,"http://localhost:8080/java_image_server/index.html"
,"http://localhost:8080/java_image_server/index2.html"
);
获取图片内容接口,先校验Referer请求头,在白名单列表中,才允许访问:
//获取图片内容接口:GET /imageShow?imageId=1
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//防盗链:先获取Referer请求头,在白名单列表中,才允许访问,否则返回403
String referer = req.getHeader("Referer");
//不在白名单中
if(!WHITE_LIST.contains(referer)){
//返回403
resp.setStatus(403);
return;
}
七、测试
自动化测试
采用selenium和unittest框架完成了自动化测试,对项目中图片上传功能和删除图片部分编写简单的脚本,并生成测试报告。
from HTMLTestRunner import HTMLTestRunner
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
import unittest
import testImageServer1
from ddt import ddt
@ddt
class testImageServer(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.url = "http://localhost:8080/java_image_server"
self.driver.get(self.url)
self.driver.maximize_window()
time.sleep(3)
def tearDown(self):
self.driver.quit()
# 上传图片
def test_upload(self):
driver = self.driver
driver.get(self.url)
time.sleep(3)
driver.find_element(By.XPATH, "//*[@id='blog-collapse']/form/div[1]/input").send_keys(
r"C:\Users\xyy\Pictures\zhinengxinxi\Denoised.jpg")
time.sleep(3)
driver.find_element(By.XPATH, "//*[@id='blog-collapse']/form/div[2]/input").click()
time.sleep(20)
# 删除图片
def test_delete(self):
driver = self.driver
driver.get(self.url)
time.sleep(3)
driver.find_element(By.XPATH, "//*[@id='container']/div[1]/button").click()
time.sleep(3)
driver.switch_to.alert.accept()
time.sleep(3)
if __name__ == '__main__':
unittest.main(verbosity=0)
import os
import sys
import time
import unittest
from HTMLTestRunner import HTMLTestRunner
import testImageServer1
def createsuit():
testima = unittest.TestSuite()
# 向测试套件添加测试用例
# testima.addTest(TestBaiDu("test_search_set"))
# testima.addTest(TestBaiDu("test_baidu_search"))
# 把一个类里面所有测试用例添加进去unittest.makeSuite(testbaidu1.Baidu1)
testima.addTest(unittest.makeSuite(testImageServer1.testImageServer))
return testima
if __name__ == '__main__':
# 1.创建一个文件夹
# 当前路径
curpath = sys.path[0]
# 当前路径下resultreport 文件夹不存在就创建一个
if not os.path.exists(curpath+'/resultreport'):
os.mkdir(curpath+'/resultreport')
# 2.解决重复命名的问题 当前时间命名
now = time.strftime("%Y-%m-%d-%H %M %S", time.localtime(time.time()))
print(time.time())
print(time.localtime(time.time()))
# 准备HTML报告输出的文件
filename = curpath+'/resultreport/'+now+'resultreport.html'
# 创建测试报告 HTML格式的测试执行报告
fp = open(filename, "wb")
# 创建执行对象
runner = HTMLTestRunner(stream=fp, title="图片服务器测试上传删除", description="用例执行情况:", verbosity=2)
suit = createsuit()
runner.run(suit)
fp.close()
文章来源:https://www.toymoban.com/news/detail-466165.html
测试用例
文章来源地址https://www.toymoban.com/news/detail-466165.html
到了这里,关于图片服务器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!