文章目录
1.关于 WebSocket 协议
首先,需要先了解一下 WebSocket 这个使用浏览器进行全双工通信的协议:
WebSocket 作为 HTML5 标准的一部分,而它现在逐渐成为了独立的协议标准。
WebSocket ,即 Web 浏览器与 Web服务器之间全双工通信标准。其中,WebSocket协议由 IETF 定为标准,WebSocket API由W3C定为标准,仍在开发中的WebSocket 技术主要是为了解决 Ajax 和 Comet 里 XMLHttpRequest 附带的缺陷所引起的问题。
(Ajax 【只更新局部页面】的问题:实时从服务器获取内容,有可能导致大量请求产生。
Comet【先将响应挂起,当服务器有内容更新时,在返回该响应】的问题:为了保留相应,一次连接的持续时间变长了,为了维持连接,可能耗费很多资源。)
一旦 Web 服务器 与 客户端之间建立起 WebSocket 协议的通信连接,之后所有的通信都依靠这个专用协议进行。通信过程中,可互相发送 JSON【一种数据格式】、XML【可扩展标记语言】、HTML 或图片等任意格式的数据。
❀由于是建立在 HTTP 基础上的协议,因此连接的发起方仍是客户端,一旦确立 WebSocket 通信连接,不论服务器还是客户端,任意一方都可以直接向对方发送报文。
2.项目的准备工作
2.1 新建 Maven【优秀的 项目构建 的管理工具 & 统一管理依赖】并导入相关依赖:
选择这个模板:
新建项目:
注意这里的目录一定要修改成自己下载的 Maven ,而不是用 IDEA 默认的。
控制台出现 BUILD SUCCESS,此时 MAVEN 创建成功:
在pom.xml 中将 UTF-8 的版本改为 1.8:
接下来,需要添加 junit 单元测试、 freemarker 模板引擎【基于模版生成静态文件的通用 工具】、druid 数据源【为监控而生的数据库连接池】、I/O处理和内容编码、Servlet【一套处理网络请求的规范。】 API、gson 【Google提供的 用来在 Java对象 和 JSON数据 之间进行映射的Java类库】的依赖:
<dependencies>
<!--junit单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<!-- websocket-->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- freemarker模版引擎 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<!-- druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
<!-- Apache commons I/O处理和内容编码 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.6.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.25-incubating</version>
</dependency>
</dependencies>
2.2 关于数据库 JDBC
复习一下 JDBC:五步走。
(1)加载驱动
通过 Class 类的静态方法forName(String className)
实现,传入"com.mysql.jdbc.Driver" 。
(2)获取连接
使用 DriverManager 的getConnection(url,uname,upass)
传入数据库的路径、用户名 和 密码。
JDBC连接的URL :jdbc:mysql://localhost:3306/jdbc?useSSL=false
(3)创建一个 Statement:
要执行SQL语句,必须获得 java.sql.Statement 实例,Statement 实例分为以下3种类型:
- 执行静态语句,通常通过Statement实例实现。
Statement st = conn.createStatement();
- 执行动态语句,通常通过PreparedStatement实现。
PreparedStatement ps = conn.prepareStatement(sql);
- 执行数据库存储过程.通常通过CallableStatement实现。
CallableStatement cs = conn.prepareCall("{CALL demoSp(?,?)}");
(4)执行 SQL 语句
查询语句 select 用executeQuery(sql)
返回一个结果集 ResultSet ,可以用next()
对结果进行遍历。
更新 update,插入 insert 或 删除 delete 语句用 executeUpdate(sql)
返回 SQL 语句执行后的影响行数。
(5)关闭资源
关闭顺序要和声明顺序相反。
以上 5 个操作在本项目中被封装在 utils 包下的 CommUtil 类中。
启动 MySQL 服务,并在IDEA中连接数据库:
接下来,为聊天室创建数据库 jdbc 和表 user:
create database if not exists `jdbc`
default character set `utf8`;
use jdbc;
create table user
(
id int primary key auto_increment comment '用户ID',
username varchar (20) unique not null comment '用户名',
password varchar (100) not null comment 'MD5加密后的密码'
)
charset ='utf8';
先加一条记录:
mysql> insert into user(username,password)
-> values('ohh','123');
Query OK, 1 row affected (0.16 sec)
热个身,熟悉一下 JDBC 的操作,创建单元测试 JDBCDemo1,先看看查询操作的执行结果:
import org.junit.Test;
import java.net.ConnectException;
import java.sql.*;
public class JDBCDemo1 {
@Test
public void test() throws ClassNotFoundException, SQLException {
//加载驱动
Class.forName("com.mysql.jdbc.Driver");
//获取连接
Connection connection= DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc?useSSL=false","root","jdpy1229jiajia");
//执行语句
String sql="select * from user";
Statement statement=connection.createStatement();
ResultSet resultSet=statement.executeQuery(sql);
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getNString("password");
System.out.println("id为" + id + ",用户名" + username + ",密码为" + password);
}
//释放资源
connection.close();
statement.close();
resultSet.close();
} }
运行结果为:
Okay ,再来看看 insert 的操作:
@Test
public void test1() throws SQLException, ClassNotFoundException {
//加载驱动
Class.forName("com.mysql.jdbc.Driver");
//获取连接
Connection connection= DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc?useSSL=false","root","jdpy1229jiajia");
//执行语句
String sql="insert into user(username,password) values ('test1','123');" ;
Statement statement=null;
try {
statement=connection.createStatement();
} catch (SQLException e) {
e.printStackTrace();
}
assert statement != null;
int resultRows=statement.executeUpdate(sql,Statement.RETURN_GENERATED_KEYS);
System.out.println(resultRows);
//释放资源
connection.close();
statement.close();
}
运行结果为
以上是两个SQL语句执行的例子,接下来来看看 SQL 注入的问题,在查询的单元测试中输入:
String username="test1";
String password="123";
String sql="select * from user where username='"+username+"' and password='"+password+"'";
这段sql是这样被划分的:
是可以正常运行的:
接下来,将 username 改成这个形式,而密码改成别的
String username="test1' or 1=1";
或者改成 String username="test1'--";
主要提供了正确的用户名,都可以查询出用户的信息,原因如下:
为了避免SQL注入,使用 PrepareStatement 类(预处理 SQL),在 SQL语句中写入 ? 占位符,如下所示。
String username="test1";
String password="123";
String sql="select * from user where username=? and password=?";
PreparedStatement statement=connection.prepareStatement(sql);
statement.setString(1,username);
statement.setString(2,password);
好,现在关于 JDBC 以及 SQL 语句的简单操作已经 OK 了,回到项目中,我们要来配置 properties 文件,Properties 是 HashTable 的子类,可以放 key-value 键值对,为 JDBC 需要用到的 驱动、URL、用户名 和 密码,创建资源文件db.properties,放置在 main/resource 下:
drivername=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc?useSSL=false
username=root
password=jdpy1229jiajia
想要后续的类都能加载到这些配置,我们在工具类 CommUtil 中,(这个类用来封装获取配置文件 db.properities 的工具方法)写入静态方法 loadProperties,获取这个属性文件(所有的工具方法都应该是静态方法):
package java.utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class CommUtil {
public CommUtil() {
}
public static Properties loadProperties(String filename)
{
Properties properties=new Properties();
//获取类加载器下的 和它同目录的所有文件,找 db.properities
InputStream in=CommUtil.class.getClassLoader().getResourceAsStream(filename);
try {
properties.load(in);
} catch (IOException e) {
e.printStackTrace();
}
return properties;
}
}
注意,此时项目结构中,需要把 java 包及以下的包都变为蓝色的 Sources ,这样才能获取到配置文件。(因为java不能作为包名,否则因为类加载器机制,会抛出 java.lang.SecurityException)而且中间建立个包 chatRoom 把 java 和 utils 隔开来。
边写边测试:
package utils;
import org.junit.Test;
import java.util.Properties;
import static org.junit.Assert.*;
public class CommUtilTest {
@Test
public void loadProperties() {
String filename="db.properties";
Properties properties= java.utils.CommUtil.loadProperties(filename);
System.out.println(properties);
}
}
运行结果:
加载属性文件成功。
接下来,将 JDBC 5步走封装到类 JDBCUtils 包中:
private static String driverName;
private static String url;
private static String username;
private static String password;
首先,这 4 个字段是必须的,无论什么操作都要先加载这 4 个属性,那就在类加载时一同加载它们,而且只需加载一次——使用静态代码块。考虑到:(1)不同用户取得的连接应是不同的,即 连接不是共有的。 (2)只有查询操作会返回 ResultSet ,更新等操作是不返回的,所以关闭资源的操作也是不同的。 所以,获取连接和关闭资源的操作都不会写到静态代码块中,
```java
import java.sql.*;
import java.util.Properties;
public class JDBCUtils {
private static String driverName;
private static String url;
private static String username;
private static String password;
static
{Properties properties=CommUtil.loadProperties("db.properties");
driverName=properties.getProperty("drivername");
url=properties.getProperty("url");
username=properties.getProperty("username");
password=properties.getProperty("password");
//接下来,加载驱动
try {
Class.forName(driverName);
} catch (ClassNotFoundException e) {
// e.printStackTrace();
System.out.println("加载数据库驱动出错");
}
}
//不同用户获取的连接是不同的,所以写成static 方法即可
public static Connection getConnection()
{
try {
return DriverManager.getConnection(url,username,password);
} catch (SQLException e) {
// e.printStackTrace();
System.err.println("获取数据库连接出错");
}
return null;
}
//关闭数据库资源
//方法的重载
public static void closeResource(Connection connection, Statement statement)
{
if(connection!=null)
{
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(statement!=null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void closeResources(Connection connection,Statement statement,ResultSet resultSet)
{
//调用上面的两个参数的方法
closeResource(connection,statement);
if(resultSet!=null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}}
}
}
边写边测试,第一个测试用例是执行查询语句,使用 statement 的情况,运行结果如下,说明 JDBCUtils 正常:
再使用预编译 PrepareStatement 的,写一个单元测试:
@Test
public void test4() throws SQLException {
Connection connection=null;
PreparedStatement statement=null;
ResultSet resultSet=null;
try{
connection=JDBCUtils.getConnection();
String sql="select * from user where id = ? and username = ?";
statement=connection.prepareStatement(sql);
statement.setInt(1,1);
statement.setString(2,"ohh");
//替代第几个占位符,从1开始
resultSet=statement.executeQuery();
while (resultSet.next())
{int id=resultSet.getInt("id");
String username=resultSet.getString("username");
String password=resultSet.getString("password");
System.out.println("id为"+id+",用户名为"+username+",密码为"+password);}
}
catch (SQLException e){}
finally {
JDBCUtils.closeResources(connection, statement, resultSet);
}
}
}
运行结果:
以上,查找操作 OK 了,来看看更新操作。
2.3 MD5加密
之前在 pom 文件中有导入 commons-io 依赖:
<!-- Apache commons I/O处理和内容编码 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
项目中要用到这个包的 DigestUtils 工具类中的 md5Hex方法,将字符串通过MD5 【Message Digest 信息摘要,对原信息进行数字变换后得到的 128 位特征码,具有不可逆性、离散性(多一个空格差别都很大)】算法转成十六进制字符串,形式,用于用户密码的加密:
public static String md5Hex(String data) {
return Hex.encodeHexString(md5(data));
}
现在尝试对用户1 的密码进行 MD5码哈希。
@Test
public void testInsert()
{Connection connection=null;
PreparedStatement statement=null;
try{
connection=JDBCUtils.getConnection();
String sql="insert into user (username,password) values(?,?)";
statement=connection.prepareStatement(sql);
statement.setString(1,"java1");
statement.setString(2,DigestUtils.md5Hex("java1"));
int influeRow=statement.executeUpdate();
Assert.assertEquals(1,influeRow);
}
catch (SQLException e){}
finally {
JDBCUtils.closeResource(connection,statement);
}}
测试用例通过,查看表,确实有生成128位的 MD5 码:
2.4 数据源与数据库连接池
❀Java中的数据源 DataSource 就是连接到数据库的一条路径 ,数据源中并无真正的数据,它仅仅记录的是你连接到哪个数据库,以及如何连接。它包含 连接池 和 连接池管理 两部分。数据源又可以分为两大类:直连的数据源 【用户每次请求都需要向数据库获得连接,创建连接通常需要消耗相对较大的资源,且耗时】和 连接池的数据源 。
❀连接池思想:
数据库连接是一种 关键的 有限的 昂贵的 资源,如果每次访问数据库的时候,都需要进行数据库连接,那么势必会造成性能低下;同时,如果用户失忘记释放数据库连接,会导致资源的浪费等。而数据库连接池就是刚好可以解决这些问题,数据库连接池的基本思想是在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法, 通过管理连接池中的多个连接对象(Connection),实现连接对象(connection)资源复用,从而大大提高了数据库连接方面的性能。在系统初始化的时候,将 数据库连接对象(Connection) 存储在内存中,当用户需要访问数据库时候,并不是建立一个新的连接,而是从连接池中取出一个已经建立好的空闲连接对象。连接池负责分配、管理、释放数据库连接对象。注意:连接池是由容器(比如tomcat) 提供的,同时容器也管理着连接池。
本项目中使用 Druid【为监控而生的数据库连接池】作为数据库连接池。
为数据源创建一个配置文件 database.properties,除了 JDBC 需要的那 4 个参数以外,还要配置:
(1) 监控统计 拦截的filters【过滤器,用于拦截用户请求】,“stat”用于统计,统计 SQL 的各种执行信息。“wall”用于防火墙:
filter=stat
(2)初始化数量
initialSize=5
(3)最大活跃数
maxActive=30
(4)间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis=60000
(5)一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
(6)给 vaildationQuery 赋值,它是用来验证数据库连接的语句,这个语句至少返回 一条数据的查询语句 。每种数据库都有自己的验证语句。MySQL 的是 SELEVT 1
validationQuery=SELECT 1
(7)接下来这几个布尔类型的赋值,我不大明白是什么意思,简书上有篇文章是这么说的: “testOnBorrow 和 testOnReturn 在生产环境一般是不开启的,主要是性能考虑。失效连接主要通过testWhileIdle保证,如果获取到了不可用的数据库连接,一般由应用处理异常。对于常规的数据库连接池,testOnBorrow 等配置参数的含义和最佳实践可以参考官方文档。
链接:https://www.jianshu.com/p/8a77e9ba6b2d
阿里巴巴在github 上的源码和配置说明链接在此:
https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
poolPreparedStatements=false
接下来就可以愉快地使用 Gruid 连接池啦。
在 utils 包中新建一个类 JDBCUtilWizGruid:
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.alibaba.druid.pool.DruidPooledConnection;
import com.alibaba.druid.support.spring.stat.annotation.Stat;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class JDBCUtilWizDruid {
private static DataSource dateSource;
static {
Properties properties = CommUtil.loadProperties("database.properties");
try {
dateSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
// e.printStackTrace();
System.err.println("获取数据源失败");
}
}
public static DruidPooledConnection getConnection()
{
try {
return (DruidPooledConnection) dateSource.getConnection();
} catch (SQLException e) {
// e.printStackTrace();
System.err.println("连接数据库失败");
}
return null;
}
public static void closeResource(Connection connection, Statement statement)
{
if(connection!=null)
{
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(statement!=null)
{
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void closeResource(Connection connection, Statement statement, ResultSet resultSet)
{closeResource(connection,statement);
if(resultSet!=null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
可以看到,它也是完成了加载驱动、获取连接、关闭资源的操作,将 用连接池 与 直连 的数据源做对比,也就是之前的 JDBCUtils 与 现在这个 JDBCUtilWizDruid:
写一个单元测试,数据源采用连接池形式:
@Test
public void testInsertwizGruid()
{Connection connection=null;
PreparedStatement statement=null;
try{
connection= JDBCUtilWizDruid.getConnection();
String sql="insert into user (username,password) values(?,?)";
statement=connection.prepareStatement(sql);
statement.setString(1,"java2");
statement.setString(2,DigestUtils.md5Hex("java2"));
int influeRow=statement.executeUpdate();
Assert.assertEquals(1,influeRow);
}
catch (SQLException e){}
finally {
JDBCUtilWizDruid.closeResource(connection,statement);
}}
这时查看表,确实有生成 “java2” 这个用户。
2.5 Gson【Google提供的用来在Java对象和JSON数据之间进行映射的Java类库。】
Gson 可以将一个 JSon 字符串【以 “{” 开始,同时以"}"结束,键值对之间以 “:” 相隔,不同的键值对之间以 “,” 相隔】转成一个 Java 对象——反序列化——fromJson() , 或者将一个 Java 对象转成 JSon——序列化——toJson(),示意图如下:
新建实体 entity 包,在其下创建 user 类,这个类的属性应该对应数据库中表 user 的属性,并为其设置 getter、setter 方法,toString 方法:
package entity;
public class user {
private int id;
private String username;
private String password;
}
在CommUtil 类中提供一个序列化 objectToJson 与 反序列化 jsonToObjec的方法create() :
//先生成gson对象,后续调用它的实例方法
private static final Gson gson=new GsonBuilder().create();
public static String objectToJson(Object obj)
{return gson.toJson(obj);}
public static Object jsonToObject(String jsonstr,Class objclass)
{return gson.fromJson(jsonstr,objclass);}
生成 gson 对象的方法 和 生成数据源对象的方法有异曲同工之妙:
进行单元测试,先产生一个user 类的 Java对象,将其序列化:
@Test
public void gsaonTest1()
{
user user1=new user();
user1.setId(12);
user1.setUsername("xiaoJia");
user1.setPassword("eat");
String Jsonstr=CommUtil.objectToJson(user1);
System.out.println(Jsonstr);
}
运行结果,看到了 Json:
再来个单元测试,看看反序列的效果:
@Test
public void gsonTest2()
{String jsonstr="{\"id\":12,\"username\":\"xiaoJia\",\"password\":\"eat\"}";
user user2= (user) CommUtil.jsonToObject(jsonstr,user.class);
System.out.println(user2);
运行结果,反序列化成功: