这个项目做的是实现一个类似于相册的功能,可以进行图片的上传、查看、删除功能;那它有哪些应用场景呢?最常见的比如在写csdn时,想插入一张图片,让你输入图片链接地址时,这时就可以把图片的url拿过来;
①、第三方库的使用,比如fileupload、Servlet、gson等 ②、JavaEE,JAVA和数据库还有tomcat的综合应用 ③、基于MD5进行校验 ⑤、HTTP协议的应用 ⑥、简单的前端实现HTML+CSS+JS ⑦、GSON数据格式 ⑧、软件测试的基本思想应用 ⑨、maven的使用 为什么选择maven开发?
1、maven一个命令就可以完成项目构建过程 2、他可以进行强大的依赖管理 3、可以分阶段进行构建 4、提供web项目的模式
实现一个HTTP服务器对图片进行增删查,分步讨论, 查:用户有两种需求,想查看所有的图片,那直接输入访问,如果想查看指定的某一张就需要提供图片的具体ID 增和删:一张图片的保存分为属性和图片具体内容,属性存在数据库里,图片的具体内容存在磁盘文件当中 一张图片的属性应该有哪些?id不用说,接下来肯定要有名称、图片尺寸、上传时间、图片的类型、图片的路径、MD5是为了去重引入的一个字段,下面会介绍 md5:计算校验和,在应用层对数据进行校验,把长字符串通过一定规则变成短字符串,来判断两张图片是否是一张图片
把它封装到一个类里,用的是单例模式,这个类只能创建一个实例,节省资源,但会出现线程不安全,解决办法是三步走(加锁、volatile、双重判断),最后记得关闭数据库连接,
public class DBUtil { private static final String URL = "jdbc:mysql://127.0.0.1:3306/java_image_server?characterEncoding=utf8&useSSL=true"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; private static volatile DataSource dataSource = null; 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步 ①获取数据库连接 ②创建并拼装SQL语句 ③执行sql语句 ④关闭数据库连接
public class ImageDao { /** * 把 image 对象插入到数据库中 * @param image */ public void insert(Image image) { // 1. 获取数据库连接 Connection connection = DBUtil.getConnection(); // 2. 创建并拼装 SQL 语句 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) { //不等于1说明没插进来 // 程序出现问题, 抛出一个异常 throw new JavaImageServerException("插入数据库出错!"); } } catch (SQLException | JavaImageServerException e) { e.printStackTrace(); //解决异常方法是打印栈 } finally { // 4. 关闭连接和statement对象 DBUtil.close(connection, statement, null); } } /** * 查找数据库中的所有图片的信息 * @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; try { // 3. 执行 SQL 语句 statement = connection.prepareStatement(sql); resultSet = statement.executeQuery();//把数据库响应的查询结果放在result对象中供使用 // 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; } /** * 根据 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; try { // 3. 执行 SQL 语句 statement = connection.prepareStatement(sql); statement.setInt(1, imageId); resultSet = statement.executeQuery(); // 4. 处理结果集 if (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")); return image; } } catch (SQLException e) { e.printStackTrace(); } finally { // 5. 关闭链接 DBUtil.close(connection, statement, resultSet); } return null; } /** * 根据 imageId 删除指定的图片 * @param imageId */ public boolean 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); } return false; } public static void main(String[] args) { //4. 测试删除图片 //ImageDao imageDao = new ImageDao(); //imageDao.delete(1); } 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; try { // 3. 执行 SQL 语句 statement = connection.prepareStatement(sql); statement.setString(1, md5); resultSet = statement.executeQuery(); // 4. 处理结果集 if (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")); return image; } } catch (SQLException e) { e.printStackTrace(); } finally { // 5. 关闭链接 DBUtil.close(connection, statement, resultSet); } return null; } }继承HttpServlet覆写里面的doget、dopost、dodelete方法来实现 post方法 获取到图片相关的元信息(Image对象), 并写入数据库 1、创建 factory 对象和 upload 对象 2、使用 upload 对象解析请求 3、对请求信息进行解析, 转换成 Image 对象 4、将 Image 对象写入数据库中 5、获取到图片内容, 写入到磁盘中 这里也引入servlet第三方库
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency>在这里插入代码片 public class ImageServlet extends HttpServlet { /** * 查看图片属性: 既能查看所有, 也能查看指定 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 考虑到查看所有图片属性和查看指定图片属性 // 通过是否 URL 中带有 imageId 参数来进行区分. // 存在 imageId 查看指定图片属性, 否则就查看所有图片属性 // 例如: URL /image?imageId=100 // imageId 的值就是 "100" // 如果 URL 中不存在 imageId 那么返回 null String imageId = req.getParameter("imageId");//获得客户端设置的数据 if (imageId == null || imageId.equals("")) { // 查看所有图片属性 selectAll(req, resp); } else { // 查看指定图片 selectOne(imageId, resp); } } 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);//发送请求内容至页面,直接在页面输出内容 } 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); } /** * 上传图片 * @param req * @param resp * @throws ServletException * @throws IOException */ @Override 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 对象进一步解析请求(解析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\": \"jiexi failed\" }"); return; } // c) 把 FileItem 中的属性提取出来, 转换成 Image 对象, 才能存到数据库中 // 当前只考虑一张图片的情况 FileItem fileItem = items.get(0);//FileItem类是一个接口,它的实现类是DiskFileItem Image image = new Image(); image.setImageName(fileItem.getName()); image.setSize((int)fileItem.getSize()); // 手动获取一下当前日期, 并转成格式化日期, yyMMdd => 20200218 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/" + 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\": \"Write disk failed\" }"); return; } } // 3. 给客户端返回一个结果数据 //resp.setContentType("application/json; charset=utf-8"); // resp.getWriter().write("{ \"ok\": true }"); resp.sendRedirect("index.html"); //http://192.168.3.24:8080/java_image_server/index.html } /** * 删除指定图片 * @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\": \"jiexi failed\" }"); 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 not exist in db\" }"); return; } // 3. 删除数据库中的记录 imageDao.delete(Integer.parseInt(imageId)); // 4. 删除本地磁盘文件 File file = new File(image.getPath()); file.delete(); resp.setStatus(200); resp.getWriter().write("{ \"ok\": true }"); }1、先要解析出Imageid 2、根据ID去数据库中查找。得到图片的属性 3、根据路径打开文件,以流的形式读取内容 4、把读到的数据写到响应对象中
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 Resolution failure\" }"); 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(); } }通过HTTP中的refer字段判定是否是指定网站请求图片,你想允许谁访问,就把链接地址加进来,
static private HashSet<String> whiteList = new HashSet<>(); static { whiteList.add("http://127.0.0.1:7070/java_image_server/index.html"); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { /*String referer = req.getHeader("Referer"); if (!whiteList.contains(referer)) { resp.setContentType("application/json; charset: utf-8"); resp.getWriter().write("{ \"ok\": false, \"reason\": \"wei shouquan de fangwen\" }"); return; }*/整体思路 1.修改上传图片代码, 使用 md5 作为文件名. 2.修改 DAO 层代码, 在 DAO 层实现一个 selectByMD5 方法, 根据 MD5 来查找数据库中的图片信息. 3.修改上传图片代码, 存储文件时先判定, 该 md5 对应的文件是否存在, 存在就不必写磁盘了. 4.修改删除图片代码, 先删除数据库记录, 删除完毕后, 看数据库中是否存在相同 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; try { // 3. 执行 SQL 语句 statement = connection.prepareStatement(sql); statement.setString(1, md5); resultSet = statement.executeQuery(); // 4. 处理结果集 if (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")); return image; } } catch (SQLException e) { e.printStackTrace(); } finally { // 5. 关闭链接 DBUtil.close(connection, statement, resultSet); } return null; }先要引入第三方依赖库Junit
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency>单元测试的代码
public class ImageDaoTest { @Test public void insert() { //图片有属性进行测试 Image image=new Image(); image.setImageName("test.jpg"); image.setSize(1024) ; image.setUploadTime("2020-07-01"); image.setContentType(" image/png") ; image.setPath("./image/d54c32d23621fb9ba448c71f1996c871"); image.setMd5("d54c32d23621fb9ba448c71f1996c871"); ImageDao imageDao=new ImageDao(); imageDao.insert(image); //图片为空测试 Image kong=new Image(); imageDao.insert(kong); } @Test public void selectAll() { ImageDao imageDao=new ImageDao(); ArrayList<Image> lists= (ArrayList<Image>) imageDao.selectAll(); for(Image image:lists){ System.out.println(image.getImageName()); } } @Test public void selectOne() { ImageDao imageDao=new ImageDao(); //id存在 Image image1=imageDao.selectOne(113); System.out.println(image1.getImageName()); //id不存在(报错--空指针异常) //Image image2=imageDao.selectOne(1); //System.out.println(image2.getImageName()); } @Test public void delete() { ImageDao imageDao=new ImageDao(); //id存在 imageDao.delete(24); //删除不存在的id(报错) //imageDao.delete(1); } @Test public void selectByMd5() { ImageDao imageDao=new ImageDao(); //md5存在 Image image=imageDao.selectByMd5("e27dd4d4ad59d838e9f8ee0b5e382cfb"); //Image image2=imageDao.selectByMd5("4c32d23621fb9ba448c71f1996c871"); //md5不存在(报错) System.out.println(image.getImageName()+image.getImageId()); // System.out.println(image2.getImageName()+image2.getImageId()); } }布局 1、图片比较多时,每一行最多五张图片,剩下的折行展示 2、每张图篇的大小一-致,都是200px* 200px 3、每一张图片都在一个div框里面,图片左下角有图片名称,删除按钮 4、 每一页最上面是logo,标题,接下来右边是图片上传按钮,最后一部分展示的是已经上传到服务器上的图片。 文字部分 1、字体的大小、字体的粗细、是否斜体展示、展示的位置 2、字体是否成功展示、页面上图片文件的名称是否按照设置的大小,字体形式展示 图片部分 1、图片是否完全展示,即上传的图片和展示的图片内容致 2、每一行的图片个数相等,展示风格-一致 3、点击图片本身可以放大图片 4、图片的放大.点击图片右边的放大按钮,也可以放大图片 5、可以连续查看放大图片 6、页面最多展示多少张图片
上传功能 1、点击选择上传按钮,可以出现电脑中资源选择的页面 2、点击选择上传按钮,可以出现电脑中资源选择的页面,选中一张图片,点击打开按钮,方框中"未选择任何页面”–>"图片名称” 3、点击上传按钮,页面会刷新,展示出刚刚上传的图片。存放图片的位置会有新的文件出现。数据库中会插入一条新的数据 4、上传图片格式为JPG,PNG格式的图片均可以上传成功 5、不选择任何文件,直接点击上传,应该有提示”请您选择一张图片”, 6、上传整个文件夹,无法上传,只会打开该文件夹,无法上传文件夹 7、不允许一次性上传多个文件(无法选中多个图片文件 8、上传文件大小大于磁盘空间剩余大小,提示"磁盘空间不足,上传 失败! 9、磁盘空间剩余0上传文件,提示”磁盘空间不足,上传 失败! 10、上传除 了图片以外的其他类型的图片,上传失败,比如doc、ppt、安装程序。 11、无法进行批量上传图片 12、上传不存在的图片 13、上传 一个图片名称和数据库中图片名称相同、内容不同 14、上传 一个图片名称和数据库中图片名称相同、内容不同图片名称相同,图片内容不同 删除功能 1、选择某一张图片下面的删除功能,点击,会出现提示删除弹框,点击确定,图片成功从页面上移除· 2、不能进行批量删除.(有删除全部图片按钮) 3、删除某一-张图片之后,页面整体图片为会发生重新排版 4、删除图片后,在服务器存放图片的路径下,该图片是否消失 5、删除图片后数据库中存放该图片的数据也相应被删除 6、删除相同名称的其中一个图片,不会对其它名称相同的图片产生影响。 7、删除名称相同,图片内容也相同的图片中-一张图片,对和该图片相同的图片不会产生影响 异常情况 1、数据库服务未启动,上传文件失败 2、弱网(不同网络的情况下) . 图片加载、上传和展示功能 3、加载(展示)已经损坏的文件,web前端页面不再展示,比如文件名称被篡改或者文件本身损坏 4、在上传的过程中,停止tomcat,查看是否上传成功
从点击按钮到提交在三秒以内,符合358原则
1、各种浏览器访问均可正常操作(搜狗、谷歌、IE、火狐、360) 2、相同浏览器的不同版本 3、不同的操作系统、苹果、linux、windows7 10 xp
1、防止sql注入 2、上传带有病毒的文件 3、超过图片超过最大限值时服务器会不会崩溃
1、上传图片时,只要选中图片即可图片输入框,即可上传图片,不一定非是按钮 2、页面功能按钮设计的直观易用