文章目录
1. 写代码前的设计
1.1 数据库设计
- 数据库用来存储图片属性(元信息)。图片正文将以稳健的形式直接存放在磁盘上,数据库中记录一个path就对应到磁盘上的一个图片文件。
- MD5码:图片的MD5校验和,通过原串内容经过一定的规则得到一个更短的字符串来验证整体提数据是否正确
新建数据库 CREATE SCHEMA `image_system` ;
建表 CREATE TABLE `image_system`.`image_server` (
`image_id` INT NOT NULL AUTO_INCREMENT,
`image_name` VARCHAR(45) NULL,
`size` INT NULL,
`upload_time` VARCHAR(45) NULL,
`content_type` VARCHAR(45) NULL,
`path` VARCHAR(1024) NULL,
`md5` VARCHAR(1024) NULL,
PRIMARY KEY (`image_id`));
1.2 服务器API设计(前后端交互接口设计)
-
新增图片
-
查看指定图片属性
-
查看所有图片属性
-
删除指定图片
-
查看指定图片内容
-
JSON格式来完成数据序列化,方便网络传输
//JSON格式数据展示
“key1”:value1,
“key2”:value2,
“key3”:value3,
“key4”:value4,
“key5”:value5,
其中,JSON格式的数据是无序的,前后没有顺序关系,会使用到GSON开源的JSON解析库
2.后端开发
2.1连接数据库
(1) dao层先创建DBUtil—封装获取数据库连接的过程
public class DBUtil {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/image_system?characterEncoding=utf8&useSSL=false";
private static final String USERNAME = "root";
private static final String PASSWORD = "******";//
//获取连接
private static volatile DataSource dataSource = null;
private static Object PreparedStatement;
private static Object ResultSet;
//通过此方法创建DataSource的实例
public static DataSource getDataSource() throws SQLException {
if (dataSource == null) { // (2) 双重判定 降低锁冲突
//如果直接写dataSource=new MysqlDataSource()
// 多线程环境下调用是线程不安全的
//所以 改成线程安全(1)加锁 (2)双重判定 (3)volatile
synchronized (DBUtil.class) {
if (dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource) dataSource).setUrl(URL);
((MysqlDataSource) dataSource).setUser(USERNAME);
((MysqlDataSource) dataSource).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) {
// 先关 resultSet,
//再关 statement
//最后关 connection
//先创建的后关
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
(2) 创建Image类,对应到一个图片对象,限制为private,手动创建get、set方法
(3) 创建ImageDao类,Image对象管理器,借助这个类可以完成Image在数据库中的增删改查操作
(一)查找所有
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;
try {
// 3、 执行 sql, 并获取结果集合
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
// 4、处理结果集
while (resultSet.next()) {
//拼装image对象
Image image = new Image();
image.setImageId(resultSet.getInt("image_id"));
image.setImageName(resultSet.getString("image_name"));
image.setSize(resultSet.getInt("size"));
image.setUploadTime(resultSet.getString("upload_time"));
image.setContentType(resultSet.getString("content_type"));
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;
}
查找所有可以优化,若数据库中数据量过大,一次查找全部不显示,可以加入删选条件,分页显示
(二)查找指定
public Image selectOne(int imageId) {
// 1. 获取数据库连接
Connection connection = DBUtil.getConnection();
//2、构造SQL
String sql = "select * from image_table where image_id = ?";
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
// 3、 执行 SQL 语句
statement = connection.prepareStatement(sql);
statement.setInt(1, imageId);
resultSet = statement.executeQuery();
// 4、 遍历结果集合(这个结果中应该只有一个)
if (resultSet.next()) { //这里可以用if是因为根据id查找 id是唯一的 所以结果 要么只能找到一条 要么找不到
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;
}
(三)删除指定
public boolean delete(int imageId) {
// 1、 获取数据库连接
Connection connection = DBUtil.getConnection();
//2、拼装SQL语句
String sql = "delete from image_table where image_id = ?";
PreparedStatement statement = null;
try {
// 3、 执行 SQL 语句
statement = connection.prepareStatement(sql);
statement.setInt(1, imageId);
int ret = statement.executeUpdate();//影响多少行
// return ret > 0;
if (ret!=1){
throw new JavaImageServerException("删除失败");
}
} catch (SQLException|JavaImageServerException e) {
e.printStackTrace();
} finally {
//4、关闭连接
DBUtil.close(connection, statement, null);
}
return false;
}
(四)插入图片信息
public boolean insert(Image image) {
//1、获取数据库连接 用封装好的DBUtil
Connection connection = DBUtil.getConnection();
//2、创建并拼装SQL语句
PreparedStatement statement = null;
String sql = "insert into image_table values(null, ?, ?, ?, ?, ?, ?)";//7个字段
try {
// 2. 拼装 PreparedStatement
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());
System.out.println(statement);
// 3. 执行SQL语句, 返回值表示影响了几行表格
int ret = statement.executeUpdate();
// return ret > 0;
if (ret != 1) {
// 程序出现问题 抛出异常
throw new JavaImageServerException("插入数据出错!");
}
} catch (SQLException | JavaImageServerException e) {
e.printStackTrace();
} finally {
//4、关闭连接和statement对象
DBUtil.close(connection, statement, null);
}
return false;
}
插入图片信息的时候一定要注意:插入的顺序要按照一开始设计数据库的顺序插入,不然会导致插入错误,虽然在插入的时候不会报错。但是如果路径插入错误后续就无法根据路径找到图片并显示。
2.2 基于Servlet搭建服务器
这一部分主要是重写父类HttpServlet的doPost, doGet, doDelete方法
(一)doGet
设计约定,selectALL和selectOne采用Get方法
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//考虑两方面 查看唯一图片属性 & 查看指定图片属性
// 通过URL是否带imageId参数进行区分
//存在imageId,则查看指定图片属性,否则就查看所有。
//例如URL/image?imageId=100,
//imageId的值就是100
String imageId=req.getParameter("imageId");
//URL中不存在imageId,则返回null
if (imageId==null||imageId.equals("")){
//查看所有图片属性
selectAll(req,resp);
}else {
//查看指定
selectOne(imageId,resp);
}
}
(二)selectAll和selectOne方法
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));
//2、使用gson把查到的数据转为json格式,并写回给响应对象
Gson gson=new GsonBuilder().create();
String jsonData=gson.toJson(image);
resp.getWriter().write(jsonData);
}
selectAll同理
(三)doPost方法
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1、获取图片的属性信息,并且存入数据库
//a)需要创建一个factory对象和upload对象,为获取图片属性所做的准备工作,是固定的逻辑
FileItemFactory factory=new DiskFileItemFactory();
ServletFileUpload upload=new ServletFileUpload(factory);
//b)通过upload对象进一步解析请求req
//FileItem代表一个上传的文件对象,理论上,HTTP支持一个请求中同时上传多个文件
//所以多个文件就用一个List表示
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\": \"请求解析失败\"}");//按照之前设计好的JSON格式 注意添加转义字符
return;
}
//c)提取FileItem中的属性,转化为Image对象,才能存到数据库中
//当前只考虑一张图片的情况 转化为Image对象
FileItem fileItem=items.get(0);//取第一张图片
Image image=new Image();
image.setImageName(fileItem.getName());
image.setSize((int)fileItem.getSize());
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");//获取当前时间
image.setUploadTime(simpleDateFormat.format(new Date()));
image.setContentType(fileItem.getContentType());
image.setPath("./image"+System.currentTimeMillis()+"_"+image.getImageName());
//文件在服务器上要存储的路径,自行构造(原本这里构造为image路径加文件名作为新路径)
//但发现当不同图片名字相同时,路径会被覆盖,所以在路径中加上毫秒级时间戳
image.setMd5("333");
//存到数据库中
ImageDao imageDao=new ImageDao();
imageDao.insert(image);
//2、获取图片的内容信息,并且写入磁盘文件
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}");
}
(四)doDelete方法
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json; charset=utf-8");
//1、指定id删除,获取请求中的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 (image==null){
//此时请求中传入的id在数据库中不存在
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}");
}
3.前端开发
前端开发三剑客:HTML 、CSS 和 JavaScript
HTML:网页的骨架
CSS:描述网页上组件的样式(位置,颜色,大小,字体,背景)
JavaScript:描述前端页面上的动作
前端不太好介绍呀,随便写吧,我想换个好看的背景的,没成功,后边再改改吧。
4.优化
4.1防盗优化
为防止恶意盗图,可以通过实现添加白名单方式,实现防盗。但是这个方法很容易被人攻击,通过构造相同的refer进行破解。
4.2磁盘优化
为了节省磁盘空间,当用户上传相同的图片,磁盘可以只存储一份。
通过计算得到的md5码是相同的。修改上传和删除部分的代码,上传时若检测到数据库中含有该md5码可以不上传磁盘,删除时若数据库中含有相同md5码的记录,先不删除磁盘,以免影响 其他图片的显示。