项目背景
图片服务器本质是一个服务器,主要基于Servlet技术开发编程。
主要核心功能有:
- 上传图片
- 展示图片
- 删除图片
等功能
图片服务器又被称为图床,主要应用于许多网站环境,例如:博客编写中需要插入图片,网页设计嵌入图片等。
图床设计本质上是往文本中放了一个URL链接,而真正的资源在另外一个服务器上面。
通过URL链接可以连接到图片资源,而为了保护图片资源,我们还加入了一个防盗链的功能。
项目所涉及知识点
- web服务器设计开发(Servlet)
web服务器主要指HTTP服务器,传统简单的HTTP服务使用Sockte编程,这样复杂麻烦,实际上我们有完整的HTTP服务器直接去使用,在这里我们主要使用的是Tomcat。
Tomcat为我们提供了一个开发框架,其中Tomcat中就为我们提供了一个Servlet编程接口,可以为我们实现服务器编程。
通过Servlet编程,我们可自己开发出来一套编程逻辑,部署在Tomcat上,实现一个服务器的搭建,避免了我们再去设计编程底层的Sockte。
- 使用数据库技术(MySQL)
在这里我们使用的是JDBC技术编程MySQL,实现对数据库的创建、存储、删除。
- 数据库设计
根据实际应用的场景设计数据库表结构。
- 前后端交互的API设计
基于HTTP协议设计。
- 使用JSON数据格式
主要使用Java中为我们提供的GSON库操作JSON数据
- 测试HTTP服务器
学习使用Postman测试工具
- 使用HTML、CSS、JavaScript技术设计构建一个简单的网页
服务器设计
在真正开发web项目的时候,编写代码之前都需要做两件非常重要的设计
(1)数据库设计
(2)前后端交互接口设计
数据库设计
考虑在本项目中需要什么样的数据库?需要什么样的表结构?
- 登录数据库
mysql -h rm-m5eb14wun22842aq3.mysql.rds.aliyuncs.com -u root -p
在这里我使用了阿里云的外置MySQL数据库,可以独立存储、独立运行,不占用当前ESC云服务资源,更好的提高效率。
-h的意思就是以以下链接连接到数据库,-u是用户名,-p是使用密码
其实MySQL本质也是一个服务器,安装的数据库本体就是服务器,我们的命令行程序就是客户端。
因此就像我可以访问这个外置的服务器,其他客户端可以连接到数据库,这样我们就可以查看到储存的内容。
扩展资料:
我们在连接MySQL时,显示welcome to the MariaDB
那么MariaDB是什么呢?
在数据库发展前期,最厉害的是Oracle,市场占有率最高,但是十分昂贵,是搭配OpenPOWER的小型机服务器使用(高计算力、能力强大,应用于金融行业),之后出现了MySQL,MySQL是免费的,越来越多的人对它改进,生态越来越完全,抢占了Oracle的市场,随后MySQL被收购,于是开发者在被收购之前,备份了源代码,公布在了社区,继续被维护发展,这个就是MariaDB,随着时代发展,这两者也已经不再一样。
- 数据库的结构设计
创建数据库
create database java_image_server;
选择库
use java_image_server;
创建表
在这里我们就需要考虑我们的项目需要什么样的表结构才能实现我们的图片储存功能了
create table image_table(
imageID int not null primary key auto_increment,//对每个图片设置不同的ID方便使用,not null意思是非空约束,约束强制字段始终包含值,这意味着如果不向字段添加值,就无法插入新纪录或者更新新纪录,primary key auto_increment意思是自增主键,通常希望在每次插入数据时,数据库自动生成字段的值,主键必须包含唯一的值且不可为空。
imageName varchar(50),//代表图片的名称,每个存进数据库的图片都有其自带的名称
size int,//代表图片大小,这是图片的属性信息,是研发过程必不可少的部分
upLoadTime varchar(50),//代表图片上传时间,MySQL提供了专用的数据类型来表示时间,我们主要目的在后端编程,不在数据库,暂时使用简单的类型实现
contentType varchar(50),//HTTP协议中描述正文类型的数据
path varchar(1024),//图片储存的路径,数据库表存储的是图片属性信息,图片内容是存储在服务器的某个磁盘位置上的
md5 varchar(1024));//校验和
contentType:
例如 text/html 这一传输中正文类型是 html 格式的,image/jpg 说明这一正文数据是 jpg 图片格式的等等,
这是服务器传输必不可少的部分,是对数据正确解码的不可缺少的信息。
path:
数据库中存储的图片是图片的属性(元信息),图片正文是以文件的形式存储在磁盘上的。
数据库表中就需要记录一个 path 对应到磁盘上的文件位置。
当然也可以将正文直接存储在数据库表中,这样做存储规格有限,而且低效,不适合我们当前的项目。
校验和:
是在TCP/IP传输消息中验证数据是否正确的字段信息,在IP报头、UDP协议等都有使用,传输层、网络层、应用层都需要其校验信息。
其原理是,用一个更短的字符串来验证整体数据是否正确,这个短字符串是根据原串内容通过一定的规则来计算出来的。
TCP、UDP中常使用的是CRC算法实现的(循环冗余算法,把数据内容循环相加,溢出也继续相加所得的一种校验和方式)
md5 算法是一种字符串哈希算法,是一种散列算法,类似于哈希表,存储数据时希望存储的是一个键值对,其中key不仅仅是一个int,可以是多种类型,hash算法可以将这多种类型的值经过一系列的数学变化得到一个整数,让这个整数对应到数组下标。
服务器API(前后端交互接口)设计
涉及知识点
- maven
是JAVA开发提供的一种构造机制,能够帮助我们编译代码,同时能够帮我们自动安装一些第三方jar包,当我们需要什么依赖时可以对pom.xml编程下载对应依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bit</groupId>
<artifactId>java_image_server</artifactId>
<version>1.0-SNAPSHOT</version>
<!--打包方式是 war 包,一种用于 web 应用的包,原理类似 jar 包 -->
<packaging>war</packaging>
<!-- 指定属性信息 -->
<properties>
<encoding>UTF-8</encoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- 加入 serverlet 依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<!-- servlet 版本和 tomcat 版本有对应关系,切记 -->
<version>3.1.0</version>
<!-- 这个意思是我们只在开发阶段需要这个依赖,部署到 tomcat 上时就不需要了 -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<!--JSON库,gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
</dependencies>
<build>
<!-- 指定最终 war 包的名称 -->
<finalName>java_image_server</finalName>
<!-- 明确指定一些插件的版本,以免受到 maven 版本的影响 -->
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
- JSON数据格式
是一种数据组织格式,是一种键值对结构。
通过大括号包含,引号包含一个值,冒号分割,逗号分离键值对,最后一个键值对的逗号可有可无。
例如:
JSON只是一个数据格式与编程语言无关,出身于JavaScript,但可以应用与多个语言之中。
在这里我们将要使用到JSON数据格式,完成数据的序列化,方便进行网络传输,使用GSON 库(Google开发)实现对JSON数据的操作
简单编写熟悉一下JSON:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.HashMap;
class Hero{
public String name;
public String skill1;
public String skill2;
public String skill3;
public String skill4;
}
public class TestGson {
public static void main(String[] args) {
// /**
// * Json支持Hashmap键值对的类型
// */
// HashMap<String,Object> hashMap = new HashMap<>();
// hashMap.put("name","曹操");
// hashMap.put("skill1","剑气");
// hashMap.put("skill2","三段跳");
// hashMap.put("skill3","加攻击吸血");
// hashMap.put("skill4","加攻速");
/**
* Json也支持类的创建转化
*/
Hero hero = new Hero();
hero.name = "曹操";
hero.skill1 = "剑气";
hero.skill2 = "三段跳";
hero.skill3 = "加攻击吸血";
hero.skill4 = "加攻速";
//通过map转成JSON 结构的字符串
//1.创建一个GSON对象
Gson gson = new GsonBuilder().create();
//2.使用toJson方法把键值对结构转成Json字符串
//String str = gson.toJson(hashMap);
String str = gson.toJson(hero);
System.out.println(str);
}
}
在JSON生成的数据中对顺序是没有要求的,
两者完全等价。
- 文件上传操作如何在HTML中完成
文件上传在HTTP协议中进行的方式:
HTML中编写含有许多标签,例如:<div> <img> <a>
在这里我们用到 form表单
<html>
<head>
</head>
<body>
<form id="upload-form" action="upload.php" method="post" enctype="multipart/form-data" >
<input type="file" id="upload" name="upload" /> <br />
<input type="submit" value="Upload" />
</form>
</body>
</html>
- 存在问题:
存在乱码说明在HTML编写中使用了未知字符,也有可能是不同格式的空格,删去即可。
为了能够更清楚地看到我们上传操作的具体细节,我们是使用专用的HTTP抓包工具 Fiddler 直接抓取HTML代码看看都发生了什么:
我们使用Fiddler 得到了一个完整的HTML上传过程协议格式
404错误原因是服务器未响应,还没写,当然没响应啦!
解析
正式设计接口功能(约定HTTP协议格式)
客户端上传图片构造如这样一个的请求
服务端也要按照这个格式来解析
- 新增图片功能
请求格式:
(方法)POST (路径)/image
Content-Type: multipart/form-data;
[正文内容]
(包含图片自身的一些信息)
------WebKitFormBoundary6QofcWvj8HaumOp9
Content-Disposition: form-data; name=“upload”; filename=“01.jpg”
Content-Type: image/jpeg
[图片正文的二进制内容]
响应格式:
上传成功:
HTTP/1.1 200 OK
(JSON字段)
{
“ok”:true,
}
上传失败:
HTTP/1.1 200 OK
{
“ok”:false,
“reason”:“具体失败原因”
}
- 查看所有图片属性功能(属性信息不是图片内容)
根据请求方法的不同,就可以区分所采取的功能是什么。
请求格式:
(方法)GET (路径)/image
(请求查看属性,没有上传,就没有body)
响应格式:
访问成功:
HTTP/1.1 200 OK
[ (查看所有图片属性,包含多个元素,使用JSON格式中的数组形式,JSON的数组形式用 [] 表示)
{
imagID:1,
imgeName:“1.png”,
contetType:“image/png”,
size:1000,
upLoadTime:20201015,
md5:“112233545”,
path:"./data/1.png"
},
{
(同上)
},
{
(同上)
},
]
访问失败:
HTTP/1.1 200 OK
[ ]
疑问:为什么响应要用200,其他可以吗?
API设计失败与成功的规定有很多方式,我们可以用200表示成功,404表示失败,也可以使用body中的 ok 字段 true 表示成功,false 表示失败,也可以使用 [] 有内容表示成功,为空表示失败。
- 查看指定图片属性功能
请求格式:
GET /image?imageID=[具体数值]
响应格式:
访问成功:
HTTP/1.1 200 OK
{
imagID:1,
imgeName:“1.png”,
contetType:“image/png”,
size:1000,
upLoadTime:20201015,
md5:“112233545”,
path:"./data/1.png"
}
访问失败:
HTTP/1.1 200 OK
{
ok:false,
reason:“具体的出错原因”
}
- 删除指定图片功能
服务器实现代码的时候就可以判定方法,如果是DELET方法就执行删除操作。
请求格式:
DELET /image?imageID=[具体图片ID]
响应格式:
访问成功:
HTTP/1.1 200 OK
{
ok:true,
}
访问失败:
HTTP/1.1 200 OK
{
ok:false,
reason:“具体出错原因”
}
删除等方法不一定就要使用DELET这样区分,我们还可以例如:GET /image?imageID=XXX&delete=1 ,这样专门设置一个参数来代表删除也可以,这就是要进行HTTP接口设计约定的原因。
- 查看指定图片内容
请求格式:
GET /imageShow?imageID=[具体的图片ID]
响应格式:
访问成功:
HTTP/1.1 200 OK
Content-Type:image/png
[具体的图片二进制内容]
访问失败:
HTTP/1.1 200 OK
{
ok:false,
reason:“具体出错原因”
}
编写源代码开发
数据库操作(dao层)
dao包 :数据访问层,这里面的类都是围绕数据操作展开的。
先创建 DBUtil 类:封装获取数据库连接的过程
package dao;//数据访问层
import com.mysql.jdbc.Connection;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DBUtil {
private static final String URL = "jdbc:mysql://rm-m5eb14wun22842aq3.mysql.rds.aliyuncs.com:3306/java_image_sever?characterEncoding=utf8&useSSL=true";
private static final String USERNAME = "root";
private static final String PASSWORD = "JAVAxuexi4";
//加上volatile关键字保证每次线程访问都是更新的
private static volatile DataSource dataSource = null;
//连接url
public static DataSource getDataSource(){
//通过这个方法来创建 DataSource 的实例
//单例模式,这样是线程不安全的,线程二容易发生抢占,多次创建dataSource
// if (dataSource == null){
// dataSource = new MysqlDataSource();
// }
// return dataSource;
//线程安全方案:1)加锁
//仅仅只是加锁,是低效的,每次调用方法都会屏障,因此需要再加一层判断
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 java.sql.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();
}
}
}
关于连接数据库的URL:
关于设计模式:
在这里我们使用的是单例模式,关于单例模式又可以分为饿汉模式和懒汉模式。
饿汉模式就是构造方法是直接返回对象的,也被称为立即加载
其原理是在类加载时会将进行加载,等到调用时该类已经被处理好了所以能保证多线程调用下,调用的是同一个实例。
因此,饿汉模式是线程安全的。
缺点:1.程序启动前,需要创建类对象,会导致程序启动较慢。
2.如果有多个单例类对象实例启动顺序不确定。
private static DataSource dataSource = new MysqlDataSource();
public static DataSource getDataSource(){
return dataSource;
}
懒汉模式加入了一个 if 判断,也被称为延迟加载
就是在调用get()方法的时候实例才被创建。
private static DataSource dataSource = null;
public static DataSource getDataSource(){
if (dataSource == null){
dataSource = new MysqlDataSource();
}
return dataSource;
}
但仅仅使用懒汉模式是不能保证线程安全的,例如
这样就会导致多次创建DataSource这个对象,就不能保证这个类是单例模式,所谓单例模式就是应该保证创建和实现始终都是单个实例的。
因此要实现线程安全的单例模式,应该给线程加锁,然而仅仅加锁,每次调用方法都会再加锁一次,是低效的,因此需要再加一层判断。
另外为了保证对象的数值不会因为多线程的抢占调用出错,应该保证每次创建都会更新,因此加上volatile关键字。
关于关闭连接
每次使用完数据库连接是需要手动关闭的,因此需要编写关闭方法。
对于关闭方法,每次连接打开了许多不同的接口,这些接口都是有顺序的
如果关闭顺序出错,就会出现连接异常,因此要注意关闭的顺序一定要遵循:先打开的后关闭,后打开的先关闭
实体类 Image
对应一个图片的对象(包含图片的相关属性)。
创建image对象,一个对象对应一个图片。
package dao;
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 + '\'' +
'}';
}
}
关于属性
代码中private所私有化的那些都是图片的属性,方便图片信息在JDBC的传输中使用,它与数据库中所创建的表结构所包含内容是一致的。
这里属于Java的封装特性,将属性私有化,只通过setter、getter 等构造方法对其进行值的操作,如果外面的程序可以随意修改一个类的成员变量,就不会造成不可预料的程序错误。
关于setter、getter和toString方法
IDEA中提供了十分方便的编程插件帮助我们快速编程实现,alt+enter快捷键会弹出一个 Generate 选择想要实现的方法,它都会快速编辑出来。
ImageDao 对象管理器
借助这个类完成对Image对象的增删改查操作。
在这里实现对数据库操作的功能,这里也将构成实现我们的服务器的主要功能。
package dao;
import common.JavaImagerServerException;
import org.omg.PortableInterceptor.ServerRequestInfo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class ImageDao {
/**
* 把image对象插入到数据库中
*/
public void insert(Image image){
//1. 获取数据库连接
Connection connection = DBUtil.getConnection();
//2. 创建并拼装 SQL 语句(使用PrepareStatement拼装)
String sql = "insert into image_table values(null,?,?,?,?,?,?)";//当插入一条图片信息时按照已经建好的数据库表首位都是自增主键,是不可操作的,设置为null。
PreparedStatement statement = null;
try {
statement = connection.prepareStatement(sql);//受查异常
statement.setString(1,image.getImageName());//PrepareStatement对象提供了许多对数据库语言的操作,这里set系列的方法可以对指定索引的问号,替换成其他元素,注意这里的索引是从1开始的。
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 JavaImagerServerException("插入数据库出错!");
}
} catch (SQLException | JavaImagerServerException 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";
//3. 执行 SQL 语句
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();//查找数据库
//4. 处理结果集
while (resultSet.next()){//判断当前有没有得到结果集
Image image = new Image();
image.setImageID(resultSet.getInt("imageID"));//将获取到的imageID插入到image对象中
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"));//将获取到的imageID插入到image对象中
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 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 JavaImagerServerException("删除数据库操作失败");
}
} catch (SQLException | JavaImagerServerException e) {
e.printStackTrace();
}finally {
//4. 关闭连接
DBUtil.close(connection,statement,null);
}
}
/**
* 用于简单测试
* @param args
*/
public static void main(String[] args) {
//1.测试插入数据
//数据库连接使用的是127.0.0.1时会出现报错:
//由于数据库是外置在云服务器上的,不在本地,这个程序直接在本地运行是无法访问数据库的
//此时应该部署在云服务器上才能看到效果
//具体步骤:打一个jar包,把jar包部署在云服务器上,就可以了
// Image image = new Image();
// image.setImageName("1.png");
// image.setSize(100);
// image.setUpLoadTime("20201018");
// image.setContentType("image/png");
// image.setPath("./data/1.png");
// image.setMd5("11223344");
// ImageDao imageDao = new ImageDao();
// imageDao.insert(image);
//2. 测试查找所有图片信息
// ImageDao imageDao = new ImageDao();
// List<Image> images = imageDao.selectAll();
// System.out.println(images);
//3. 测试查找指定图片的信息
// ImageDao imageDao = new ImageDao();
// Image image = imageDao.selectOne(1);
// System.out.println(image);
//4. 测试删除指定图片信息
// ImageDao imageDao = new ImageDao();
// imageDao.delete(1);
}
}
关于selectAll方法
这个方法的目的是实现查找出所有数据库表中的数据,如果数据库中内容只有几千条,还是能够实现的;如果出现上亿条数据,这样直接调出所有数据会非常低效,这里是本项目一个缺点。
关于delete方法
这里我使用的是根据imageID来单个操作删除,当然也可以直接对整个库删除,但这样操作风险太大,如果有误操作的话,会造成数据的无法恢复,我的项目上不提倡使用,工作工程上更应该谨慎设计。
关于所有方法的实现
基本上JDBC的编写步骤都是四步:
- 获取数据库连接
- 创建并拼装SQL语句
- 执行SQL语句
- 关闭所有连接
如果有需要返回值的中间需要加入处理结果集。
关于异常处理
当我们对数据库操作时,使用的是PreparedStatement进行的报文交换,可以有效防止SQL注入,但SQL操作难免会有异常出现,所以需要我们处理这些异常。
一般这一类异常属于受查异常,
受检查异常是你必须要捕获的异常,否则无法通过编译, 而非受检查异常你可以捕获或者不捕获.
非受检查的异常,只能打印日志,而做不了其他的事情.
这里不论是受查异常和非受查异常都仅仅是打印调用栈或是打印日志,但我们还有其他的异常处理方式。
关于关闭连接
我们在关闭连接时要注意,有异常处理的方法中,程序会有两个走向,正常运行和异常报错。
如果我们将关闭连接写在try内的话,是不能保证程序一定会关闭连接的。所以我们需要使用finally关键字,不论发生什么都必须要运行关闭连接。
这里会出现一个错误,那就是不能关闭statement等类,是因为要把对象的声明放在try的外部,保证对象的生命周期在整个方法内。
关于测试
我们再进行测试功能是否正确完成时,发现是无法在IDEA上直接debug的,因为需要连接数据库,而IDEA使用的是外网,直接把数据库服务器暴露在外网环境下是致命的,所以一般只使用内网链接,哪怕是使用阿里云提供的专用数据库,也是通过控制台内网连接在专门的ECS服务器上的。
这里我们需要将程序打jar包,部署在服务器上运行测试JDBC部分的功能。
关于jar包
jar包就是类似于zip 这样的压缩包,把程序编译好一堆class文件放在一起打包,可以方便上传到其他环境下直接就可以使用。
关于部署
可以直接将jar包通过云服务器中的 lrzsz 直接上传到云服务器,同时IDEA社区中也提供了阿里云的插件,通过插件可以直接从IDEA 上传到云服务器。
关于为什么使用datasource不用JDBCdriver
两种方式都是可以的,并没有什么优劣,我优先使用datasource是因为,它内建了一个连接池,每次建立连接对象时,就可以直接获取已有的连接相比JDBCdriver高效一点。
基于Servlet 来搭建服务器(API层)
Servlet 相关代码执行方式和平时写的不太一样,平时代码是从main方法运行的。
Servlet 中是没有main方法的,Servlet只是Tomcat的一个接口,运行是通过Tomcat调用实现的。
Tomcat的工作原理是和HTTP服务器是十分相似的。
- 启动时要绑定端口号8080
- 进入一个主循环,在这个主循环里,调用accept,获取当前的请求的链接。
- accep是一个阻塞方法,直到接收到客户端发送的请求才能继续运行。
- 读取客户端发送的数据(字符串)
- 把这个字符串数据按照HTTP协议来进行解析
- 解析出的HTTP请求的方法和URL之后,找到对应的Servlet并执行对应的doXXX方法
1)Tomcat根据URL查找映射关系表,找到api.ImageServlet 类
2)Tomcat根据GET 方法决定给 api.ImageServlet创建一个对象,并且调用其中的 doGet 方法
3)执行doGet方法,向resp对象中写了一个 hello!
4)Tomcat构造resp对象,根据这个对象生成HTTP响应报文,再通过socket写回给浏览器
安装Servlet
maven依赖:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<!-- servlet 版本和 tomcat 版本有对应关系,切记 -->
<version>3.1.0</version>
<!-- 这个意思是我们只在开发阶段需要这个依赖,部署到 tomcat 上时就不需要了 -->
<scope>provided</scope>
</dependency>
创建子类ImageServlet继承HttpServlet父类
为了测试Servlet的功能,首先复写一个doGet类实现一个字符串的上传显示。
package api;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ImageServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//req 对象中包含了请求的所有信息
//resp 对象是把要生成的结果放到里面去
//当前继承的方法就是要根据请求,生成响应
//状态码
resp.setStatus(200);
//将hello这个字符串写入http响应的body当中
resp.getWriter().write("hello!");
}
}
关于GET、POST等方法
HttpServlet包中提供的doXXX系列的方法,其实和HTTP协议的方法是一一对应的。
如果服务器收到GET请求,通过Tomcat解析,就会自动调用Servlet的doGet方法。
同理,其他方法也是如此。
这样就一一实现了我们前面的设计部分的响应和请求部分。
关于多态
Java开发的核心任务就是管理软件的复杂程度,而Java的面向对象的3大特征就是在减少代码编写中的复杂程度。
Java的封装机制帮助我们解决了让代码不仅仅混乱在一个主类中,而是分开使用,并且还让使用者不再关注其中的细节是什么。
而多态实现了接口的多类型化,让使用时更进一步,不再关注对象的类型和具体实现。
多态是大多数框架的主要实现方式。
关于接口参数
当我们使用了doGet方法时,会调用到两个重要的参数:
HttpServletRequest req 请求
HttpServletResponse resp 响应
req就可以包含入相关的Http协议的请求信息,resp就包含了相关的响应信息。
当测试想要在页面显示一个字符串时,此时我们是服务器编程,那么就把自己看作服务器,浏览器也就是客户端给我们了一个请求,我们给予一个响应才能在浏览器页面上显示,因此,在这里应该修改resp参数才能实现我们想要测试的功能。
最终实现:
package api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dao.Image;
import dao.ImageDao;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class ImageServlet extends HttpServlet {
/**
* 实现插入图片功能
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//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.setStatus(200);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": false,\"reason\": \"请求解析失败!\"}");//JSON数据格式
return;
}
//3)把 FileItem 对象中的属性提取出来,转换成Image对象,才能存储到数据库中
// 当前只考虑一张图片的情况
FileItem fileItem = items.get(0);
Image image = new Image();
image.setImageName(fileItem.getName());
image.setSize((int)fileItem.getSize());
//手动获取一下当前日期,并转换成格式化日期,yy-MM-dd 对应 2020-10-20
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.setMd5("11111111");//暂时先不计算
//存到数据库当中
ImageDao imageDao = new ImageDao();
imageDao.insert(image);
//2. 获取图片的内容信息,并且写入磁盘文件
File file = new File(image.getPath());
try {
fileItem.write(file);
} catch (Exception e) {
e.printStackTrace();
resp.setStatus(200);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": false,\"reason\": \"写入磁盘失败!\"}");
return;
}
//3. 给客户端返回一个结果数据
resp.setStatus(200);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": true }");
}
/**
* 实现查看所有和查看指定图片的属性功能
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 考虑到查看所有图片属性和查看指定图片属性
// 通过查看 URL 中 是否带有 imageID 的参数区分两种方法
// 存在 imageID 查看指定图片属性,否则就查看所有图片属性
// 如果 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 {
//1. 创建一个 ImageDao 对象,并查找数据库
ImageDao imageDao = new ImageDao();
List<Image> images = imageDao.selectAll();
//2. 把查找到的结果转成 JSON 格式字符串,并且写入 resp 对象
if (images == null){
//此时请求在数据库中查找不到数据
resp.setStatus(200);
resp.getWriter().write("[ ]");
return;
}
Gson gson = new GsonBuilder().create();
// jsonData字符串就和我们设计时约定的是一样的
// 只要把之前的相关字段都统一约定成一样的命名格式,下面的操作就可以一步到位的完成转换
String jsonData = gson.toJson(images);
resp.setContentType("application/json; charset=utf-8");
resp.setStatus(200);
resp.getWriter().write(jsonData);
}
private void selectOne(String imageID, HttpServletResponse resp) throws IOException {
ImageDao imageDao = new ImageDao();
Image image = imageDao.selectOne(Integer.parseInt(imageID));
resp.setContentType("application/json; charset=utf-8");
if (image == null){
//此时请求中传入的 id 在数据库中不存在
resp.setStatus(200);
resp.getWriter().write("{ \"ok\": false,\"reason\": \"imageID在数据库中不存在!\"}");
return;
}
Gson gson = new GsonBuilder().create();
String jsonData = gson.toJson(image);
resp.setContentType("application/json; charset=utf-8");
resp.setStatus(200);
resp.getWriter().write(jsonData);
}
/**
* 实现删除指定图片功能
* @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 (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 }");
}
}
关于汉字显示都是问号
汉字是按照一定方式进行编码的。汉字编码格式一般有两种:
- UTF-8
这是 IDEA 创建文件的默认格式,也是Java编程中常使用的一种编码格式 - GBK
浏览器按使用操作系统的默认编码格式,Windows系统默认使用的是GBK编码格式
因此当我们在使用GET、POST方法时,应注意处理编码集。
关于上传图片用到的第三方库 fileupload
要实现对上传图片内容的读写操作,需要用到一个第三方库 fileupload,因此需要加载maven依赖:
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
req中包含了客户端对服务器发送的请求报文,为了提取这些图片属性和解析图片内容,我们使用第三方库 fileupload实现。
fileupload返回的是一个List 类型的对象,使用List链表是因为HTTP支持一个请求上传多个文件,可以解析出多个文件。
public List<FileItem> parseRequest(HttpServletRequest request) throws FileUploadException {
return this.parseRequest(new ServletRequestContext(request));
}
每个链表结构中存储的FileItem对象对应一个上传文件,我们对FileItem对象取出属性信息就可以存在我们对应的Image对象中。
同时,使用File操作准备文件路径,就可以使用write()方法将FileItem对象中的内容存储在对应的文件路径位置。
创建ImageShow子类以区分查看属性和查看内容功能
package api;
import dao.Image;
import dao.ImageDao;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
public class ImageShow extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1. 解析出 imageID
String imageID = req.getParameter("imageID");
if (imageID == null || imageID.equals("")){
resp.setStatus(200);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": false,\"reason\": \"imageID 解析失败!\" }");
}
//2. 根据 imageID 查找数据库,得到对应的图片属性信息(需要知道图片的存储路径path)
ImageDao imageDao = new ImageDao();
Image image = imageDao.selectOne(Integer.parseInt(imageID));
if (image == null){
//此时请求中传入的 id 在数据库中不存在
resp.setStatus(200);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": false,\"reason\": \"imageID在数据库中不存在!\"}");
return;
}
//3. 根据路径打开文件,读取其中的内容,写入到响应对象中
resp.setContentType(image.getContentType());
File file = new File(image.getPath());
// 由于图片是二进制文件,我们应该使用字节流的方式来读取文件
FileInputStream fileInputStream = new FileInputStream(file);
OutputStream outputStream = resp.getOutputStream();
byte[] bytes = new byte[1024];
while (true){
int len = fileInputStream.read(bytes);
if (len == -1){
//文件读取结束
break;
}
// 此时已经读到一部分数据,放在 bytes 里,要把bytes中的内容写到响应对象中
outputStream.write(bytes);
}
fileInputStream.close();
outputStream.close();
}
}
配置web_app中xml文件
仅仅实现了代码还是无法运行的,服务器就像一个计算机,仅仅安装了软件不告诉运行路径也是运行不了的。因此就像所有软件要在计算机上注册一样,我们也需要写一个注册文件,告诉Tomcat去哪里寻找和运作。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
<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>
<servlet>
<servlet-name>ImageShow</servlet-name>
<servlet-class>api.ImageShow</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ImageShow</servlet-name>
<url-pattern>/imageShow</url-pattern>
</servlet-mapping>
</web-app>
关于其中标签的功能
配置这个xml文件中主要包含了两个部分的标签:
<servlet>
标签主要告诉Tomcat当前这个 servlet对应到代码中的哪个类。
<servlet-mapping>
标签主要告诉Tomcat当前servlet对应到URL的path是什么。
一个典型URL的成份:
这个配置文件的目的是为了让Tomcat明白,收到的请求中访问的某个路径对应的Servlet类对应是哪个,进一步才能执行代码。
后端服务器整个完成实现后,我们就可以通过浏览器上传图片,上传后的图片在云服务器上时,就可以在写博客等情境下,使用URL,直接获取到图片了。
实现前端页面(展示图片,增删操作)(了解即可)
实现前端的主要应用技术
HTML: 构成网页的骨架,决定一个页面的内容和其嵌套结构
CSS: 描述网页上组件的样式(位置,颜色,大小,字体,背景…)
JavaScript: JavaScript和Java没有任何关系,不过是借助Java的名气起的名字罢了,主要用来描述前端页面上的动作,和用户具体的交互行为。
HTML、CSS、JavaScript都可以写在同一个HTML文件中,也可以分开写,当浏览器加载这个HTML时,就会运行这些代码。
Java运行在JVM虚拟机上,必须要云服务器的平台辅助,JavaScript等都是运行在浏览器上的。
网页提供了许多可用的模板,在这里我使用了现成的前端模板。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="">
<meta name="keywords" content="">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>图片服务器展示</title>
<meta name="renderer" content="webkit">
<meta http-equiv="Cache-Control" content="no-siteapp"/>
<link rel="icon" type="image/png" href="assets/i/favicon.png">
<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" sizes="192x192" href="assets/i/app-icon72x72@2x.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Amaze UI"/>
<link rel="apple-touch-icon-precomposed" href="assets/i/app-icon72x72@2x.png">
<meta name="msapplication-TileImage" content="assets/i/app-icon72x72@2x.png">
<meta name="msapplication-TileColor" content="#0e90d2">
<link rel="stylesheet" href="assets/css/amazeui.min.css">
<link rel="stylesheet" href="assets/css/app.css">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body id="blog-article-sidebar">
<!-- header start -->
<header class="am-g am-g-fixed blog-fixed blog-text-center blog-header">
<div class="am-u-sm-8 am-u-sm-centered">
<img width="200" src="http://47.104.62.86:8080/java_image_server/imageShow?imageID=7" alt="展示Logo"/>
<h2 class="am-hide-sm-only">图片服务器项目展示</h2>
</div>
</header>
<!-- header end -->
<hr>
<!-- nav start -->
<nav class="am-g am-g-fixed blog-fixed blog-nav">
<button class="am-topbar-btn am-topbar-toggle am-btn am-btn-sm am-btn-success am-show-sm-only blog-button" data-am-collapse="{target: '#blog-collapse'}" ><span class="am-sr-only">导航切换</span> <span class="am-icon-bars"></span></button>
<div class="am-collapse am-topbar-collapse" id="blog-collapse">
<ul class="am-nav am-nav-pills am-topbar-nav">
</ul>
<form class="am-topbar-form am-topbar-right am-form-inline"method="post" action="image" enctype="multipart/form-data">
<div class="am-form-group">
<input type="file" class="am-form-field am-input-sm" id="upload" name="upload">
</div>
<div class="am-form-group" >
<input type="submit" class="am-form-field am-input-sm" style="height: 41px">
</div>
</form>
</div>
</nav>
<!-- nav end -->
<hr>
<!-- content srart -->
<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 v-bind:src="'imageShow?imageID=' + image.imageID"><h3>{{image.imageName}}</h3>
<button style="width:100%" v-on:click.stop="remove(image.imageID)" class="am-btn am-btn-success">删除</button>
</div>
</div>
</figure>
</div>
<!-- content end -->
<footer class="blog-footer">
<div class="am-g am-g-fixed blog-fixed am-u-sm-centered blog-footer-padding">
<div class="am-u-sm-12 am-u-md-4- am-u-lg-4">
<h3>模板简介</h3>
<p class="am-text-sm">这是一个使用amazeUI做的简单的前端模板。<br> 博客/ 资讯类 前端模板 <br> 支持响应式,多种布局,包括主页、文章页、媒体页、分类页等<br>嗯嗯嗯,不知道说啥了。外面的世界真精彩<br><br>
Amaze UI 使用 MIT 许可证发布,用户可以自由使用、复制、修改、合并、出版发行、散布、再授权及贩售 Amaze UI 及其副本。</p>
</div>
<div class="am-u-sm-12 am-u-md-4- am-u-lg-4">
<h3>社交账号</h3>
<p>
<a href=""><span class="am-icon-qq am-icon-fw am-primary blog-icon blog-icon"></span></a>
<a href=""><span class="am-icon-github am-icon-fw blog-icon blog-icon"></span></a>
<a href=""><span class="am-icon-weibo am-icon-fw blog-icon blog-icon"></span></a>
<a href=""><span class="am-icon-reddit am-icon-fw blog-icon blog-icon"></span></a>
<a href=""><span class="am-icon-weixin am-icon-fw blog-icon blog-icon"></span></a>
</p>
<h3>Credits</h3>
<p>我们追求卓越,然时间、经验、能力有限。Amaze UI 有很多不足的地方,希望大家包容、不吝赐教,给我们提意见、建议。感谢你们!</p>
</div>
<div class="am-u-sm-12 am-u-md-4- am-u-lg-4">
<h1>我们站在巨人的肩膀上</h1>
<h3>Heroes</h3>
<p>
<ul>
<li>jQuery</li>
<li>Zepto.js</li>
<li>Seajs</li>
<li>LESS</li>
<li>...</li>
</ul>
</p>
</div>
</div>
<div class="blog-text-center">© 2015 AllMobilize, Inc. Licensed under MIT license. Made with love By LWXYFER</div>
</footer>
<!--[if (gte IE 9)|!(IE)]><!-->
<script src="assets/js/jquery.min.js"></script>
<!--<![endif]-->
<!--[if lte IE 8 ]>
<script src="http://libs.baidu.com/jquery/1.11.3/jquery.min.js"></script>
<script src="http://cdn.staticfile.org/modernizr/2.8.3/modernizr.js"></script>
<script src="assets/js/amazeui.ie8polyfill.min.js"></script>
<![endif]-->
<script src="assets/js/amazeui.min.js"></script>
<script src="assets/js/pinto.min.js"></script>
<script src="assets/js/img.js"></script>
<script>
var app = new Vue(
{
el:'#app',
data:{
images:[
]
},
methods:{
getImages(){
$.ajax({
url: "image",
type: "get",
context: this,
success: function (data,status) {
//回调函数
//此处代码在浏览器收到响应之后,才会执行
//参数中的data就相当于收到的 HTTP 响应中的 body 部分
this.images = data;
$('#app').resize();
}
})
},
remove(imageID){
$.ajax(
{
url: "image?imageID="+imageID,
type: "delete",
context: this,
success: function (data, status) {
this.getImages();
//弹出对话框
alert("删除成功!");
}
}
)
}
}
}
)
app.getImages();
</script>
</body>
</html>
关于如何修改
对于不懂前端的开发者来说,做减法要比从零开始简单的多,因此我们可以使用现成的模板,在浏览器开发者模式下,观察那一部分对应代码的位置,想要修改哪里就点哪里找代码位置就可以了。
关于前端的一部分知识内容
HTML
-
ul 标签,表示无序列表
li 嵌套在 ul 中,表示一个具体的导航目录 -
html中的 class 属性就是引用了一个 CSS 编写的一个类,是为了让页面更加完美好看。
-
为了实现页面上传,我们可以把之前使用的一个文件上传的
<form>
表单粘贴过来。 -
为了展示图片,我们发现,HTML 中
<img>
部分用来展示图片的,并且是根据URL显示的,那么我们可以把他改成对应服务器上图片的URL即可。
但固定的URL总是不能按我们的意愿随时上传随时显示。此时就需要我们使用JavaScript对这里进行编程。
项目测试
写完一个阶段部分的代码一定要进行测试,哪怕仅仅是一个简单的逻辑,这样可以保证错误及时发现及时改正。
测试不仅仅是专门的测试工程师来进行,开发工程师也需要进行测试,这样做是在工程上保证一个项目的双重保障(double check)。
单元测试
单元测试就是把一个类/方法作为一个单元进行测试。一旦出现问题,就可以及时发现问题所在,缩小问题查找的范围,避免项目过大,找不到bug的位置。
这里我们使用的十分简单的main方法直接运行,但是Java为我们提供了专门的单元测试框架JUnit,可以更加优雅的组织单元测试代码。
Postman(Http客户端测试工具)
我们使用单元测试法对每个接口方法测试都是直接在浏览器上查看最终实现的效果,但是DELETE方法我们无法在没有前端实现的情况下直接能够看到效果。
这里我们可以使用一种测试工具:Postman
通过输入我们服务器的URL链接,选择想要测试的方法,发送之后,就可以看到关于响应的HTTP body中的结构
关于项目缺陷
ImageDao类中selectAll方法的数据过载异常
这个方法的目的是实现查找出所有数据库表中的数据,如果数据库中内容只有几千条,还是能够实现的;如果出现上亿条数据,这样直接调出所有数据会非常低效,很可能直接导致数据库数据出现紊乱,或者应用程序直接崩溃。
解决方案: 更科学地做法是可以指定一些筛选条件,例如:最多查找100个、或者查找出第100至200条,这样做成类似网页中翻页的模式。
异常的优化处理
出现异常之后,处理的具体措施:
当前我们大部分的异常处理方式都是打印调用栈。
除此之外我们还可以有其他方式处理异常:
- 让程序直接终止
程序崩溃等让程序直接终止的方式,可以让整体程序不再继续将错就错,及时止损,不会造成更多的数据错误。 - 监控报警,通知程序员
这也是大多数运营工程师要接触的东西,当服务器全天候运行时,出现异常错误,及时通知技术人员处理。
JDBC编程中冗余和每个方法的代码相似问题的优化解决
当前我们的JDBC代码编写大多都是相似步骤和相似内容的,并且代码中也存在不少冗余,这使得我们的编程效率下降,对于这一点。
JavaEE中提供了一部分开发框架可以解决这一问题,其中的MyBaits(ORM框架)中所包含的类和方法就可以把数据库中的记录直接映射到Java代码对象上,避免了SQL语句,无需创建连接,通过简单的对象操作就可以实现JDBC编程。
关于扩展功能实现单次上传多张图片
fileupload返回的是一个List 类型的对象,使用List链表是因为HTTP支持一个请求上传多个文件,可以解析出多个文件。
public List<FileItem> parseRequest(HttpServletRequest request) throws FileUploadException {
return this.parseRequest(new ServletRequestContext(request));
}
每个链表结构中存储的FileItem对象对应一个上传文件,我们对FileItem对象取出属性信息就可以存在我们对应的Image对象中。
同时,使用File操作准备文件路径,就可以使用write()方法将FileItem对象中的内容存储在对应的文件路径位置。
如果想要同时上传多个文件,就可以对获取图片属性和写入磁盘操作做一个循环,实现多次写入。
关于不能上传同文件名文件问题
按照我们现有的设定,图片储存的文件路径是:./image/文件名
这样就决定了我们在上传一个文件时,写入磁盘的文件可能会因为文件名相同但内容不同而无法写入。
我们可以加入时间戳实现写入磁盘的文件名不同,就可以保存这类文件。
时间戳就是以1970年1月1日0分0秒为基准时刻计算当前时刻与基准时刻所得的差值。
这样就保证了每次上传文件写入磁盘路径名不同而存储同名文件了。
后面我们可以使用md5校验和也可以实现时间戳的功能。
防盗链机制
当我们上传了自己的图片到服务器后,我们的图片链接可以提供给多人使用,也可以提供在多个客户端,而访问的人过多,就会导致服务器崩溃。
所以需要一个防盗链机制来防止过多人使用访问我的图片。
我们发现在HTTP传输的协议格式中header部分有一个referer字段,用来说明这个请求来自于哪个链接。
因此我们可以制作一个白名单机制来实现防盗链,通过提取请求中referer字段判断是否在服务器白名单中,不在就不可访问。
static private HashSet<String> whiletList = new HashSet<>();
static {
whiletList.add("http://47.104.62.86:8080/java_image_server/index.html");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//防盗链,白名单
String referer = req.getHeader("Referer");
if (!whiletList.contains(referer)){
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": false,\"reason\": \"未授权的非法访问!\" }");
return;
}
}
优化磁盘存储空间
当我们上传图片的时候很也有可能会出现,图片名不同而内容相同的情况,那么我们就可以使用md5 校验和来实现,当两个图片内容完全一致时,只在磁盘上存储一份,让两个名字指向同一个文件位置即可。
doPost方法部分:
//查看数据库中是否存在相同的 MD5 的图片,不存在就返回null;
Image existImage = imageDao.selectByMd5(image.getMd5());
File file = new File(image.getPath());
if (existImage == null) {
try {
fileItem.write(file);
} catch (Exception e) {
e.printStackTrace();
resp.setStatus(200);
resp.setContentType("application/json; charset=utf-8");
resp.getWriter().write("{ \"ok\": false,\"reason\": \"写入磁盘失败!\"}");
return;
}
}