目录
一、需求分析
1.登录:同一个浏览器,即使多个标签页,保持相同的session。
2.对应消息频道可以收发消息,不同的消息频道不能收发。
3.历史记录查看,新登陆用户可以查看退出登陆后的消息
二、业务背景
1.张三要发消息给李四
实现
1.客户端点到点发消息
2.服务端转发消息(本项目方法)
使用http协议是不可以的因为
服务端的IP是开放在公网的,所有人都能访问
客户端的IP是不开放的
2.WebSocket实现消息推送流程
一.客户端和服务器端建立连接,只能客户发起请求,双方建立链接。
//客户端js代码
websocket = new WebSocket("ws://localhost:8080/java_chatroom/test/1");
1.1双方建立连接(双方通知打开连接的回调函数执行)
1.2客户端建立连接回调
//连接成功建立的回调方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}
1.3服务端建立连接回调,同时建立长连接Session
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
this.userId = userId;
System.out.println("打开连接: " + userId);
}
二、双方的通信都是全双工,客户端和服务端都可以主动收发消息
2.1客户端发送消息
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
2.2服务端发送消息
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println("收到消息! " + userId + ": " + message);
session.getBasicRemote().sendText(message);
}
建立连接以后,就会有的会话对象,只要有sesiion引用就可以发消息,区别于Servlet的session
三、双方接收消息:被动接受(事件驱动的异步回调)
3.1:客户端接收消息
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
3.2:服务端接收消息
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println("收到消息! " + userId + ": " + message);
//session.getBasicRemote().sendText(message);
}
三、前后端接口和数据库系统设计
1.用户相关的接口
1.注册
请求:
POST /register
{
name: xxx,
password: xxx,
nickName: "蔡徐坤",
signature: "我擅长唱跳rap篮球", }
响应:
HTTP/1.1 200 OK
{
ok: 1,
reason: xxx
}
2.登录 输入账号密码,点击登录按钮,调用的接口
请求:
POST /login
{
name: xxx,
password: xxx
}
响应:
HTTP/1.1 200 OK
{
ok: 1,
reason: xxx,
userId: xxx,
name: xxx,
nickName: xxx,
signature: xxx
}
3.检查登陆状态 页面初始化,调用的接口
请求:
GET /login
响应:
响应:
HTTP/1.1 200 OK
{
ok: 1,
userId: xxx,
name: xxx,
nickName: xxx,
signature: xxx
}
注销
请求:
GET /logout
响应:
HTTP/1.1 200 OK
{
ok: 1,
reason: xxx
}
2.频道相关接口
查找频道
请求:
GET /channel
响应:
HTTP/1.1 200 OK
[
{
channelId: 1,
channelName: xxx
},
{
channelId: 2,
channelName: xxx
}
]
3.数据库表的设计
user表
create table user (
userId int primary key auto_increment,
name varchar(50) unique,
password varchar(50),
nickName varchar(50), -- 昵称
iconPath varchar(2048), -- 头像路径
signature varchar(100),
lastLogout DateTime -- 上次登录时间
); -- 个性签名
频道表
create table channel (channelId int primary key auto_increment,
channelName varchar(50)
);
insert into channel values(null, '体坛赛事');
insert into channel values(null, '娱乐八卦');
insert into channel values(null, '时事新闻');
insert into channel values(null, '午夜情感');
信息表
create table message (messageId int primary key auto_increment,
userId int, -- 谁发的
channelId int, -- 发到哪个频道中
content text, -- 消息内容
sendTime DateTime default now() -- 发送时间
);
insert into message values (null, 1, 1, 'hehe1', now());
insert into message values (null, 1, 1, 'hehe2', now());
insert into message values (null, 1, 1, 'hehe3', now());
四、功能交互实现原理及代码展示
1.输入url访问主页
2.调用检查登陆状态接口
2.1参数ok:true时显示登录信息
3.响应ok:false不包含用户信息显示未登录界面
4.点击登录按钮显示登录框
5.输入账号密码,点击登录按钮调用login接口,创建session
请求:
POST /login
{
name: xxx,
password: xxx
}
响应:
HTTP/1.1 200 OK
{
ok: 1,
reason: xxx,
userId: xxx,
name: xxx,
nickName: xxx,
signature: xxx
}
6.参数ok:true表示登陆成功跳转至频道列表页面
7.查找频道信息初始化websocket
app.getChannels();
app.initWebSocket();
7.1查找频道接口
请求:
GET /channel
响应:
HTTP/1.1 200 OK
[
{
channelId: 1,
channelName: xxx
},
{
channelId: 2,
channelName: xxx
}
]
7.2调用初始化websocket方法,筛选频道
if('WebSocket' in window){
this.websocket = new WebSocket("ws://localhost:8080/java_chatroom/message/" + this.user.userId);
console.log("link success")
}else{
alert('Not support websocket')
}
//连接发生错误的回调方法
this.websocket.onerror = function () {
alert("连接发生错误!");
};
//连接成功建立的回调方法
this.websocket.onopen = function (event) {
console.log("连接建立成功");
}
//接收到消息的回调方法
this.websocket.onmessage = function (event) {
let message = JSON.parse(event.data);
app.messages.push(message);
//筛选频道
app.curChannelMessages = app.messages.filter((message, i) => {
if (message.channelId == app.curChannelId) {
return true;
}
return false;
});
}
//连接关闭的回调方法
this.websocket.onclose = function () {
console.log("服务器断开连接");
}
8.历史记录和新消息的区别
五、开发过程及结果展示
1.工具类Util类开发Json序列化与数据库操作
public class Util {
//序列化
private static final ObjectMapper M = new ObjectMapper();
//数据库连接池
private static final MysqlDataSource DS = new MysqlDataSource();
//初始化
static {
//设置json序列化/反序列化的日期格式
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
M.setDateFormat(df);
DS.setURL("jdbc:mysql://localhost:3306/java_chatroom");
DS.setUser("root");
DS.setPassword("111111");
DS.setUseSSL(false);
DS.setCharacterEncoding("UTF-8");//解决插入修改数据,如果是中文,乱码的问题
}
/**
* json序列化:java对象转换为json字符串
*/
public static String serialize(Object o){
try {
return M.writeValueAsString(o);
} catch (JsonProcessingException e) {
throw new AppException("json序列化失败:"+o, e);
}
}
/**
* 反序列化:把json字符串转换为java对象
*/
public static <T> T deserialize(String s, Class<T> c){
try {
return M.readValue(s, c);
} catch (JsonProcessingException e) {
//如果出现这个异常,一般都是json字符串中的键,在class中没有找到对应的属性
throw new AppException("json反序列化失败", e);
}
}
public static <T> T deserialize(InputStream is, Class<T> c){
try {
return M.readValue(is, c);
} catch (IOException e) {
//如果出现这个异常,一般都是json字符串中的键,在class中没有找到对应的属性
throw new AppException("json反序列化失败", e);
}
}
/**
* 获取数据库连接
*/
public static Connection getConnection(){
try {
return DS.getConnection();
} catch (SQLException e) {
throw new AppException("获取数据库连接失败", e);
}
}
/**
* 释放jdbc资源
*/
public static void close(Connection c, Statement s, ResultSet r){
try {
if(r != null) r.close();
if(s != null) s.close();
if(c != null) c.close();
} catch (SQLException e) {
throw new AppException("释放数据库资源出错", e);
}
}
public static void close(Connection c, Statement s){
close(c, s, null);
}
public static void main(String[] args) {
//测试json序列化
Map<String, Object> map = new HashMap<>();
map.put("ok", true);
map.put("d", new Date());
System.out.println(serialize(map));
//测试数据库连接: 需要把init.sql在cmd执行,初始化数据库,表,数据
System.out.println(getConnection());
}
}
2.model层实体类设计与数据库相关联
2.1用户实体类
//数据库使用,前后端ajax,session保存时基于对象和二进制数据转换(这里要实现串行化接口)
@Getter
@Setter
@ToString
public class User extends Response implements Serializable {
private static final Long serialVersionUID = 1L;
private Integer userId;
private String name;
private String password;
private String nickName;
private String iconPath;
private String signature;
private java.util.Date lastLogout;
}
2.2消息实体类
@Setter
@Getter
@ToString
public class Message {
private Integer messageId;
private Integer userId;
private Integer channelId;
private String content;
private java.util.Date sendTime;
//接收客户端发送的消息,转发到所有客户端的消息,需要昵称
private String nickName;
}
2.3频道实体类
@ToString
@Getter
@Setter
public class Channel {
private Integer channelId;
private String channelName;
}
2.4信息返回实体类
@Setter
@Getter
@ToString
public class Response {
//当前接口响应是否操作成功
private boolean ok;
//操作失败时,前端要展示的错误信息
private String reason;
private Object data;//保存业务数据
}
3.service层根据前后端接口编写Servlet
1.请求url,对应Servlet的路径
2.一次init,多次service根据请求方法调用doxxx方法,
3.1登录功能开发
3.1.1根据前端登录接口调用LoginServlet
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
//检测登陆状态
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json");
//响应的数据:根据接口文档,user类中都包含了约定的字段
User u = new User();
//获取当前请求的session,并再获取用户信息,如果获取不到,返回ok:false
HttpSession session = req.getSession(false);
if(session != null){
User get = (User) session.getAttribute("user");
if(get != null){
//已经登录,并获取到用户信息
u = get;
u.setOk(true);
resp.getWriter().println(Util.serialize(u));
return;
}
}
u.setOk(false);//其实不用设置,该字段为boolean,默认就是false
u.setReason("用户未登陆");
//3 返回响应数据: 从响应对象获取输出流,打印输出到响应体body
resp.getWriter().println(Util.serialize(u));
}
//登录接口
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json");
//响应的数据:根据接口文档,user类中都包含了约定的字段
User u = new User();
try {
//1.解析请求数据:根据接口文档,需要使用反序列化操作
User input = Util.deserialize(req.getInputStream(), User.class);
//2.业务处理: 数据库验证账号密码,如果验证通过,创建session,保存用户信息
//根据账号查询用户
User query = UserDAO.queryByName(input.getName());
if(query == null){
throw new AppException("用户不存在");
}
if(!query.getPassword().equals(input.getPassword())){
throw new AppException("账号或密码错误");
}
//账号密码验证成功
HttpSession session = req.getSession();
session.setAttribute("user", query);
u = query;
//构造操作成功正常返回数据:ok:true, 业务字段
u.setOk(true);
}catch (Exception e){
e.printStackTrace();
//构造操作失败的错误信息 ok;false reason:错误信息。
u.setOk(false);
//自定义异常,自己抛,为中文信息,可以给用户看
if(e instanceof AppException){
u.setReason(e.getMessage());
}else{//非自定义异常,英文信息,转一下
u.setReason("未知的错误,请联系管理员");
}
}
//3.返回响应数据: 从响应对象获取输出流,打印输出到响应体body
resp.getWriter().println(Util.serialize(u));
}
}
3.1.2LoginServlet调用UserDAO操作数据库验证用户合法性
public static User queryByName(String name) {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
//定义返回数据
User u = null;
try {
//1 获取数据库连接Connection
c = Util.getConnection();
//2 通过Connection+sql创建操作命令对象Statement
String sql = "select * from user where name=?";
ps = c.prepareStatement(sql);
//3 执行sql: 执行前替换占位符
ps.setString(1, name);
rs = ps.executeQuery();
//4 如果是查询操作,处理结果集
while (rs.next()){//移动到下一行,有数据返回true
u = new User();
//设置结果集字段到用户对象的属性中
u.setUserId(rs.getInt("userId"));
u.setName(name);
u.setPassword(rs.getString("password"));
u.setNickName(rs.getString("nickName"));
u.setIconPath(rs.getString("iconPath"));
u.setSignature(rs.getString("signature"));
java.sql.Timestamp lastLogout = rs.getTimestamp("lastLogout");
u.setLastLogout(new Date(lastLogout.getTime()));
}
return u;
}catch (Exception e){
throw new AppException("查询用户账号出错", e);
}finally {
//5 释放资源
Util.close(c, ps, rs);
}
}
3.2登陆成功后查找频道信息功能
3.2.1前端跳转接口调用ChannelServlet
@WebServlet("/channel")
public class ChannelServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json");
Response response = new Response();
try{
//查询所有频道列表返回
List<Channel> list = ChannelDAO.query();
response.setOk(true);
response.setData(list);
//ok:true, data: [{}, {}]
}catch (Exception e){
e.printStackTrace();
//目前,前端的实现,在后端报错,要返回空的List
//改造前端为解析ok, reason
//参考LoginServlet改造:
response.setReason(e.getMessage());
//ok:false, reason: ""
}
resp.getWriter().println(Util.serialize(response));
}
}
3.2.2ChannelServlet调用ChannelDAO查询
public class ChannelDAO {
public static List<Channel> query() {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
//定义返回数据
List<Channel> list = new ArrayList<>();
try {
//1 获取数据库连接Connection
c = Util.getConnection();
//2 通过Connection+sql创建操作命令对象Statement
String sql = "select * from channel";
ps = c.prepareStatement(sql);
//3 执行sql: 执行前替换占位符
rs = ps.executeQuery();
//4 如果是查询操作,处理结果集
while (rs.next()){//移动到下一行,有数据返回true
Channel channel = new Channel();
//设置属性
channel.setChannelId(rs.getInt("channelId"));
channel.setChannelName(rs.getString("channelName"));
list.add(channel);
}
return list;
}catch (Exception e){
throw new AppException("查询频道列表出错", e);
}finally {
//5 释放资源
Util.close(c, ps, rs);
}
}
}
3.3频道进入后进行消息发送和接收
3.3.1MessageWebsocketServlet关联TestWebSocket
@ServerEndpoint("/message/{userId}")
public class MessageWebsocket {
@OnOpen
public void onOpen(@PathParam("userId") Integer userId,
Session session) throws IOException {
//1.把每个客户端的session都保存起来,之后转发消息到所有客户端要用
MessageCenter.addOnlineUser(userId, session);
//2.查询本客户端(用户)上次登录前的消息(数据库查)
List<Message> list = MessageDAO.queryByLastLogout(userId);
//3.发送当前用户在上次登录后的消息
for(Message m : list){
session.getBasicRemote().sendText(Util.serialize(m));
}
System.out.println("建立连接:"+userId);
}
@OnMessage
public void onMessage(Session session,
String message){
//1.遍历保存的所有session,每个都发送消息
// MessageCenter.sendMessage(message);
MessageCenter.getInstance().addMessage(message);
//2.消息还要保存在数据库:
// (1)反序列化json字符串为message对象
Message msg = Util.deserialize(message, Message.class);
// (2)插入数据库
int n = MessageDAO.insert(msg);
System.out.printf("接收到消息:%s\n", message);
}
@OnClose
public void onClose(@PathParam("userId") Integer userId){
//1.本客户端关闭连接,要在之前保存的session集合中,删除
MessageCenter.delOnlineUser(userId);
//2.建立连接要获取用户上次登录以后的消息,所以关闭长连接就是代表用户退出
//更新用户的上次登录时间
int n = UserDAO.updateLastLogout(userId);
System.out.println("关闭连接");
}
@OnError
public void onError(@PathParam("userId") Integer userId, Throwable t){
System.out.println("出错了");
MessageCenter.delOnlineUser(userId);
t.printStackTrace();
//和关闭连接的操作一样
}
}
3.3.2调用MessageDAO层操作数据库
public static int insert(Message msg) {
Connection c = null;
PreparedStatement ps = null;
try{
c = Util.getConnection();
String sql = "insert into message values(null,?,?,?,?)";
ps = c.prepareStatement(sql);
ps.setInt(1, msg.getUserId());
ps.setInt(2, msg.getChannelId());
ps.setString(3, msg.getContent());
ps.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
return ps.executeUpdate();
}catch (Exception e){
throw new AppException("保存消息出错", e);
}finally {
Util.close(c, ps);
}
}
public static List<Message> queryByLastLogout(Integer userId) {
Connection c = null;
PreparedStatement ps = null;
ResultSet rs = null;
//定义返回数据
List<Message> list = new ArrayList<>();
try {
//1 获取数据库连接Connection
c = Util.getConnection();
//2 通过Connection+sql创建操作命令对象Statement
String sql = "select m.*,u.nickName from message m join user u on u.userId=m.userId where m.sendTime>(select lastLogout from user where userId=?)";
ps = c.prepareStatement(sql);
//3 执行sql: 执行前替换占位符
ps.setInt(1, userId);
rs = ps.executeQuery();
//4 如果是查询操作,处理结果集
while (rs.next()){//移动到下一行,有数据返回true
Message m = new Message();
//获取结果集字段,设置对象属性
m.setUserId(userId);
m.setNickName(rs.getString("nickName"));
m.setContent(rs.getString("content"));
m.setChannelId(rs.getInt("channelId"));
list.add(m);
}
return list;
}catch (Exception e){
throw new AppException("查询用户["+userId+"]的消息出错", e);
}finally {
//5 释放资源
Util.close(c, ps, rs);
}
}
}
3.4退出登录功能
3.4.1前端接口跳转执行LogoutServlet
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json");
HttpSession session = req.getSession(false);
if(session != null){
User user = (User) session.getAttribute("user");
if(user != null){
//用户已登录:删除session中保存的用户信息(注销)
session.removeAttribute("user");
//注销成功,返回ok: true
Response r = new Response();
r.setOk(true);
resp.getWriter().println(Util.serialize(r));
return;
}
}
//用户未登陆
Response r = new Response();
r.setReason("用户未登陆,不允许访问");
resp.getWriter().println(Util.serialize(r));
}
}
3.4.2LogoutServlet删除session会话信息并进行更新登陆时间操作
public static int updateLastLogout(Integer userId) {
Connection c = null;
PreparedStatement ps = null;
try{
c = Util.getConnection();
String sql = "update user set lastLogout=? where userId=?";
ps = c.prepareStatement(sql);
ps.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
ps.setInt(2, userId);
return ps.executeUpdate();
}catch (Exception e){
throw new AppException("修改用户上次登录时间出错", e);
}finally {
Util.close(c, ps);
}
}
六、项目开发重难点
1.Session会话
Session:登陆后,注销前或超时前都是一个会话,用来解决未登录的敏感资源访问问题
代码:
登陆时:账号密码验证通过后request.getSession();
登陆后,都可以获取到Session对象
2.ajax
异步请求回调(等请求的过程中干别的)
过程:
1.HTTP请求数据:路径,请求方法,请求数据
2.通过路径调用后端Servlet中的相关类,找到对应的请求方法doPost
3.对象序列化为json字符串
4.异步回掉函数,不是傻乎乎的等
//接口:
请求:
POST /login
{
name: xxx,
password: xxx
}
响应:
HTTP/1.1 200 OK
{
ok: 1,
reason: xxx,
userId: xxx,
name: xxx,
nickName: xxx,
signature: xxx
}
//前端ajax代码——登录功能
loginAccount() {
console.log("login");
$.ajax({
url: 'login',
type: 'post',
contentType: 'application/json',
data: JSON.stringify({
name: app.login.inputUsername,
password: app.login.inputPassword,
}),
success: function(data, status) {
if (!data.ok) {
alert('登陆失败! ' + data.reason);
app.login.isLogin = false;
return;
}
app.user.userId = data.userId;
app.user.name = data.name;
app.user.nickName = data.nickName;
app.user.signature = data.signature;
app.login.isLogin = true;
app.login.showLoginDialog = false;
app.getChannels();
app.initWebSocket();
}
})
},
4.涉及Web技术
1.开发流程:开发——打包——部署——运行——验证
打包:maven package
部署:复制war文件到tomcat/webapps下
运行:运行tomcat自动解压webapps目录下的war文件,每个文件夹就是一个web项目(应用上下文路径)
2.IDEA部署Tomcat应用上下文路径保持一致。
5.认识WebSocket
服务端主动向客户端发送。
消息推送方法:
轮询方式:客户端定时向服务端发送ajax请求,服务器接收到请求后马上返回消息并关闭连接。
优点:后端程序编写比较容易。
缺点:TCP的建立和关闭操作浪费时间和带宽,请求中有大半是无用,浪费带宽和服务器资源。
实例:适于小型应用。
长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息
并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
优点:在无消息的情况下不会频繁的请求,耗费资源小。
缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。
实例:WebQQ、Hi网页版、Facebook IM。
长连接:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用
xhr请求,服务器端就能源源不断地往客户端输入数据。
优点:消息即时到达,不发无用请求;管理起来也相对方便。
缺点:服务器维护一个长连接会增加开销,当客户端越来越多的时候,server压力大!
实例:Gmail聊天
webSocket:HTML5 WebSocket设计出来的目的就是取代轮询和长连接,使客户端浏览器具备像C/S
框架下桌面系统的即时通讯能力,实现了浏览器和服务器全双工通信,建立在TCP之上,虽然
WebSocket和HTTP一样通过TCP来传输数据,但WebSocket可以主动的向对方发送或接收数据,就像
Socket一样;并且WebSocket需要类似TCP的客户端和服务端通过握手连接,连接成功后才能互相通
信。
优点:双向通信、事件驱动、异步、使用ws或wss协议的客户端能够真正实现意义上的推送功能。
缺点:少部分浏览器不支持。
示例:社交聊天(微信、QQ)、弹幕、多玩家玩游戏、协同编辑、股票基金实时报价、体育实况更
新、视频会议/聊天、基于位置的应用、在线教育、智能家居等高实时性的场景。
6.序列化与反序列化
public class Util {
//序列化
private static final ObjectMapper M = new ObjectMapper();
//数据库连接池
private static final MysqlDataSource DS = new MysqlDataSource();
//初始化
static {
//设置json序列化/反序列化的日期格式
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
M.setDateFormat(df);
DS.setURL("jdbc:mysql://localhost:3306/java_chatroom");
DS.setUser("root");
DS.setPassword("111111");
DS.setUseSSL(false);
DS.setCharacterEncoding("UTF-8");//解决插入修改数据,如果是中文,乱码的问题
}
/**
* json序列化:java对象转换为json字符串
*/
public static String serialize(Object o){
try {
return M.writeValueAsString(o);
} catch (JsonProcessingException e) {
throw new AppException("json序列化失败:"+o, e);
}
}
/**
* 反序列化:把json字符串转换为java对象
*/
public static <T> T deserialize(String s, Class<T> c){
try {
return M.readValue(s, c);
} catch (JsonProcessingException e) {
//如果出现这个异常,一般都是json字符串中的键,在class中没有找到对应的属性
throw new AppException("json反序列化失败", e);
}
}
public static <T> T deserialize(InputStream is, Class<T> c){
try {
return M.readValue(is, c);
} catch (IOException e) {
//如果出现这个异常,一般都是json字符串中的键,在class中没有找到对应的属性
throw new AppException("json反序列化失败", e);
}
}
}
7.JDBC操作
1.占位符替换:使用变量,对象中的属性
2.查询操作,返回的结果集,多行数据转换为list,一行数据转换为一个对象