前言
两年前的存货,前几天有同学需要,csdn上设置了付费,这里免费开放
失踪人口回归兄弟们,差不多连续5天没有发布博文了,许久不见。当然也是最近确实不在状态而且比较忙,所以就没有去更新博文,加上最近作业时真的多,各种大作业顶不住。
那么今天也是给大家展示一个小dome,基于SpringBoot + mybatis + websocket 做的在线聊天器。本来是要做在线群聊的,但是这个前端我是真的不想调了,本来是想嫖的页面的,结果怎么说,还是自己做吧,可控。
代码也比较简单,后端搭建写起来其实也就两三个小时,主要是前端,那玩意零零散散花了两天,然后还有这个调试,总体代码从星期一开始写,到星期三写完了,后面没空不想写,于是拖到今天调测完,砍了不少功能。
ok,我们先来看看这个效果图哈。
效果
主页面
消息提示
聊天页面
大概就这样,当然还有登录,注册。
登录注册
大概的话就是这个样子。
OK,现在呢,咱们就进入到这个代码阶段。
前端
首先我们先看到前端。
项目构建
既然是从0开始,那么我们就从o开始说。咱们这个是使用vue2 + elementui 写的,为什么是vue2,简单,我没升级嘛。能跑就行,我也不是专门写前端的。
首先省略 使用 vue-cli 开始项目哈。
依赖
我这里就说我这里使用到的依赖。
elementUI axios websocket
就这几个主要的。
项目结构
这里其实就三个页面。
登录注册
在此之前,我们先来看看这个路由设计哈。
routes: [
{
path: '/',
name: 'mian',
component: main
},
{
path: '/login',
name: 'login',
component: login
},
{
path: '/register',
name: 'register',
component: register
}
],
mode: "history"
就非常的简单哈。
验证码部分
在这里我们先来说说,这个验证码部分吧。
这个呢,是由前端生成的。是这个组件在干活
<template>
<div class="s-canvas">
<canvas id="s-canvas" :width="contentWidth" :height="contentHeight"></canvas>
</div>
</template>
<script>
export default {
name: "SIdentify",
props: {
identifyCode: {
type: String,
default: '1234'
},
fontSizeMin: {
type: Number,
default: 25
},
fontSizeMax: {
type: Number,
default: 30
},
backgroundColorMin: {
type: Number,
default: 255
},
backgroundColorMax: {
type: Number,
default: 255
},
colorMin: {
type: Number,
default: 0
},
colorMax: {
type: Number,
default: 160
},
lineColorMin: {
type: Number,
default: 100
},lineColorMax: {
type: Number,
default: 255
},
dotColorMin: {
type: Number,
default: 0
},
dotColorMax: {
type: Number,
default: 255
},
contentWidth: {
type: Number,
default: 112
},
contentHeight: {
type: Number,
default: 31
}
},
methods: {
// 生成一个随机数
randomNum(min, max) {
return Math.floor(Math.random() * (max - min) + min)
},
// 生成一个随机的颜色
randomColor(min, max) {
let r = this.randomNum(min, max)
let g = this.randomNum(min, max)
let b = this.randomNum(min, max)
return 'rgb(' + r + ',' + g + ',' + b + ')'
},
drawPic() {
let canvas = document.getElementById('s-canvas')
let ctx = canvas.getContext('2d')
ctx.textBaseline = 'bottom'
// 绘制背景
ctx.fillStyle = this.randomColor(this.backgroundColorMin, this.backgroundColorMax)
ctx.fillRect(0, 0, this.contentWidth, this.contentHeight)
// 绘制文字
for (let i = 0; i < this.identifyCode.length; i++) {
this.drawText(ctx, this.identifyCode[i], i)
}
this.drawLine(ctx)
this.drawDot(ctx)
},
drawText(ctx, txt, i) {
ctx.fillStyle = this.randomColor(this.colorMin, this.colorMax)
ctx.font = this.randomNum(this.fontSizeMin, this.fontSizeMax) + 'px SimHei'
let x = (i + 1) * (this.contentWidth / (this.identifyCode.length + 1))
let y = this.randomNum(this.fontSizeMax, this.contentHeight - 5)
var deg = this.randomNum(-45, 45)
// 修改坐标原点和旋转角度
ctx.translate(x, y)
ctx.rotate(deg * Math.PI / 180)
ctx.fillText(txt, 0, 0)
// 恢复坐标原点和旋转角度
ctx.rotate(-deg * Math.PI / 180)
ctx.translate(-x, -y)
},
drawLine(ctx) {
// 绘制干扰线
for (let i = 0; i < 5; i++) {
ctx.strokeStyle = this.randomColor(this.lineColorMin, this.lineColorMax)
ctx.beginPath()
ctx.moveTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.lineTo(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight))
ctx.stroke()
}
},
drawDot(ctx) {
// 绘制干扰点
for (let i = 0; i < 80; i++) {
ctx.fillStyle = this.randomColor(0, 255)
ctx.beginPath()
ctx.arc(this.randomNum(0, this.contentWidth), this.randomNum(0, this.contentHeight), 1, 0, 2 * Math.PI)
ctx.fill()
}
}
},
watch: {
identifyCode() {
this.drawPic()
}
},
mounted() {
this.drawPic()
}
}
</script>
<style scoped>
.s-canvas {
height: 38px;
}
.s-canvas canvas{
margin-top: 1px;
margin-left: 8px;
}
</style>
之后就是引用,这个是很简单的。
登录页面
在开始之前,我还要说一下这个代理转发,这个代理我是直接在前端做的,我的原则是压力给到前端~
ok ,到这里咱们就能够放代码了
<template>
<div>
<el-form :model="formLogin" :rules="rules" ref="ruleForm" label-width="0px" class="login-bok">
<el-form-item prop="account">
<el-input v-model="formLogin.account" placeholder="账号">
<i slot="prepend" class="el-icon-s-custom"/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="密码" v-model="formLogin.password">
<i slot="prepend" class="el-icon-lock"/>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-row :span="24">
<el-col :span="12">
<el-input v-model="formLogin.code" auto-complete="off" placeholder="请输入验证码" size=""></el-input>
</el-col>
<el-col :span="12">
<div class="login-code" @click="refreshCode">
<!--验证码组件-->
<s-identify :identifyCode="identifyCode"></s-identify>
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<div class="login-btn">
<el-button type="primary" @click="goregist()" style="margin-left: auto;width: 35%" >注册</el-button>
<el-button type="primary" @click="submitForm()" style="margin-left: 27%;width: 35%">登录</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import SIdentify from "../../components/SIdentify";
export default {
name: "login",
components: { SIdentify },
data() {
return{
formLogin: {
account: "",
password: "",
code: "",
token: '',
success: '',
},
identifyCodes: '1234567890abcdefjhijklinopqrsduvwxyz',//随机串内容
identifyCode: '',
// 校验
rules: {
account:
[
{ required: true, message: "请输入用户名", trigger: "blur" }
],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
}
}
},
mounted () {
// 初始化验证码
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
},
methods:{
refreshCode () {
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
},
makeCode (o, l) {
for (let i = 0; i < l; i++) {
this.identifyCode += this.identifyCodes[this.randomNum(0, this.identifyCodes.length)]
}
},
randomNum (min, max) {
return Math.floor(Math.random() * (max - min) + min)
},
goregist(){
this.$router.push("/register")
}
,
logincount(){
this.axios({
url: "/boot/login",
method: 'post',
headers: { "type": "hello" },
data: {
account: this.formLogin.account,
password: this.formLogin.password.toLowerCase(),
}
}).then(res =>{
this.formLogin.success = res.data.success
this.formLogin.token = res.data.token
if(this.formLogin.success =='1'){
//设置token七天过期
localStorage.setExpire("token",this.formLogin.token,604800000);
alert("登录成功~")
this.$router.push("/")
}
else {
alert("用户名或密码错误!")
}
})
},
submitForm(){
if (this.formLogin.code.toLowerCase() !== this.identifyCode.toLowerCase()) {
this.$message.error('请填写正确验证码')
this.refreshCode()
}
else {
this.logincount()
}
}
},
}
</script>
<style scoped>
.login-bok{
width: 30%;
margin: 150px auto;
border: 1px solid #DCDFE6;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 30px #DCDFE6;
}
</style>
注册页面
<template>
<div>
<el-form :model="formRegist" :rules="rules" ref="ruleForm" label-width="0px" class="login-bok">
<el-form-item prop="account">
<el-input v-model="formRegist.account" placeholder="创建账号" :maxlength="16" >
<i slot="prepend" class="el-icon-s-custom"/>
</el-input>
</el-form-item>
<el-form-item prop="username">
<el-input v-model="formRegist.username" placeholder="创建用户" :maxlength="16" >
<i slot="prepend" class="el-icon-s-custom"/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" placeholder="输入密码" :maxlength="16" v-model="formRegist.password">
<i slot="prepend" class="el-icon-lock"/>
</el-input>
</el-form-item>
<el-form-item prop="againpassword">
<el-input type="password" placeholder="再次输入密码" :maxlength="16" v-model="formRegist.againpassword">
<i slot="prepend" class="el-icon-lock"/>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-row :span="24">
<el-col :span="12">
<el-input v-model="formRegist.code" auto-complete="off" placeholder="请输入验证码" size=""></el-input>
</el-col>
<el-col :span="12">
<div class="login-code" @click="refreshCode">
<!--验证码组件-->
<s-identify :identifyCode="identifyCode"></s-identify>
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<div class="login-btn">
<el-button type="primary" @click="gologin()" style="margin-left: auto;width: 35%">返回登录</el-button>
<el-button type="primary" @click="submitForm()" style="margin-left: 27%;width: 35%" >确定</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import SIdentify from "../../components/SIdentify";
import axios from "axios";
export default {
name: "register",
components: {SIdentify},
data() {
return {
formRegist: {
account: "",
username: "",
password: "",
againpassword: "",
code: ""
},
flag: '',
pass: '1',
identifyCodes: '1234567890abcdefjhijklinopqrsduvwxyz',//随机串内容
identifyCode: '',
// 校验
rules: {
username:
[
{required: true, message: "请输入用户名", trigger: "blur"}
],
account:
[
{required: true, message: "请输入账号", trigger: "blur"}
],
password: [{required: true, message: "请输入密码", trigger: "blur"}],
againpassword: [{required: true, message: "请再次输入密码", trigger: "blur"}],
code: [{required: true, message: "请输入验证码", trigger: "blur"}]
}
}
},
mounted() {
// 初始化验证码
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
},
methods: {
refreshCode() {
this.identifyCode = ''
this.makeCode(this.identifyCodes, 4)
},
makeCode(o, l) {
for (let i = 0; i < l; i++) {
this.identifyCode += this.identifyCodes[this.randomNum(0, this.identifyCodes.length)]
}
},
randomNum(min, max) {
return Math.floor(Math.random() * (max - min) + min)
},
gologin() {
this.$router.push("/login")
}
,
RightDataInput(){
if(this.formRegist.account.length<4){
this.pass='0'
alert("账号长度不得小于4")
}
if(this.formRegist.password.length<8){
this.pass='0'
alert("账号长度不得小于8")
}
if(this.formRegist.account===null || this.formRegist.password===null){
this.pass='0'
this.$message.error('请填写账号或密码')
}
if(this.formRegist.password.toLowerCase() !== this.formRegist.againpassword.toLowerCase()){
this.pass='0'
this.$message.error('密码与上次输入不匹配')
alert('密码与上次输入不匹配')
}
}
,
Register(){
this.axios({
url: "/boot/register",
method: 'post',
headers: { "type": "hello" },
data: {
account: this.formRegist.account,
username: this.formRegist.username,
password: this.formRegist.password.toLowerCase(),
}
}).then(res =>{
this.flag = res.data.flag;
if(this.flag =='1'){
alert("注册成功")
this.$router.push("/login")
}
else {
alert("注册失败!")
}
})
}
,
submitForm() {
this.RightDataInput()
if (this.formRegist.code.toLowerCase() !== this.identifyCode.toLowerCase()) {
this.$message.error('请填写正确验证码')
this.refreshCode()
}
else {
if(this.pass=='1'){
console.log("账号提交注册")
this.Register()
}
}
},
}
}
</script>
<style scoped>
.login-bok{
width: 30%;
margin: 150px auto;
border: 1px solid #DCDFE6;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 30px #DCDFE6;
}
</style>
我这里连函数都没有封装,是很能够看懂的哈。
主页面
这个就是我们的重点了。
首先这里调用了三个接口。
这三个接口一目了然是吧。
流程
不过在开始之前,我们先简单来说说,这玩意的调用流程,这个非常重要。
大概就是这样的,因为我们是有token的,所以我们只需要拿到token就可以去确定用户身份,然后设置session。
token 是为了做七天保持登录,如果用session,浏览器关了就没了,存储在localstage得加个密,所以直接使用token。
websocket
这几个点就不用多说了。
主要是,第一用户来了消息要显示出来,就是那个角标要出来,消息提示要出来。
这个是这样做的。
有个集合,
消息过来了,就
读完了,我就把这编号删掉,这样就搞定了。
那么这个就是websocket
loadmessage
然后是信息加载。这个没啥好说的。
访问之后,把那个数据渲染一下。
消息发送
这个消息发送也简单,主要是修改数据就可以。
完整代码
ok,这个比较主要的点说完了,我们来看看这个完整代码。
<template>
<div>
<div style="width: 70%;height: 500px;margin: 0 auto">
<el-container>
<el-header style="color: white">
<p>欢迎来到随聊</p>
<el-button type="success" round style="position: fixed;left: 70%;top: 12%" @click="loginout">退出登录</el-button>
</el-header>
<div style="height: 40px;width: 100%;background-color: #1794c9">
<p style="margin-right: 85%;color: white">在线聊友</p>
</div>
<el-container>
<el-aside width="200px">
<div style="width: 180px;height: 500px;margin: 20px auto">
<el-row>
<el-card style="height: 70px" shadow="hover" v-for="(user,index) in Users" :key="index">
<el-button v-if="selectId==user.id" type="primary"
style="width: 100%" @click="select(user.id,user.username)"
>
<p v-if="user.id!=currentId">
<i>{{user.username}}</i>
</p>
<p v-else>ME</p>
</el-button>
<el-button v-else type="primary" plain
style="width: 100%" @click="select(user.id,user.username)"
>
<p v-if="user.id!=currentId">
<el-badge v-if="badgeMes(user.id)" value="new" class="item">
<i>{{user.username}}
</i>
</el-badge>
<el-badge v-else>
<i>{{user.username}}</i>
</el-badge>
</p>
<p v-else>ME</p>
</el-button>
</el-card>
</el-row>
</div>
</el-aside>
<el-main>
<el-main class="show" style="width: 90%;margin: 0 auto;height: 350px;
background-color: #f0faff;border: 5px #2981ce;border-radius: 10px;
">
<div >
<!-- 这部分是咱们的对话内容-->
<div style="width: 90%;height: 80px;margin: 0 auto"
v-for="(Message,index) in MessagesList" :key="index"
>
<div v-model="currentId" v-if="currentId!==Message.fromID && selectId==Message.fromID">
<br>
<div style="display:inline-block;width: 10%;
border: 1px solid #14e0bf;;font-size: 3px;border-radius: 100px"
>
<p style="text-align: center;">{{Message.fromName}}:</p>
</div>
<div style="display:inline-block;width: 60%;
border-radius: 10px;border: 1px solid #0c93ef;"
>
<p style="width: 100%;text-align: left">{{Message.message.message}}</p>
</div>
</div>
<div v-model="currentId" v-if="currentId===Message.fromID && selectId==Message.message.toID"
>
<div style="display:inline-block;width: 60%;
border-radius: 10px;border: 1px solid #0c93ef;"
>
<p style="width: 100%;text-align: right">{{Message.message.message}}</p>
</div>
<div style="display:inline-block;width: 10%;
border: 1px solid #14e0bf;;font-size: 3px;border-radius: 100px"
>
<p style="text-align: center;">:{{currentName}}</p>
</div>
</div>
</div>
</div>
</el-main>
<br>
<div style="width: 90%;margin: 0 auto;
background-color: white;border-radius: 5px;
">
<el-input v-model="sendMsg" type="textarea" :rows="3"
placeholder="说点什么吧~"
></el-input>
<br><br>
<el-button style="width: 15%;margin-left: 85%" @click="submit" type="primary">
发送
</el-button>
</div>
</el-main>
</el-container>
</el-container>
</div>
<router-view/>
</div>
</template>
<script>
export default {
name: "main",
data() {
return {
read: new Set(),
lastselectId: -2,
selectId: -1,
currentId: null,
currentName: null,
sendMsg: null,
sending: null,
MessagesList: [
],
Users:[
{"id":1,"username":"小米"},
{"id":2,"username":"小明"},
{"id":3,"username":"小铭"},
{"id":4,"username":"小敏"},
]
}
},
beforeRouteEnter: (to, from, next) => {
console.log("准备进入主页");
let islogin = localStorage.getExpire("token")
if(!islogin){
next({path:'/login'});
}
next();
},
methods:{
freshMyMessage(sendMsg){
// 这个主要是自己发送的消息不会再被服务器转发给自己,需要本地刷新一下
let MyMessage={
"isSystem": false,
"fromID": this.currentId,
"fromName": this.currentName,
"message":sendMsg
}
this.MessagesList.push(MyMessage)
},
badgeMes(ID){
return this.read.has(Number(ID));
},
loadMessage(selectID,userName){
this.axios({
url: "/boot/loadmessage",
method: 'post',
data: {
currentId: this.currentId,
currentName: this.currentName,
selectID: selectID,
userName: userName
}
}).then(res =>{
let data = res.data
this.MessagesList = data
})
},
select(selectID,userName){
this.selectId = selectID
this.read.delete(Number(selectID))
//此时选的是别人,需要调用后端数据库获取以前的聊天记录,并且覆盖掉当前的记录
//为了防止用户恶意点击还需要记录一下是不是点击的同一个人
if(this.selectId==this.currentId) {
this.MessagesList=[]
}
if(this.lastselectId!=this.selectId && this.selectId!=this.currentId){
//执行消息覆盖
//这一部分其实是要访问数据库,来寻找聊天消息的,这里我不想把数据存在本地
//一方面是因为用户端负载,另一方面,换了浏览器就没了,还是要存在数据库里面
this.loadMessage(selectID,userName)
this.lastselectId = this.selectId
}else {
if(this.lastselectId==this.selectId){
alert("正在和当前用户聊天")
}
}
},
submit(){
if(this.selectId==-1){
alert("请选择聊天对象")
return;
}
if(this.sendMsg===null || this.sendMsg.length<1){
alert("请输入您的消息")
return
}if(this.currentId==this.selectId){
alert("不能和自己聊天哟~")
return;
// 选的是自己没用
}
this.sendMessage()
this.sendMsg=null
console.log("已发送信息")
},
loginout(){
localStorage.removeItem("token");
this.$router.push('/login')
}
,
getUserInfo(){
this.axios({
url: "/boot/main",
method: 'get',
headers: { "token": localStorage.getExpire("token") },
}).then(res =>{
let data = res.data
if(data.success == '0'){
alert("数据获取异常,请重新登录!")
this.loginout()
return;
}
if(data.success=='2'){
alert("登录过期请重新登录")
this.loginout()
}
this.currentId = data.currentId
this.currentName = data.currentName
})
},
// 这个是咱们websocket的方法
//建立连接,初始化weosocket
sendMessage() {
let toName = null
for(var i=0;i<this.Users.length;i++){
if(this.Users[i].id==this.selectId){
toName = this.Users[i].username
break
}
}
this.sending={
"toID":this.selectId,
"toName":toName,
"message":this.sendMsg
}
this.freshMyMessage(this.sending)
this.socket.send(JSON.stringify(this.sending));
},
//初始化建立前后台链接
init() {
if (typeof WebSocket === "undefined") {
alert("您的浏览器不支持socket");
} else {
// 实例化socket
this.socket = new WebSocket("ws://localhost:8000/chat");
// 监听socket连接
this.socket.onopen = this.open;
// 监听socket错误信息
this.socket.onerror = this.error;
// 监听socket消息
this.socket.onmessage = this.getMessage;
this.socket.onclose = this.close;
}
},
//链接成功时的回调函数
open() {
console.log("socket连接成功");
},
//链接错误时的回调
error(err) {
console.log("连接错误" + err);
},
//后台消息推送过来,接收的函数,参数为后台推过来的数据。
getMessage(msg) {
let dataJson = JSON.parse(msg.data)
//我们的消息是分为两大类的,一个是系统消息,还有一个是用户消息
if(dataJson.system){
this.Users = dataJson.message
}else {
console.log(dataJson)
this.MessagesList.push(dataJson)
this.read.add(dataJson.fromID)
}
},
//链接关闭的回调
close(event) {
//socket是链接的实例,close就是关闭链接
this.socket.close()
console.log("断开链接成功");
},
},
mounted() {
this.getUserInfo()
this.init()
}
}
</script>
<style scoped>
.el-header, .el-footer {
background-color: #1794c9;
color: #333;
text-align: center;
line-height: 60px;
}
.show:hover{
box-shadow: 0px 15px 30px rgb(30, 136, 225);
margin-top: 20px;
}
</style>
后端
那么现在我们把目光搞到后端部分。
环境
我们先来看到环境部分,这个部分呢,用到真实的依赖其实不多。然后这一次是用mybatis来做数据存储的。 然后使用hashmap来存储在线用户的标识,这样的作用和redis的作用其实一样的,都是直接存在内存里面的,所以说速度是可以的,而且没有连接的延时,不过坏处是,redis可以部署到专门的服务器里面,这个不行,不过理论上,我们可以考虑搞一个服务器专门hashmap存储内容,然后发送连接拿到数据也可以,但是这样的话不如直接使用redis不过,这个也不失为一种想法,因为用这玩意我可以自定义复杂对象。
好了,废话不多说了,我们来看看这个依赖。
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.huterox</groupId>
<artifactId>second</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>second</name>
<description>second</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 热更新的配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
自己看着复制哈(其实这个是我第二次Java作业,所以不方便直接给项目文件)
完整的项目结构长这个样子
配置
server :
port : 8000
spring:
devtools:
restart:
enabled: true
datasource:
druid:
username: Huterox
password: 865989840
url: jdbc:mysql://localhost:3306/second?useSSL=false&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
aop-patterns: com.atguigu.admin.* #springbean监控
filters: stat,wall,slf4j #所有开启的功能
stat-view-servlet: #监控页配置
enabled: true
login-username: admin
login-password: admin
resetEnable: false
web-stat-filter: #web监控
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat: #sql监控
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall: #防火墙
enabled: true
config:
drop-table-allow: false
mybatis:
mapperLocations: classpath:mapper/*.xml
config-location: classpath:mybatis-config.xml
type-aliases-package: com.huterox.second.dap.pojo
其他的几个配置文件是没啥用的
数据库设计
这个呢,我设计了四个数据库,不过用到的只有三个,因为原来想做的是有在线群聊的玩意,后来发现这个前端不好写,所以砍掉了,不过如果你感兴趣拓展其实也很简单,因为所有用户的消息其实我是已经发到前端了,只是前端怎么渲染的问题,怎么设计的问题,然后后端继续加几个接口。这个很简单的,没啥说的。
这个表的关联应该很好看懂吧,引入了几个外键,不过我是没有在数据库里面直接使用外键的,都是在代码层面去做的。
然后是我们对应的pojo类
Dao层及其mapper
然后是写各种增删查改,由于是myabtis所以要那啥,写sql,还是mp香呀。然后为了方便管理,所以我的sql语句都是在xml文件里面的。
由于都是对得到的,所以我们这边就直接把xml文件展示出来
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huterox.second.dao.mapper.FriendMapper">
<!--sql-->
<select id="getAllFriends" resultType="com.huterox.second.dao.pojo.Friend">
select * from friend
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huterox.second.dao.mapper.MessageMapper">
<insert id="addMessage">
INSERT INTO message (message,talkid) VALUES (#{message},#{talkid})
</insert>
<!--sql-->
<select id="getAllMessages" resultType="com.huterox.second.dao.pojo.Message">
select * from message
</select>
<select id="getMessagesByTalkID" resultType="com.huterox.second.dao.pojo.Message">
select * from message where talkid=#{talkid}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huterox.second.dao.mapper.TalkMapper">
<insert id="addTalk">
INSERT INTO talk (mytalk,shetalk) VALUES (#{mytalk},#{shetalk})
</insert>
<!--sql-->
<select id="getAllTalks" resultType="com.huterox.second.dao.pojo.Talk">
select * from talk
</select>
<select id="findTalk" resultType="com.huterox.second.dao.pojo.Talk">
select * from talk where mytalk=#{mytalk} and shetalk=#{shetalk}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huterox.second.dao.mapper.UserMapper">
<insert id="AddUser">
INSERT INTO user (account, username, password) VALUES (#{account},#{username},#{password})
</insert>
<!--sql-->
<select id="getAllUsers" resultType="com.huterox.second.dao.pojo.User">
select * from user
</select>
<select id="selectUserByAccount" resultType="com.huterox.second.dao.pojo.User">
select * from user where account=#{account}
</select>
<select id="selectUserByAccountAndPassword" resultType="com.huterox.second.dao.pojo.User">
select * from user where account=#{account} and password=#{password}
</select>
<select id="selectUserById" resultType="com.huterox.second.dao.pojo.User">
select * from user where id=#{id}
</select>
</mapper>
到这里dao其实就差不多了
Dao相关的服务
服务就三个
package com.huterox.second.server;
import com.huterox.second.dao.mapper.MessageMapper;
import com.huterox.second.dao.pojo.Message;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MessageService {
@Autowired
MessageMapper messageMapper;
public Long SaveMessage(String message,Long talkid){
return messageMapper.addMessage(message, talkid);
}
public List<Message> getMessagesByTalkID(Long talkid){
return messageMapper.getMessagesByTalkID(talkid);
};
}
package com.huterox.second.server;
import com.huterox.second.dao.mapper.TalkMapper;
import com.huterox.second.dao.pojo.Talk;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TalkService {
@Autowired
TalkMapper talkMapper;
public Long getTalkId(Long mytalk,Long shetalk){
Talk talk = talkMapper.findTalk(mytalk, shetalk);
if(talk!=null){
return talk.getTid();
}else {
// 由于我们的主键不是账号,所以没法指定,只能重新查询
talkMapper.addTalk(mytalk,shetalk);
talk = talkMapper.findTalk(mytalk, shetalk);
return talk.getTid();
}
}
}
package com.huterox.second.server;
import com.huterox.second.dao.mapper.UserMapper;
import com.huterox.second.dao.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
UserMapper userMapper;
public User selectUserById(Integer id){return userMapper.selectUserById(id);}
public User selectUserByAccount(String account){
return userMapper.selectUserByAccount(account);
}
public User selectUserByAccountAndPassword(String account,String password){
return userMapper.selectUserByAccountAndPassword(account,password);
}
@Transactional(rollbackFor = Exception.class)
public int addUser(String account,String username,String password) throws Exception {
// 发生异常回滚
int flag = 1;
try {
userMapper.AddUser(account, username, password);
}catch (Exception e){
flag = -1;
throw new Exception("用户添加异常");
}
return flag;
}
}
登录注册实现
交互信息
在此之前,我们还需要明确一下这个后端会返回给前端的数据。
这个很重要
登录注册
ok,前面铺垫了那么多终于到了登录注册这种服务层面了。
首先这个业务层面没什么好说的
package com.huterox.second.controller;
import com.huterox.second.dao.message.LoginMessage;
import com.huterox.second.dao.pojo.User;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.util.Map;
@Controller
@ResponseBody
public class Login {
@Autowired
UserService userService;
@Autowired
LoginMessage loginMessage;
@Autowired
TokenProccessor tokenProccessor;
@PostMapping(value = "/login" )
public LoginMessage login(@RequestBody Map<String,Object> accountMap, HttpSession session){
String account;
String username;
String password;
User user = null;
try {
account = (String) accountMap.get("account");
password = (String) accountMap.get("password");
user = userService.selectUserByAccountAndPassword(account,password);
}catch (Exception e){
e.printStackTrace();
loginMessage.setSuccess(-1);
}
if(user!= null){
// 这里我们把用户的id给前端,后面我们验证这个id就好了
String token = tokenProccessor.createToken(String.valueOf(user.getId()));
loginMessage.setSuccess(1);
loginMessage.setToken(token);
}else {
loginMessage.setSuccess(-1);
}
return loginMessage;
}
}
package com.huterox.second.controller;
import com.huterox.second.dao.message.RegisterMessage;
import com.huterox.second.server.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
@Controller
@ResponseBody
public class Register {
@Autowired
RegisterMessage registerMessage;
@Autowired
UserService userService;
@PostMapping("/register")
public RegisterMessage Register(@RequestBody Map<String, Object> userMap) throws Exception {
String account = (String) userMap.get("account");
String username = (String)userMap.get("username");
String password = (String) userMap.get("password");
if(account!=null && password!=null){
userService.addUser(account,username,password);
registerMessage.setFlag(1);
}else {
registerMessage.setFlag(-1);
}
return registerMessage;
}
}
这样要说的是这个拦截器,和token加密
token生产解析
这个是使用jwt来做的。首先这里封装了一个工具类。
package com.huterox.second.utils;
import com.huterox.second.dao.pojo.User;
import com.huterox.second.server.UserService;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Component
public class TokenProccessor {
@Autowired
UserService userService;
private static final long EXPIRE_TIME=60*60*1000*24*7; //过期时间7天
private static final String KEY = "huterox"; //加密秘钥
/**
* 生成token
* 由于只有当账号密码正确之后才会生成token所以这边只需要用户名进行识别
* @param account 用户账号
* @return
*/
public String createToken(String account){
Map<String,Object> header = new HashMap<String,Object>();
header.put("typ","JWT");
header.put("alg","HS256");
JwtBuilder builder = Jwts.builder().setHeader(header)
.setExpiration(new Date(System.currentTimeMillis()+EXPIRE_TIME))
.setSubject(account)//设置信息,也就是用户名
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,KEY);//加密方式
return builder.compact();
}
/**
* 验证token是否有效
* @param token 请求头中携带的token
* @return token验证结果 2-token过期;1-token认证通过;0-token认证失败
*/
public int verify(String token){
Claims claims = null;
try {
//token过期后,会抛出ExpiredJwtException 异常,通过这个来判定token过期,
claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody();
}catch (ExpiredJwtException e){
return 2;
}
//从token中获取用户名,当用户查询通过后即可
String id = claims.getSubject();
// User user = userService.selectUserById(Integer.parseInt(id));
if(id != null){
return 1;
}else{
return 0;
}
}
public String GetIdByToken(String token){
Claims claims = null;
try {
//token过期后,会抛出ExpiredJwtException 异常,通过这个来判定token过期,
claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
//从token中获取用户名,当用户查询通过后即可
String id = claims.getSubject();
return id;
}
}
拦截器
之后是拦截器
package com.huterox.second.config;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class TokenConfig implements WebMvcConfigurer {
@Autowired
UserService userService;
@Autowired
TokenProccessor tokenProccessor;
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowCredentials(true)
// .allowedHeaders("*")
// .allowedMethods("*")
// .allowedOrigins("*");
//
// }
// 跨域,我们前端做了跨域所以的话后端不用,也是为了安全,不能什么阿猫阿狗都过来访问
@Override
public void addInterceptors(InterceptorRegistry registry){
List<String> excludePath = new ArrayList<>();
//排除拦截,除了注册登录(此时还没token),其他都拦截
excludePath.add("/register"); //登录
excludePath.add("/login"); //注册
excludePath.add("/chat"); //
excludePath.add("/loadmessage");
excludePath.add("/static/**"); //静态资源
excludePath.add("/assets/**"); //静态资源
registry.addInterceptor(new TokenInterceptor(tokenProccessor))
.addPathPatterns("/**")
.excludePathPatterns(excludePath);
WebMvcConfigurer.super.addInterceptors(registry);
}
}
package com.huterox.second.config;
import com.alibaba.fastjson.JSON;
import com.huterox.second.dao.message.TokenInMessage;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenInterceptor implements HandlerInterceptor {
TokenProccessor tokenProccessor;
public TokenInterceptor(TokenProccessor tokenProccessor) {
this.tokenProccessor = tokenProccessor;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{
if(request.getMethod().equals("OPTIONS")){
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
response.setCharacterEncoding("utf-8");
String token = request.getHeader("token");
int result = 0;
if(token != null){
result = tokenProccessor.verify(token);
if(result == 1){
// System.out.println("通过拦截器");
return true;
}
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try{
TokenInMessage tokenInMessage = new TokenInMessage();
tokenInMessage.setSuccess(result);//0表示验证失败,2表示过期
response.getWriter().append(JSON.toJSONString(tokenInMessage));
// System.out.println("认证失败,未通过拦截器");
}catch (Exception e){
e.printStackTrace();
response.sendError(500);
return false;
}
return false;
}
}
到这里的话一个完整的用户登录流程是开发好了。 那么接下来就是基于这个破玩意来干活了
聊天室
现在进入我们的另一个大头。
首先是配置。
package com.huterox.second.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
//注入ServerEndpointExporter bean对象,自动注册使用注解@ServerEndpoint的bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
进入聊天室
进入这个聊天室的话,需要给上session,这样才能辨别用户。
package com.huterox.second.controller;
import com.huterox.second.dao.message.MainMessage;
import com.huterox.second.dao.pojo.User;
import com.huterox.second.server.UserService;
import com.huterox.second.utils.TokenProccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Objects;
@Controller
@ResponseBody
public class Main {
@Autowired
MainMessage mainMessage;
@Autowired
UserService userService;
@Autowired
TokenProccessor tokenProccessor;
@RequestMapping("/main")
public MainMessage main(HttpServletRequest request, HttpSession session){
// 能够进来这个页面说明是已经登录了的
String token = request.getHeader("token");
Long id = (Long) session.getAttribute("id");
String name = (String) session.getAttribute("name");
System.out.println("id:"+id+"-name:"+name+"进入Mian");
if(id!=null && name!=null){
// 如果有人恶意修改session,我也没辙,反正到时候恶意修改后数据可能是拿不到的
mainMessage.setSuccess(1);
mainMessage.setCurrentId(id);
mainMessage.setCurrentName(name);
}else {
User user = userService.selectUserById(
Integer.parseInt(Objects.requireNonNull(tokenProccessor.GetIdByToken(token)))
);
if (user!=null){
// 我们在这里的目的是为了设置session,这样好进行聊天标记
session.setAttribute("id",user.getId());
session.setAttribute("name",user.getUsername());
mainMessage.setSuccess(1);
mainMessage.setCurrentId(user.getId());
mainMessage.setCurrentName(user.getUsername());
}else {
mainMessage.setSuccess(0);
}
}
return mainMessage;
}
}
聊天部分
首先聊天有两个部分,一个是广播所有用户,一个是转发,因为有用户登录的时候需要告诉别的用户。
这个部分,有代码注释,我在这里就不说了。
这里主要使用到两个东西。
一个是让websocket获取session的玩意
package com.huterox.second.websocket;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//获取HttpSession对象
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
还有一个是转化信息的工具类
package com.huterox.second.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huterox.second.dao.message.ResultMessage;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
@Component
public class MessageUtils {
public static String getMessage(boolean isSystemMessage,Long fromID,String fromName,Object message){
try {
ResultMessage result = new ResultMessage();
result.setSystem(isSystemMessage);
result.setFromName(fromName);
result.setMessage(message);
if (fromID!=null){
result.setFromID(fromID);
}
//把字符串转成json格式的字符串
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(result);
}catch (JsonProcessingException e){
e.printStackTrace();
}
return null;
}
}
当然还有存储的服务,不过这个在service里面做了。
完整代码
ok,现在看看完整代码
package com.huterox.second.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huterox.second.dao.message.MessageSend;
import com.huterox.second.server.MessageService;
import com.huterox.second.server.TalkService;
import com.huterox.second.utils.MessageUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
public class ChatEndpoint {
private static TalkService talkService;
private static MessageService messageService;
@Autowired
public void setTalkService(TalkService talkService){
ChatEndpoint.talkService = talkService;
}
@Autowired
public void setMessageService(MessageService messageService){
ChatEndpoint.messageService = messageService;
}
//用来存储每个用户客户端对象的ChatEndpoint对象
private static Map<Long,ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();
private static Map<Long,String> onlineUserNames = new ConcurrentHashMap<>();
// 用来存储talkID 的这样只需要查询一次数据库就可以拿到talkID了,加快速度
private static Map<String,Long> talkID = new ConcurrentHashMap<>();
//声明session对象,通过对象可以发送消息给指定的用户
private Session session;
//声明HttpSession对象,我们之前在HttpSession对象中存储了用户id
private HttpSession httpSession;
//连接建立
@OnOpen
public void onOpen(Session session, EndpointConfig config){
this.session = session;
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
//存储登陆的对象
Long userID = (Long) httpSession.getAttribute("id");
String name = (String) httpSession.getAttribute("name");
System.out.println("id:"+userID+"-name:"+name+"进入聊天室");
onlineUsers.put(userID,this);
onlineUserNames.put(userID,name);
//将当前在线用户的用户名推送给所有的客户端
//1 获取消息
String message = MessageUtils.getMessage(true, null, null,getUsers());
//2 调用方法进行系统消息的推送
broadcastAllUsers(message);
}
private void broadcastAllUsers(String message){
try {
//将消息推送给所有的客户端
Set<Long> IDS = onlineUsers.keySet();
for (Long ID : IDS) {
ChatEndpoint chatEndpoint = onlineUsers.get(ID);
chatEndpoint.session.getBasicRemote().sendText(message);
}
}catch (Exception e){
e.printStackTrace();
}
}
//返回在线用户ID
private Set<Long> getId(){
return onlineUsers.keySet();
}
//返回在线用户名
// {"id":4,"username":"小敏"},
private Set<Map<String,String>> getUsers(){
Set<Map<String,String>> set = new HashSet<>();
for (Map.Entry<Long, String> entry : onlineUserNames.entrySet()) {
Map<String,String> temp = new HashMap<>();
temp.put("id",String.valueOf(entry.getKey()));
temp.put("username",entry.getValue());
set.add(temp);
}
return set;
}
private Long getTalkID(Long mytalk,Long shetalk){
String Key = mytalk+"to"+shetalk;
if (talkID.get(Key)!=null){
return talkID.get(Key);
}else {
Long talkId = talkService.getTalkId(mytalk, shetalk);
talkID.put(Key,talkId);
return talkId;
}
}
private Long SaveMessage(Long mytalk,Long shetalk,String message){
Long talkID = this.getTalkID(mytalk, shetalk);
return messageService.SaveMessage(message, talkID);
}
//收到消息,并且我们需要把消息存储到数据库内
@OnMessage
public void onMessage(String message, Session session){
//将数据转换成对象
try {
ObjectMapper mapper =new ObjectMapper();
System.out.println(message);
MessageSend mess = mapper.readValue(message, MessageSend.class);
Long toID = mess.getToID();
String toName = mess.getToName();
String data = mess.getMessage();
Long userID = (Long) httpSession.getAttribute("id");
String userName = (String) httpSession.getAttribute("name");
// 在这里存储消息
this.SaveMessage(userID,toID,data);
System.out.println(mess);
String resultMessage = MessageUtils.getMessage(false, userID,userName,mess);
//发送数据
if(toID!=null) {
// 发送的数据长这个样子
// {"isSystem": false,
// "fromID": 2,
// "message":{"toID":1,"toName":"Futerox","message":"Hello"}
// }
onlineUsers.get(toID).session.getBasicRemote().sendText(resultMessage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//关闭
@OnClose
public void onClose(Session session) {
Long userID = (Long) httpSession.getAttribute("id");
//从容器中删除指定的用户
onlineUsers.remove(userID);
onlineUserNames.remove(userID);
String message = MessageUtils.getMessage(true,null,null,getUsers());
broadcastAllUsers(message);
}}
聊天信息加载
终于到了最后一步了,聊天信息的加载。
这一步主要是靠这玩意
代码如下:
package com.huterox.second.utils;
import com.huterox.second.dao.message.MessageSend;
import com.huterox.second.dao.message.ResultMessage;
import com.huterox.second.dao.pojo.Message;
import com.huterox.second.server.MessageService;
import com.huterox.second.server.TalkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Component
public class LoadMessageUtils {
//这个玩意主要是拿过来返回消息列表的,方便加载
// 加载的消息有两方面,一个是你自己和别人说的,另一个是别人跟你说的,所以需要两个查询
private static TalkService talkService;
private static MessageService messageService;
@Autowired
public void setTalkService(TalkService talkService){
LoadMessageUtils.talkService = talkService;
}
@Autowired
public void setMessageService(MessageService messageService){
LoadMessageUtils.messageService = messageService;
}
public static List<ResultMessage> LoadMessages(Long mytalk,Long shetalk,
String myName,String sheName){
// 我们的写操作要比读操作更多,所以不用CopyOnWriteArrayList<>();
List<ResultMessage> resultMessages= Collections.synchronizedList(new ArrayList<ResultMessage>());
Long mytalkID = talkService.getTalkId(mytalk,shetalk);
Long shetalkID = talkService.getTalkId(shetalk,mytalk);
if(mytalkID!=null){
addMessages(mytalkID,resultMessages,myName,sheName,mytalk,shetalk);
}
if (shetalkID!=null){
addMessages(shetalkID,resultMessages,sheName,myName,shetalk,mytalk);
}
return resultMessages;
}
private static void addMessages(Long talkID,List<ResultMessage> LoadMessages,
String fromName,String toName,Long mytalk,Long shetalk){
List<Message> messagesByTalkID = messageService.getMessagesByTalkID(talkID);
for (Message message : messagesByTalkID) {
ResultMessage resultMessage_ = new ResultMessage();
MessageSend messageSend_ = new MessageSend();
resultMessage_.setFromID(mytalk);
resultMessage_.setFromName(fromName);
messageSend_.setMessage(message.getMessage());
messageSend_.setToID(shetalk);
messageSend_.setToName(toName);
resultMessage_.setMessage(messageSend_);
LoadMessages.add(resultMessage_);
}
}
}
总结
到这里的话,认认真真看这篇博文的话,应该是可以直接复刻这个项目的,毕竟这个底裤都拿出来了,这dome。 然后优化的是有很多优化的。对了这里就不得不说这个一个小bug了,这个是前端的问题,就是这个页面在chrome浏览器是没有啥问题的,但是在Firefox,这个用户名渲染会出现问题。其他的还好说。