WebSocket高級
效果展示
用户登录界面
登录成功后的聊天界面(发送消息给指定用户)
一对一消息推送
用户接收消息界面
用户接收到消息后会显示接收到的并且未读消息条数(未查看才会有)
当点击查看了消息,未读条数将会消失
此时便实现了一对一消息的发送和接收
右下角通过群发的模式,将消息推送给所有在线的用户
项目目录及代码展示
SpringBoot版本号为 2.1.5.RELEASE
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
项目的pom文件
<!-- springboot配置信息 -->
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!-- springboot-Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 热部署依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- 这个需要为 true 热部署才有效 -->
<scope>true</scope>
</dependency>
<!-- ****************************以下工具为非必须**************************** -->
<!-- lang3工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- hutool工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<!-- slf4j日志处理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.55</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.2.0</version>
</dependency>
<!-- SpringBoot整合MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!-- redis模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- JWT工具 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- httpclient数据加密工具 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打jar包时 如果 不配置该插件,打出来的jar包没有清单文件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
项目中的配置文件
###########################【基础配置】#########################
spring.banner.charset=UTF-8
spring.messages.encoding=UTF-8
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.force=false
server.servlet.encoding.enabled=false
server.port=80
spring.application.name=ws
server.servlet.session.timeout=3600s
###########################【MySQL配置】#########################
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://110.42.177.172:3306/wsapp?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
###########################【MyBatis】#########################
mybatis.mapper-locations=classpath*:mapper/*.xml
mybatis.type-aliases-package=cn.molu.app.pojo
###########################【MyBatis-Plus】#########################
mybatis-plus.mapper-locations=classpath*:mapper/*.xml
mybatis-plus.type-aliases-package=cn.molu.ws.pojo
# 表名前缀
mybatis-plus.global-config.db-config.table-prefix=tb_
# id策略为自增长
mybatis-plus.global-config.db-config.id-type=auto
###########################【Web】#########################
# 静态资源访问权限
spring.web.resources.static-locations=classpath:/resources/,classpath:/static/,classpath:/templates/
#spring.mvc.servlet.path=classpath:/static/views
spring.thymeleaf.cache=false
spring.thymeleaf.check-template=true
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5
# mvc静态资源配置 thymeleaf 前后缀 默认静态页面是在resource/templeats/ 下面的资源
spring.mvc.static-path-pattern=/**
# thymeleaf默认在templates下
# spring.mvc.view.prefix=/templeats/
# thymeleaf默认后缀为.html
# spring.mvc.view.suffix=.html
###########################【Redis】#########################
# Redis数据库索引(默认为0)
spring.redis.database=1
# Redis服务器地址
spring.redis.host=110.42.177.172
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=moluroot
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-idle=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=5000ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒) 30s
spring.redis.timeout=30000ms
###########################【Jpa】#########################
# 让hibernate的sql语句显示出来,这样才知道到底是通过 Redis 取到的数据,还是依然是从数据库取到的数据
spring.jpa.show-sql=true
###########################【JWT盐值】#########################
#盐 值 jwt单点登录盐值 生成密文token 根据需求生成MD5密文盐值,该盐值是UUID串
jwt.secret=2d31f8324db94f99b37fdd16c4ac787a
###########################【热部署】#########################
# 重启目录
spring.devtools.restart.additional-paths=src/main/java
# 设置开启热部署
spring.devtools.restart.enabled=true
# 设置字符集
spring.freemarker.charset=utf-8
# 页面不加载缓存,修改后立即生效
spring.freemarker.cache=false
java中的配置类
配置HttpSession对象,方便在WebSocket核心类中使用 HttpSession
import java.util.Map;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
/**
* @tite 用来获取HttpSession对象.
* @author 陌路
* @date 2022-04-16 上午12:27:38
*/
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
/**
* @title 该配置可以在不手动传入HttpSession的情况下在websocket服务类中使用
*/
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request,
HandshakeResponse response) {
// 获取httpsession对象
HttpSession httpSession = (HttpSession) request.getHttpSession();
// 存放httpsession对象
Map<String, Object> userProperties = sec.getUserProperties();
if (httpSession == null || HttpSession.class == null) {
return;
}
userProperties.put(HttpSession.class.getName(), httpSession);
}
}
Redis序列化配置
对存储在redis中的数据进行序列化的统一配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis序列化配置类
* @author 陌路
* @date 2022-05-02 上午1:55:50
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lcf) {
RedisTemplate<String, Object> restTemplate = new RedisTemplate<String, Object>();
// 为String类型的key设置序列化
restTemplate.setKeySerializer(new StringRedisSerializer());
// 为String类型的value设置序列化
restTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 为Hash类型的key设置序列化
restTemplate.setHashKeySerializer(new StringRedisSerializer());
// 为Hash类型的value设置序列化
restTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
restTemplate.setConnectionFactory(lcf);
return restTemplate;
}
}
WebConfig配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @title web配置类 设置默认访问页面
* @author 陌路
* @date 2021/8/15
* @apiNote 配置默认页面
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 自定义静态路径,spring.resource.static-locations=classpath:/static/
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("/chat/login");
}
}
WebSocket核心配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @Desc 首先需要注入 ServerEndpointExporter , 这个bean会自动注册使用 @ServerEndpoint
* 的注解来声明WebSocket endpoint。
* 注意:如果使用独立的Servlet容器,而不是直接使用SpringBoot内置容器,就不需要注入
* ServerEndpointExporter,因为他将有容器自己提供和管理。
* @author 陌路
* @date 2022-04-16
*/
@Configuration
public class WebSocketConfig {
/**
* @title 扫描注册使用 @ServerEndpoint 注解的类
* @return ServerEndpointExporter
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WS中使用Redis
在WebSocket核心类中无法直接使用Redis工具类,需要使用以下配置类
在WebSocket核心类中需要使用
SpringUtils.getBean(RedisUtils.class);
来获取Redis工具类对象
private RedisUtils redisUtils = SpringUtils.getBean(RedisUtils.class);
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
@Component
public final class SpringUtils implements BeanFactoryPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringUtils.class);
private static ConfigurableListableBeanFactory beanFactory;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
SpringUtils.beanFactory = beanFactory;
}
public static ConfigurableListableBeanFactory getBeanFactory() {
return beanFactory;
}
/**
* 获取对象
* @return Object 一个以所给名字注册的bean的实例
* @throws org.springframework.beans.BeansException
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
if (getBeanFactory() == null) {
//zhengkai.blog.csdn.net
LOGGER.info("本地调试Main模式,没有BeanFactory,忽略错误");
return null;
} else {
T result = (T) getBeanFactory().getBean(name);
return result;
}
}
/**
* 获取类型为requiredType的对象
* @throws org.springframework.beans.BeansException
*/
public static <T> T getBean(Class<T> name) throws BeansException {
if (getBeanFactory() == null) {
LOGGER.info("本地调试Main模式,没有BeanFactory,忽略错误");
return null;
} else {
T result = (T) getBeanFactory().getBean(name);
return result;
}
}
/**
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
*/
public static boolean containsBean(String name) {
return getBeanFactory().containsBean(name);
}
/**
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
* 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().isSingleton(name);
}
/**
* @return Class 注册对象的类型
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().getType(name);
}
/**
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
return getBeanFactory().getAliases(name);
}
}
User实体类对象
/**
* (User)实体类
* @author 陌路
* @since 2022-04-21 16:53:50
*/
@Data
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 477020946096486016L;
/** 用户id */
private Integer id;
/** 用户名 */
private String username;
/** 用户手机号 */
private String phone;
/** 用户密码 */
private String password;
}
Result数据返回对象
/**
* @Desc 数据的响应类.
* @author 陌路
* @date 2022-04-16 上午10:53:15
*/
@Data
public class Result {
/** 响应标志,成功/失败 */
private boolean flag;
/** 响应给前台的消息 */
private String message;
/** 用户名 */
private String username;
/** 用户id */
private String userId;
/** 日期时间 */
private String dateStr;
}
消息返回实体对象
/**
* 服务端发送给客户端的消息.
*/
@Data
public class ResultMessage {
/** 是否是系统消息 */
private Boolean systemMsgFlag;
/** 发送方姓名 */
private String fromName;
/** 发送方id */
private String fromId;
/** 接收方姓名 */
private String toName;
/** 接收方id */
private String toId;
/** 发送的数据 */
private Object message;
/** 接收到消息的日期时间 */
private String dateStr;
/** 存储对象数据 */
private Map<?, ?> map;
}
接收消息实体对象
/**
* @Desc 浏览器发送给服务器的websocket数据.
*/
@Data
public class Message {
/** 发送方姓名 */
private String fromName;
/** 发送方id */
private String fromId;
/** 接收方 */
private String toName;
/** 接收方id */
private String toId;
/** 发送的数据(接收到的数据) */
private String message;
/** 未读消息的数量 */
private int count;
/** 接收到消息的日期时间 */
private String dateStr;
}
Mapper对象
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import cn.molu.app.pojo.User;
/**
* (User)表数据库访问层
* @author 陌路
* @since 2022-04-21 16:55:04
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
Service接口类
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import cn.molu.app.pojo.User;
import cn.molu.app.vo.R;
/**
* (User)表服务接口
* @author 陌路
* @since 2022-04-21 16:55:04
*/
public interface UserService {
/**
* 用户登录
* @return R
*/
R login(String phone, String password, Map<String, Object> params, HttpServletResponse res);
User queryUserByToken(String token);
String getToken(User user) throws Exception;
}
Service接口实现类
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.molu.app.mapper.UserMapper;
import cn.molu.app.pojo.User;
import cn.molu.app.service.UserService;
import cn.molu.app.utils.ObjectUtils;
import cn.molu.app.utils.RedisUtils;
import cn.molu.app.vo.R;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* (User)表服务实现类
* @author 陌路
* @since 2022-04-21 16:55:04
*/
@Service("userService")
public class UserServiceImpl implements UserService {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private UserMapper userMapper;
@Resource
private RedisUtils redisUtils;
@Value("${jwt.secret}")
private String secret;
@Override
public R login(String phone, String password, Map<String, Object> params, HttpServletResponse res) {
User user = userMapper.selectOne(new QueryWrapper<User>().eq("phone", phone).eq("deleted", "0"));
ObjectUtils.checkNull(res, user, String.format("未获取到%s的数据信息!", phone));
String md5Pwd = DigestUtils.md5Hex(secret + password);
if (!StringUtils.equals(md5Pwd, user.getPassword())) {
return R.err("密码输入错误!");
}
user.setPassword("");
String token = getToken(user);
String userId = ObjectUtils.getStr(user.getId());
this.redisUtils.setObj(userId, user, Duration.ofDays(2));
return R.ok().put("token", token);
}
/**
* 解析token,获取user数据信息 对token进行检测,如果token存在,则解析出user数据信息
* 如果token不存在,则return null
* 除注册和发送验证码外不需要检测token外,其他功能均需要检测token
*/
@Override
public User queryUserByToken(String token) {
try {
String redisTokenKey = "TOKEN_" + token;
String cacheData = ObjectUtils.getStr(this.redisTemplate.opsForValue().get(redisTokenKey));
if (StringUtils.isNotEmpty(cacheData)) {
this.redisTemplate.expire(redisTokenKey, 1, TimeUnit.HOURS);
return MAPPER.readValue(cacheData, User.class);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 生成Token,并把Token放到Redis中
*/
@Override
public String getToken(User user) {
String token = "";
Map<String, Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("name", user.getUsername());
claims.put("phone", user.getPhone());
claims.put("userCode", user.getUserCode());
token = Jwts.builder().setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
String redisTokenKey = "TOKEN_" + token;
try {
this.redisTemplate.opsForValue().set(redisTokenKey, MAPPER.writeValueAsString(user), Duration.ofHours(3));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
String loginState = "LOGIN_STATE_" + user.getPhone();
String stateVal = "1&@&" + redisTokenKey;
this.redisTemplate.opsForValue().set(loginState, stateVal, Duration.ofDays(2));
return token;
}
}
后台访问接口类
import java.time.Duration;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import cn.hutool.core.lang.Validator;
import cn.molu.app.pojo.User;
import cn.molu.app.service.UserService;
import cn.molu.app.utils.ObjectUtils;
import cn.molu.app.utils.RedisUtils;
import cn.molu.app.vo.R;
@Controller
@RequestMapping("index")
public class LoginController {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
@Resource
private UserService userService;
@Resource
private RedisUtils redisUtils;
/**
* 处理用户登录请求.
* @param params 用户登录数据
* @param resp
*/
@ResponseBody
@PostMapping("/login")
public R login(@RequestParam Map<String, Object> params,
HttpServletRequest req,
HttpServletResponse res) {
ObjectUtils.checkNull(res, params, "手机号和密码不能为空...");
String phone = ObjectUtils.getStr(params.get("phone"));
String pwd = ObjectUtils.getStr(params.get("password"));
ObjectUtils.checkNull(res, phone, pwd, "手机号或密码输入错误...");
boolean isMobile = Validator.isMobile(phone);
if (!isMobile) {
return R.err("手机号格式错误...");
}
return this.userService.login(phone, pwd, params, res);
}
/**
* 登录成功后跳转到聊天页面.
* @return String 跳转到聊天室,如果没有登录,则返回登录页面
*/
@GetMapping("/toChatroom/{token}")
public ModelAndView toChatroom(@PathVariable("token") String token, HttpSession session) {
ModelAndView mv = null;
if (ObjectUtils.isEmpty(token)) {
return new ModelAndView("/chat/login");
}
User user = this.userService.queryUserByToken(token);
// token过期
if (ObjectUtils.isEmpty(user)) {
return new ModelAndView("/chat/login");
}
String userId = ObjectUtils.getStr(user.getId());
String username = user.getUsername();
mv = new ModelAndView("/views/chat");
mv.addObject("name", username);
mv.addObject("id", userId);
redisUtils.setObj(userId, user, Duration.ofDays(1));
return mv;
}
// 清除未读消息的条数
@ResponseBody
@PostMapping("/clearCount")
public void setCount(HttpServletRequest req, HttpServletResponse res) {
String fromId = req.getParameter("fromId");
String userId = req.getParameter("userId");
if (StringUtils.isNotBlank(fromId) && StringUtils.isNotBlank(userId)) {
try {
String redisCountKey = "UN_READ_MSG_COUT_" + fromId + "_" + userId;
if (this.redisUtils.isExists(redisCountKey)) {
this.redisUtils.remove(redisCountKey);
}
ObjectUtils.printJsonMsg(res, true, "清除成功", 0);
} catch (Exception e) {
LOGGER.info("清除未读消息时出现异常...{}", e.getMessage());
e.printStackTrace();
}
}
}
// 设置未读消息条数
@ResponseBody
@PostMapping("/setCount")
public void getCount(HttpServletRequest req, HttpServletResponse res) {
String fromId = req.getParameter("fromId");
String userId = req.getParameter("userId");
String count = ObjectUtils.getStr(req.getParameter("count"));
if (StringUtils.isNotBlank(fromId) && StringUtils.isNotBlank(userId)) {
try {
count = ObjectUtils.getStr(Integer.valueOf(count) + 1);
String redisCountKey = "UN_READ_MSG_COUT_" + fromId + "_" + userId;
this.redisUtils.setStr(redisCountKey, count, Duration.ofDays(7));// 有效期为7天
ObjectUtils.printJsonMsg(res, true, "设置成功", count);
} catch (Exception e) {
LOGGER.info("获取未读消息时出现异常...{}", e.getMessage());
e.printStackTrace();
}
}
}
}
Object工具类封装
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializeConfig;
import cn.molu.app.handle.BusinessException;
import cn.molu.app.vo.ResponseVo;
import lombok.extern.slf4j.Slf4j;
/**
* 对象工具类
*
* @Description
* @author 陌路
* @date 2022-07-22 下午6:50:47
*/
@Slf4j
public class ObjUtils {
/**
* 获取字符串数据
*
* @desc 对象转为字符串
* @param obj
* @return String
*/
public static String getStr(Object obj) {
if (null == obj)
return "";
if (obj instanceof String)
if ("".equals(obj) || obj.toString().trim().length() < 1 || StringUtils.isBlank((String) obj))
return "";
return obj.toString().trim();
}
/**
* 判断对象是否为空
*
* @desc 仅当对象为null或者没值时返回true
* @param obj
* @return boolean
*/
public static boolean isEmpty(Object obj) {
if (null == obj)
return true;
if (obj instanceof String)
return StringUtils.isBlank(getStr(obj));
if (obj instanceof Collection<?>)
return ((Collection<?>) obj).isEmpty();
if (obj instanceof Map<?, ?>)
return ((Map<?, ?>) obj).isEmpty();
if (obj instanceof Object[])
return Arrays.asList((Object[]) obj).isEmpty();
return ObjectUtils.isEmpty(obj);
}
/**
* 判断集合数据内容是否为空
*
* @desc 仅当所有值都为null||""时返回true
* @param c
* @return boolean
*/
public static boolean collIsBlank(Collection<?> c) {
if (null == c || c.isEmpty())
return true;
int i = 0;
for (Object v : c = new HashSet<>(c)) {
if (isEmpty(v))
i++;
}
return c.size() == i;
}
/**
* 判断Map集合内容是否为空
*
* @desc 仅当所有值都为null||""时返回true
* @param m
* @return boolean
*/
public static boolean mapIsBlank(Map<?, ?> m) {
if (null == m || m.isEmpty())
return true;
if (m.keySet().isEmpty())
return true;
return collIsBlank(m.values());
}
/**
* @desc 仅当有一个值都为null||""时返回true
* @param obj
* @return boolean
*/
public static boolean isEmpty(Object... obj) {
if (null == obj)
return true;
for (Object o : obj) {
if (StringUtils.isBlank(getStr(o)))
return true;
}
return false;
}
/**
* JSON数据转Map集合
*
* @param json
* @return Map<?,?>
*/
public static Map<?, ?> jsonToMap(String s) {
if (StringUtils.isBlank(s))
return new HashMap<String, String>();
return JSONObject.parseObject(s, Map.class);
}
/**
* Map集合转Object
*
* @return T
*/
public static <T> T mapToObj(Map<String, Object> map, Class<T> obj) {
if (null == obj || null == map || map.isEmpty())
return null;
return JSONObject.parseObject(toJSON(map), obj);
}
/**
* @title 对象数据转JSONString数据
* @Desc 实现了 SerializeConfig.globalInstance
*/
public static String toJSON(Object obj) {
if (isEmpty(obj))
return "";
return getStr(JSONObject.toJSON(obj, SerializeConfig.globalInstance));
}
/**
* JSON数据转对象
*
* @return T
*/
public static <T> T toObj(String jsonStr, Class<T> obj) {
if (StringUtils.isBlank(jsonStr) || null != obj)
return null;
return JSONObject.parseObject(jsonStr, obj);
}
/**
* 数据检验,有一个值为空则结束执行,并抛出errMsg异常
*
* @desc 为true时校验通过
* @return boolean
*/
public static boolean checkData(String errMsg, Object... params) {
if (StringUtils.isBlank(errMsg))
errMsg = ResponseVo.PARAM_ERR_STR;
if (null == params || isEmpty(params)) {
log.error("数据校验失败。。。");
throw new BusinessException(errMsg);
}
return true;
}
/**
* 比较两个对象是否相等
*
* @return boolean
*/
public static boolean equals(Object o1, Object o2) {
if (StringUtils.equals(getStr(o1), getStr(o2)) || o1.equals(o2))
return true;
return false;
}
/**
* 比较两个对象是否相等
*
* @return boolean
*/
public static boolean equalsIgnoreCase(Object o1, Object o2) {
if (StringUtils.equalsIgnoreCase(getStr(o1), getStr(o2)))
return true;
return false;
}
/**
* 把Map集合转为XML字符串
*
* @desc Map的Key和Value必须为String类型
* @param m
* @return String
*/
public static String mapToXML(Map<String, String> m) {
if (isEmpty(m))
return "";
StringBuffer sbf = new StringBuffer();
m.forEach((k, v) -> {
if (!isEmpty(k))
sbf.append(String.format("<%s>%s</%s>", k, getStr(v), k));
});
return sbf.toString();
}
/**
* 替换字符串中的?
*
* @param str
* @param val
* @return String
*/
public static String getVal(String str, String... val) {
String format = "";
for (String v : val) {
format = String.format(str, v);
}
return format;
}
/**
* 获取格式化日期 y-M-d H:m:s
*
* @return String
*/
public static String dateFormat() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
/**
* @Title 格式化日期
* @return String
*/
public static String dateFormat(Date date, String pattern) {
date = "".equals(getStr(date)) ? new Date() : date;
pattern = "".equals(getStr(pattern)) ? "yyyy-MM-dd HH:mm:ss" : pattern;
return new SimpleDateFormat(pattern).format(date);
}
/**
* 获取用户真实IP地址
*
* @param request
* @desc 不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
* 如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,哪个才是真实IP?
* 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
* 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130,
* 192.168.1.100 用户真实IP为: 192.168.1.110
*/
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("Proxy-Client-IP");
if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("WL-Proxy-Client-IP");
if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("HTTP_CLIENT_IP");
if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
if (isEmpty(ip) || "unknown".equalsIgnoreCase(ip))
ip = request.getRemoteAddr();
if (ip.replace(" ", "").replace("unknown,", "").indexOf(",") > 0)
ip = ip.substring(0, ip.indexOf(","));
return getStr(ip);
}
}
RedisUtils工具类
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
/**
* redis工具类
* @Description
* @author 陌路
* @date 2022-05-02 下午1:53:20
*/
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/******************************* String *******************************/
/**
* @title 存储为JSON类型,并设置失效时间
* @param Duration.ofDays(0); 天
* @param Duration.ofHours(0L); 小时
* @param Duration.ofMinutes(0L); 分钟
* @param Duration.ofSeconds(0L); 秒
* @param Duration.ofMillis(0L); 毫秒
*/
public void setJsonObj(String key, Object obj, Duration timeout) {
String jsonStr = ObjectUtils.toJSON(obj);
if (ObjectUtils.isEmpty(timeout)) {
this.redisTemplate.opsForValue().set(key, jsonStr);
} else {
this.redisTemplate.opsForValue().set(key, jsonStr, timeout);
}
}
/**
* @title 存储为对象类型,并设置失效时间
* @param Duration.ofDays(0); 天
* @param Duration.ofHours(0L); 小时
* @param Duration.ofMinutes(0L); 分钟
* @param Duration.ofSeconds(0L); 秒
* @param Duration.ofMillis(0L); 毫秒
*/
public void setObj(String key, Object obj, Duration timeout) {
if (ObjectUtils.isEmpty(timeout)) {
this.redisTemplate.opsForValue().set(key, obj);
} else {
this.redisTemplate.opsForValue().set(key, obj, timeout);
}
}
/**
* @title 存储为Long类型,并设置失效时间
* @param Duration.ofDays(0); 天
* @param Duration.ofHours(0L); 小时
* @param Duration.ofMinutes(0L); 分钟
* @param Duration.ofSeconds(0L); 秒
* @param Duration.ofMillis(0L); 毫秒
*/
public void setLong(String key, Long num, Duration timeout) {
this.redisTemplate.opsForValue().set(key, num, timeout);
}
/**
* @title 存储为String类型,并设置失效时间
* @param Duration.ofDays(0); 天
* @param Duration.ofHours(0L); 小时
* @param Duration.ofMinutes(0L); 分钟
* @param Duration.ofSeconds(0L); 秒
* @param Duration.ofMillis(0L); 毫秒
*/
public void setStr(String key, String val, Duration timeout) {
this.redisTemplate.opsForValue().set(key, val, timeout);
}
/**
* @title 存储多条数据
* @param map.put("name", "zs"); map.put("age", 18); // key - val
*/
public void setMap(Map<String, Object> map) {
this.redisTemplate.opsForValue().multiSet(map);
}
/**
* @title 获取多条数据
* @param keys.add("name"); keys.add("age"); // 需要获取的key
*/
public List<Object> getMap(Collection<String> keys) {
List<Object> multiGet = this.redisTemplate.opsForValue().multiGet(keys);
return multiGet;
}
/**
* @title 存储为Long类型
*/
public void setLong(String key, Long num) {
setLong(key, num, null);
}
/**
* @title 存储为对象类型
*/
public void setObj(String key, Object obj) {
setObj(key, obj, null);
}
/**
* @title 存储为JSON类型
*/
public void setJsonObj(String key, Object obj) {
setJsonObj(key, obj, null);
}
/**
* @title 存储为String类型
*/
public void setStr(String key, String val) {
setJsonObj(key, val);
}
/**
* @title 根据key获取String
*/
public String getStr(String key) {
return ObjectUtils.getStr(this.redisTemplate.opsForValue().get(key));
}
/**
* @title 根据key获取JSON
*/
public String getJson(String key) {
return getStr(key);
}
/**
* @title 根据key获取Object对象
*/
public Object getObj(String key) {
return ObjectUtils.getObj(this.redisTemplate.opsForValue().get(key));
}
/**
* @title 根据key删除数据
*/
public boolean remove(String key) {
if (ObjectUtils.isEmpty(key)) {
return false;
}
return this.redisTemplate.delete(key);
}
/**
* @title 删除多条缓存数据
*/
public boolean remove(Collection<String> keys) {
Long del = this.redisTemplate.delete(keys);
if (del > 0) {
return true;
}
return false;
}
/**
* @title 删除多条缓存数据
*/
public boolean remove(String... keys) {
if (ObjectUtils.checkArr(keys)) {
return false;
}
Set<String> set = new HashSet<String>(Arrays.asList(keys));
return remove(set);
}
/******************************* List *******************************/
/**
* @title list->左添加
*/
public void lPush(String key, Object val) {
this.redisTemplate.opsForList().leftPush(key, val);
}
/**
* @title list->左添加,添加多条数据
* @return 添加条数
*/
public Long lPushAll(String key, Object... val) {
Long count = this.redisTemplate.opsForList().leftPushAll(key, val);
if (ObjectUtils.isBlank(count)) {
return 0L;
}
return count;
}
/**
* @title list->左添加,将newVal添加到existVal的左边
* @Desc 左添加 第三个参数会被添加到第二个参数的左边
* @return 添加条数
*/
public Long lPush(String key, Object existVal, Object newVal) {
Long count = this.redisTemplate.opsForList().leftPush(key, existVal, newVal);
return count;
}
/**
* @title list->右添加
*/
public void rPush(String key, Object val) {
this.redisTemplate.opsForList().rightPush(key, val);
}
/**
* @title list->右添加,添加多条数据
* @return 添加条数
*/
public Long rPushAll(String key, Object... val) {
Long count = this.redisTemplate.opsForList().rightPushAll(key, val);
if (ObjectUtils.isBlank(count)) {
return 0L;
}
return count;
}
/**
* @title list->右添加,将newVal添加到existVal的右边
* @Desc 右添加 第三个参数会被添加到第二个参数的右边
* @return 添加条数
*/
public Long rPush(String key, Object existVal, Object newVal) {
Long count = this.redisTemplate.opsForList().rightPush(key, existVal, newVal);
return count;
}
/**
* @title list->获取指定索引位置的数据
* @Desc redis的key,开始索引,结束索引
* @return 获取到的数据集合
*/
public List<Object> getList(String key, long startIndex, long endIndex) {
List<Object> list = this.redisTemplate.opsForList().range(key, startIndex, endIndex);
return list;
}
/**
* @title list->获取List中的所有数据
* @return 获取到的数据集合
*/
public List<Object> getListAll(String key) {
List<Object> list = this.redisTemplate.opsForList().range(key, 0, -1);
return list;
}
@SuppressWarnings("unchecked")
public <T> T getListAlls(String key) {
return (T) this.redisTemplate.opsForList().range(key, 0, -1);
}
/**
* @title list->获取List中的数据条数
* @return 数据条数
*/
public Long getListSize(String key) {
Long size = this.redisTemplate.opsForList().size(key);
return ObjectUtils.isBlank(size) ? 0L : size;
}
/**
* @title list->删除List中的数据
* @param redis的key,count删除的条数,val删除的值
* @return 删除的数据条数
*/
public Long removeList(String key, Long count, Object val) {
Long rem = this.redisTemplate.opsForList().remove(key, count, val);
return ObjectUtils.isBlank(rem) ? 0L : rem;
}
/**
* @title list->删除List中的数据
* @param redis的key,val删除的值,默认只删除一条
* @return 删除成功返回true,删除失败返回false
*/
public boolean removeList(String key, Object val) {
Long rem = this.redisTemplate.opsForList().remove(key, 1, val);
return ObjectUtils.isBlank(rem);
}
/**
* @title list->删除左边的第一条数据
* @param redis的key
* @return 返回值为删除的值
*/
public Object lPop(String key) {
Object obj = this.redisTemplate.opsForList().leftPop(key);
return obj;
}
/**
* @title list->删除右边的第一条数据
* @param redis的key
* @return 返回值为删除的值
*/
public Object rPop(String key) {
Object obj = this.redisTemplate.opsForList().rightPop(key);
return obj;
}
/******************************* Set *******************************/
/**
* @title set->添加数据
* @param redis的key,数组
* @return 返回值为添加的条数
* @Desc 数据类型 ("redisKey", "val1", "val2", "val3"...)
*/
public Long sAdd(String key, Object[] obj) {
if (ObjectUtils.isBlank(obj)) {
return 0L;
}
return sAdds(key, obj);
}
/**
* @title set->多数据添加数据
* @param redis的key,数据值...
* @return 返回值为添加的条数
* @Desc 数据类型 ("redisKey", "val1", "val2", "val3"...)
*/
public Long sAdds(String key, Object... obj) {
if (ObjectUtils.isBlank(obj)) {
return 0L;
}
Long count = this.redisTemplate.opsForSet().add(key, obj);
return count;
}
/**
* @title set->获取数据
* @param redis的key
* @return 返回Set集合
* @Desc 数据类型 ("redisKey", "val1", "val2", "val3"...)
*/
public Set<Object> getSet(String key) {
if (ObjectUtils.isBlank(key)) {
return new HashSet<Object>();
}
Set<Object> set = this.redisTemplate.opsForSet().members(key);
return set;
}
/**
* @title set->获取set集合中的数据条数
* @param redis的key
* @return 返回Set集合的数据条数
* @Desc 数据类型 ("redisKey", "val1", "val2", "val3"...)
*/
public Long getSetSize(String key) {
if (ObjectUtils.isBlank(key)) {
return 0L;
}
Long size = this.redisTemplate.opsForSet().size(key);
return ObjectUtils.isBlank(size) ? 0L : size;
}
/**
* @title set->删除set集合中的数据
* @param redis的key
* @return 返回删除的条数
*/
public Long removeSet(String key, Object... obj) {
if (ObjectUtils.isBlank(obj)) {
return 0L;
}
Long rem = this.redisTemplate.opsForSet().remove(key, obj);
return ObjectUtils.isBlank(rem) ? 0L : rem;
}
/******************************* ZSet *******************************/
/**
* @title ZSet->添加ZSet数据
* @param redis的key
* @return 成功返回true,失败返回false
*/
public boolean addZSet(String key, Object obj, double val) {
if (ObjectUtils.isBlank(key)) {
return false;
}
return this.redisTemplate.opsForZSet().add(key, obj, val);
}
/**
* @title ZSet->添加ZSet数据
* @param redis的key
* @return 成功返回添加的条数
*/
public Long addZSet(String key, Map<String, Double> map) {
if (ObjectUtils.isBlank(map)) {
return 0L;
}
Set<ZSetOperations.TypedTuple<Object>> tupleSet = new HashSet<>();
for (String k : map.keySet()) {
tupleSet.add(new DefaultTypedTuple<>(k, map.get(k)));
}
Long count = this.redisTemplate.opsForZSet().add(key, tupleSet);
return ObjectUtils.isBlank(count) ? 0L : count;
}
/**
* @title ZSet->获取指定的ZSet数据
* @param redis的key,开始索引,结束索引
* @return 获取到的数据集合
*/
public Set<Object> getZSet(String key, long startIndex, long endIndex) {
if (ObjectUtils.isBlank(key)) {
return new HashSet<Object>();
}
Set<Object> set = this.redisTemplate.opsForZSet().range(key, startIndex, endIndex);
return set;
}
/**
* @title ZSet->获取所有ZSet数据
* @param redis的key
* @return 获取到的数据集合
*/
public Set<Object> getZSetAll(String key) {
if (ObjectUtils.isBlank(key)) {
return new HashSet<Object>();
}
Set<Object> set = getZSet(key, 0, -1);
return set;
}
/**
* @title ZSet->删除数据
* @return 成功返回删除的条数
*/
public Long removeZSet(String key, Object[] obj) {
if (ObjectUtils.isBlank(obj)) {
return 0L;
}
Long rem = this.redisTemplate.opsForZSet().remove(key, obj);
return ObjectUtils.isBlank(rem) ? 0L : rem;
}
/**
* @title ZSet->删除数据
* @param remove("key", "val1", "val2");
* @return 成功返回删除的条数
*/
public Long removeZSets(String key, Object... obj) {
if (ObjectUtils.isBlank(obj)) {
return 0L;
}
return removeZSet(key, obj);
}
/******************************* Expire *******************************/
/**
* @title 给指定的缓存数据添加失效时间
* @param TimeUnit.DAYS; 天
* @param TimeUnit.HOURS; 小时
* @param TimeUnit.MINUTES; 分钟
* @param TimeUnit.SECONDS; 秒
* @param TimeUnit.MILLISECONDS; 毫秒
*/
public void setExpire(String key, Long timeout, TimeUnit unit) {
this.redisTemplate.expire(key, timeout, unit);
}
/**
* @title 给指定的缓存数据添加失效时间
* @param TimeUnit.DAYS; 天
* @param TimeUnit.HOURS; 小时
* @param TimeUnit.MINUTES; 分钟
* @param TimeUnit.SECONDS; 秒
* @param TimeUnit.MILLISECONDS; 毫秒
*/
public void setTimeout(String key, Long timeout, TimeUnit unit) {
setExpire(key, timeout, unit);
}
/**
* @title 获取失效时间
* @param redis的key
* @return 失效时长
*/
public Long getExpire(String key) {
if (ObjectUtils.isBlank(key)) {
return 0L;
}
Long timeout = this.redisTemplate.getExpire(key);
return ObjectUtils.isBlank(timeout) ? 0L : timeout;
}
/**
* @title 获取指定单位的失效时间
* @param TimeUnit.DAYS; 天
* @param TimeUnit.HOURS; 小时
* @param TimeUnit.MINUTES; 分钟
* @param TimeUnit.SECONDS; 秒
* @param TimeUnit.MILLISECONDS; 毫秒
*/
public Long getExpire(String key, TimeUnit unit) {
if (ObjectUtils.isBlank(key)) {
return 0L;
}
Long timeout = this.redisTemplate.getExpire(key, unit);
return ObjectUtils.isBlank(timeout) ? 0L : timeout;
}
/******************************* Key *******************************/
/**
* @Title 判断redis中是否包含这个Key
* @return Boolean
*/
public Boolean isExists(String key) {
if (StringUtils.isNotBlank(key)) {
return this.redisTemplate.hasKey(key);
}
return false;
}
/******************************* END *******************************/
/**
* @title 获取JSON转为对象后的数据
*/
@SuppressWarnings("unchecked")
public <T> T getJsonToObj(String key, Class<T> obj) {
String jsonStr = ObjectUtils.getStr(this.redisTemplate.opsForValue().get(key));
if (ObjectUtils.isNotEmpty(jsonStr)) {
return ObjectUtils.toObj(jsonStr, obj);
}
return (T) obj.getClass();
}
}
数据返回实体封装
import java.io.Serializable;
import java.util.HashMap;
/**
* @title 数据返回类
* @author 陌路
* @date 2022-04-16 下午3:51:04
*/
public class R extends HashMap<String, Object> implements Serializable {
private static final long serialVersionUID = 1L;
/** 只返回code值 */
public static final R OK = R.set("code", 200);
/** 返回flag和code值 */
public static final R ok = R.set("code", 200).put("flag", true);
public static final String PARAM_ERROR = "输入参数错误!";
public static final String UNKNOW_ERROR = "未知的错误请联系管理员!";
public R() {
}
/**
* @title 返回默认信息 flag msg code data
* @return R
*/
public static R ok() {
R r = new R();
r.put("flag", true);
r.put("msg", "成功!");
r.put("code", 200);
r.put("data", null);
return r;
}
/**
* @param <T>
* @title 添加返回值对象data
* @return R
*/
public static <T> R ok(T data) {
R r = new R();
r.put("flag", true);
r.put("msg", "成功!");
r.put("code", 200);
r.put("data", data);
return r;
}
/**
* @title 添加返回值信息
* @return R
*/
public static R ok(String msg) {
R r = new R();
r.put("flag", true);
r.put("msg", msg);
r.put("code", 200);
r.put("data", null);
return r;
}
/**
* @title 自定返回值对象key value
* @return R
*/
public static R ok(String key, String value) {
R r = new R();
r.put("flag", true);
r.put("msg", "成功!");
r.put("code", 200);
r.put("data", null);
r.put(key, value);
return r;
}
/**
* @title 添加返回值对象和提示信息
* @return R
*/
public static R ok(Object data, String msg) {
R r = new R();
r.put("flag", true);
r.put("msg", msg);
r.put("code", 200);
r.put("data", data);
return r;
}
/**
* @title 返回默认错误信息 flag msg code
* @return R
*/
public static R err() {
R r = new R();
r.put("flag", false);
r.put("msg", "服务器返回失败!");
r.put("code", 500);
r.put("data", null);
return r;
}
/**
* @title 添加自定义错误信息 msg
* @return R
*/
public static R err(String msg) {
R r = new R();
r.put("flag", false);
r.put("msg", msg);
r.put("code", 500);
return r;
}
@Override
public R put(String key, Object value) {
super.put(key, value);
return this;
}
/**
* @title 定义静态方法
* @param key
* @param value
* @return R
*/
public static R set(String key, Object value) {
R r = new R();
r.put(key, value);
return r;
}
}
WebSocket核心类
import java.io.IOException;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.RemoteEndpoint.Basic;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.molu.app.config.GetHttpSessionConfigurator;
import cn.molu.app.pojo.Message;
import cn.molu.app.pojo.Result;
import cn.molu.app.pojo.ResultMessage;
import cn.molu.app.pojo.User;
import cn.molu.app.utils.ObjectUtils;
import cn.molu.app.utils.RedisUtils;
import cn.molu.app.utils.SpringUtils;
/**
* @Description 注解 @ServerEndpoint 配置对外暴露访问地址,外部访问格式为(ws://localhost:80/chat)
* @author 陌路
* @date 2022-04-16 上午11:53:40
*/
@Component
@ServerEndpoint(value = "/webSocket/{userId}", configurator = GetHttpSessionConfigurator.class)
public class ChatEndpoint {
private final static Logger LOGGER = LogManager.getLogger(ChatEndpoint.class);
private HttpSession httpSession;
public static Map<String, Session> onLineUser = new ConcurrentHashMap<String, Session>();
@Resource
private RedisUtils redisUtils = SpringUtils.getBean(RedisUtils.class);
/**
* 连接建立时 会调用该方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId, EndpointConfig endpointConfig) {
HttpSession httpSession = (HttpSession) endpointConfig.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
if (ObjectUtils.isBlank(httpSession) || ObjectUtils.isEmpty(httpSession.getId())) {
return;
}
if (StringUtils.isBlank(userId)) {
return;
}
Object obj = this.redisUtils.getObj(userId);
if (obj == null) {
return;
}
User user = (User) obj;
if (ObjectUtils.isBlank(user)) {
return;
}
String username = ObjectUtils.getStr(user.getUsername());
Result res = new Result();
res.setUserId(userId);
res.setUsername(username);
res.setDateStr();
res.setMessage(String.format("用户%s上线了!", username));
// 缓存数据
List<Object> listAll = this.redisUtils.getListAll("onLineUsers");
if (!onLineUser.containsKey(userId)) {
if (!isContains(listAll, res)) {
this.redisUtils.lPush("onLineUsers", res);
}
}
this.redisUtils.setObj(httpSession.getId(), res);
onLineUser.put(userId, session);
// 获取未读数据条数
Map<String, Integer> map = getUnReadCount(userId, getIds());
// 1. 获取消息,该消息为系统消息,推送给所有用户
String message = getSysMessage(getusers(), map);
// 2. 调用方法进行系统消息的推送
broadcastAllUsers(message);
LOGGER.info("系统消息推送。。。{} ", message);
}
/**
* 接收到客户端发送的数据时 会调用此方法
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
LOGGER.info("来自客户端的消息:{}", message);
if (StringUtils.isBlank(message)) {
return;
}
if ("PING".toUpperCase().equals(message.toUpperCase())) {
heartCheck(session);
return;
}
Message msgObj = JSON.parseObject(message, Message.class);
String toId = msgObj.getToId();
String text = msgObj.getMessage();
msgObj.setDateStr(new Date());
if (ObjectUtils.isBlank(toId)) {
Map<String, String> map = new HashMap<String, String>();
map.put("sendErr", "消息发送失败,请稍后重试。。。");
ObjectMapper objectMapper = new ObjectMapper();
if (session != null) {
if (session.isOpen()) {
Basic basicRemote = toSession.getBasicRemote();
basicRemote.sendText(objectMapper.writeValueAsString(map));
return;
}
}
}
// 获取推送指定用户数据
String resultMessage = getMessage(msgObj, text);
LOGGER.info("接收到好友发来的数据:{}", resultMessage);
// 点对点发送数据(给指定用户发送消息)
Session toSession = onLineUser.get(toId);
if (toSession != null) {
if (toSession.isOpen()) {
Basic basicRemote = toSession.getBasicRemote();
basicRemote.sendText(resultMessage);
}
}
}
/**
* 连接关闭时 调用此方法
*/
@OnClose
public void onClose(Session session) {
if (httpSession == null || ObjectUtils.isBlank(httpSession.getId())) {
return;
}
Object obj = redisUtils.getObj(httpSession.getId());
if (obj == null) {
return;
}
Result res = (Result) obj;
String username = res.getUsername();
String userId = res.getUserId();
// 移除已关闭连接的用户
onLineUser.remove(userId);
this.redisUtils.removeList("onLineUsers", res);
if (ObjectUtils.isBlank(onLineUser)) {
this.redisUtils.remove("onLineUsers");
}
this.redisUtils.remove(httpSession.getId());
Message msgObj = new Message();
msgObj.setMessage(String.format("%s离线了!", username));
msgObj.setFromId(userId);
msgObj.setFromName(username);
msgObj.setDateStr(new Date());
Map<String, Integer> map = getUnReadCount(userId, getIds());
String message = getSysMessage(getusers(), map);
broadcastAllUsers(message);
}
/**
* 出现错误时调用改方法
*/
@OnError
public void onError(Session session, Throwable error) {
LOGGER.info("连接出错了......{}", error);
}
/**
* 获取所有的用户名
*/
private Set<String> getIds() {
return ChatEndpoint.onLineUser.keySet();
}
/**
* 获取所有用户 如果是系统消息就返回这个
*/
private Collection<Result> getusers() {
List<Result> listAll = this.redisUtils.getListAlls("onLineUsers");
return listAll;
}
/**
* 消息的推送,将消息推送给所有用户
*/
private void broadcastAllUsers(String message) {
try {
// 将消息推送给所有的客户端
Set<String> ids = getIds();
for (String id : ids) {
Session session = onLineUser.get(id);
// 判断用户是否是连接状态
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
}
}
} catch (Exception e) {
LOGGER.error("广播发送系统消息失败!{}", e);
e.printStackTrace();
}
}
/**
* @title 组织消息内容
*/
public static String getMessage(Message msgData, String message) {
ResultMessage resultMessage = new ResultMessage();
resultMessage.setSystemMsgFlag(false);
// 消息发送人id
String fromId = msgData.getFromId();
// 消息发送人姓名
String fromName = msgData.getFromName();
resultMessage.setFromId(fromId);
resultMessage.setFromName(fromName);
resultMessage.setMessage(message);
resultMessage.setDateStr(new Date());
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(resultMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return "";
}
/**
* @Title 组织系统推送数据信息
*/
public static String getSysMessage(Collection<Result> collection, Map<String, Integer> map) {
ResultMessage resultMessage = new ResultMessage();
resultMessage.setSystemMsgFlag(true);
resultMessage.setMessage(collection);
resultMessage.setDateStr();
resultMessage.setMap(map);
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(resultMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return "";
}
/**
* @Title 心跳检测机制
*/
public static void heartCheck(Session session) {
try {
Map<String, Object> params = new HashMap<String, Object>();
params.put("type", "PONG");
session.getAsyncRemote().sendText(JSON.toJSONString(params));
LOGGER.info("应答客户端的消息:{}", JSON.toJSONString(params));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @Title isContains
*/
private boolean isContains(List<Object> listAll, Result res) {
if (ObjectUtils.isBlank(listAll) || ObjectUtils.isBlank(res)) {
return false;
}
for (Object object : listAll) {
Result obj = ObjectUtils.getObj(object);
if (res.getUserId().equals(obj.getUserId())) {
return true;
}
}
return false;
}
/**
* 获取未读消息条数
*/
public Map<String, Integer> getUnReadCount(String userId, Set<String> ids) {
Map<String, Integer> map = new HashMap<String, Integer>();
if (!ObjectUtils.isBlank(ids) && !ObjectUtils.isBlank(userId)) {
for (String id : ids) {
// 主动刷新页面,获取未读数据条数
String redisCountKey1 = "UN_READ_MSG_COUT_" + id + "_" + userId;
// 好友刷新页面,推送未读数据条数
String redisCountKey2 = "UN_READ_MSG_COUT_" + userId + "_" + id;
if (this.redisUtils.isExists(redisCountKey1) || this.redisUtils.isExists(redisCountKey2)) {
// 主动刷新页面,获取未读数据条数
String count1 = ObjectUtils.getStr(this.redisUtils.getStr(redisCountKey1));
if (StringUtils.isNotBlank(count1)) {
map.put(id, Integer.valueOf(count1));
}
// 好友刷新页面,推送未读数据条数
String count2 = ObjectUtils.getStr(this.redisUtils.getStr(redisCountKey2));
if (StringUtils.isNotBlank(count2)) {
map.put(userId, Integer.valueOf(count2));
}
}
}
}
return map;
}
}
前端核心Html代码
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Chat聊天窗</title>
<link th:href="@{/css/chat.css}" rel="stylesheet">
<script type="text/javascript" th:src="@{/js/jquery.js}"></script>
<script type="text/javascript" th:src="@{/js/div.move.js}"></script>
<script type="text/javascript" th:src="@{/js/molu.js}" ></script>
<script type="text/javascript" th:src="@{/js/chat.js}"></script>
<!-- emoji表情框滚动 -->
<link rel="stylesheet" th:href="@{/lib/meoji/css/jquery.mCustomScrollbar.min.css}" />
<link rel="stylesheet" th:href="@{/lib/meoji/css/jquery.emoji.css}" />
<!-- emoji表情框滚动 -->
<script type="text/javascript" th:src="@{/lib/meoji/script/jquery.mCustomScrollbar.min.js}"></script>
<script type="text/javascript" th:src="@{/lib/meoji/script/jquery.emoji.min.js}"></script>
</head>
<body>
<input type="hidden" name="uname" class="uname" th:value="${name}" />
<input type="hidden" name="uid" class="uid" th:value="${id}" />
<input type="hidden" name="count" class="count" />
<div class="box">
<div class="title">
<p class="chating">
<span style="float: left; padding-left: 5px; color: darkblue;" th:text="${name}"></span>
<font>请选择<span class="chating-user">用户</span>聊天</font>
</p>
<span class="min_hide" title="最小化">一</span>
<span class="close" title="关闭">X</span>
</div>
<div class="friend-list">
<ul class="friend-ul"></ul>
</div>
<div class="chat-main">
<ul class="chating-main-msg"></ul>
</div>
<div class="sys-msg">
<span>系统消息</span>
<ul class="sys-msg-ul">
</ul>
</div>
<div class="tools">
<span class="tools-list">
<a class="tools-list-a face" href="javascript:;" title="表情">☺</a>
</span>
</div>
<div class="input-text">
<div class="edit-msg" contenteditable></div>
</div>
<div class="bottom-bar">
<button class="closeDiv close-click">关闭</button>
<button class="sendMsg">发送</button>
</div>
</div>
</body>
</html>
前端核心 JS 代码
let username, userId, toName, toId;
let currenChattUser = { id: '', name: '', count: 0 };
let unReadMsgUser = { id: '', count: 0 }
let ids=[];
let imgUrl = "../../imgs/chatIc.png";
let webSocket;
let lockReconnect = false; // 网络断开重连
let wsCreateHandler = null;
let reconnectCount = 0;
$(function() {
// 关闭异步请求方式,使用同步请求
username = $(".uname").val();
userId = $(".uid").val();
//移除右下角标,去除大小调节功能
$(".bg_change_size").remove();
// 建立连接
createWebSocket();
// 发送消息
$(".sendMsg").on("click", function() {
sendMsg(webSocket);
});
emojiFace();
});
// 建立连接
function createWebSocket(){
try {
// 获取访问路径 带有端口号 ws://localhost/webSocket/001
let host = window.location.host;
// 创建WebSocket连接对象
webSocket = new WebSocket(`ws://${host}/webSocket/${userId}`);
// 加载组件
initWsEventHandle();
} catch (e) {
writeToScreen("连接出错,正在尝试重新连接,请稍等。。。");
// 尝试重新连接服务器
reconnect();
}
}
// 初始化组件
function initWsEventHandle() {
try {
// 建立连接
webSocket.onOpen = function(evt) {
onWsOpen(evt);
// 建立连接之后,开始传输心跳包
heartCheck.start();
writeToScreen("连接成功。。。");
};
// 传送消息
webSocket.onmessage = function(evt) {
// 发送消息
onWsMessage(evt);
// 接收消息后 也需要心跳包的传送
heartCheck.start();
isConn = true;
};
// 关闭连接
webSocket.onclose = function(evt) {
// 关闭连接,可能是异常关闭,需要重新连接
onWsClose(evt);
};
// 连接出错
webSocket.onerror = function(evt) {
// 连接出错
onWsError(evt);
// 尝试重新连接
reconnect();
}
} catch (e) {
writeToScreen("初始化组件失败,正在重试,请稍后。。。");
// 尝试重新创建连接
reconnect();
}
}
function onWsOpen(e) {
//writeToScreen("连接成功。。。");
}
function onWsMessage(e) {
//接收到服务器推送的消息后触发事件
message(e);
}
function onWsClose(e) {
closeFun(e);
}
function onWsError(e) {
if(reconnectCount == 2){
writeToScreen("连接出错,正在尝试重新连接服务器,请稍侯。。。" );
}
}
function writeToScreen(message) {
ml.msgBox(message, 5,3);
}
function reconnect() {
if (lockReconnect) {
return;
}
if(reconnectCount >= 30){
writeToScreen("未收到服务器的响应,连接关闭。。。");
webSocket.close();
clearTimeout(wsCreateHandler);
reconnectCount = 0;
return false;
}
console.log("正在重新连接。。。"+reconnectCount);
lockReconnect = true;
// 没链接上会一直连接,设置延迟,避免过多请求
wsCreateHandler && clearTimeout(wsCreateHandler);
wsCreateHandler = setTimeout(function() {
createWebSocket();
lockReconnect = false;
reconnectCount++;
}, 3000);
}
function message(event){
//获取服务端推送过来的消息
let result = event.data;
// 将message转为JSON对象
let res = JSON.parse(result);
// 组织好友列表
let friendList = "";
// 组织系统通知内容
let sysMsg = "";
if(res.type && res.type == "PONG"){
return;
}
if(res.sendErr){
writeToScreen(res.sendErr);
return;
}
// 是否为系统消息
if (res.systemMsgFlag) {
//为系统消息则:1. 好友列表展示 2. 系统推广
let allUser = res.message;// name = username_userId
for (let user of allUser) {
let count = 0;
let isShow = "";
$.each(res.map, function(item, num) {
if (item && item == user.userId) {
count = Number(num) > 99 ? 99 : Number(num);
isShow = `style="display:inline;"`;
if(ids.indexOf(user.userId) == -1){
ids.push(user.userId);
}
}
})
if (user.userId != userId) {
// 组织好友列表
friendList += `<li class="friend-li" οnclick='chatWith("${user.userId}","${user.username}",this);'>
<span><img class="friend-img" src="${imgUrl}"/></span>
<span>${user.username}</span>
<span class="msg-count msg-count-${user.userId}" ${isShow} title="${count}条信息未读">${count}</span>
</li>`;
// 组织系统通知
sysMsg += `<li class="sys-msg-li" style="color:#9d9d9d;font-family:宋体;">
<span style="font-size:5px;color:#999;">${user.dateStr}</span><br/>
<span>好友<font style="color:blue;">${user.username}</font>上线了</span>
</li>`;
tips("sendMsg", `用户:${user.username}上线了`);
}
}
$(".friend-ul").html(friendList);
$(".sys-msg-ul").html(sysMsg);
// 不是系统消息
} else {
let contextMsg = res.message;
let chatMsg = `<li class="friend-msg-li">
<span><img class="friend-msg-img" src="${imgUrl}" /></span>
<span class="friend-msg-span">${contextMsg}</span>
</li>`;
if (!currenChattUser.id || currenChattUser.id != res.fromId) {
let count = Number($(".msg-count-" + res.fromId).text());
if (!count || count == NaN) {
count = 0;
}
unReadMsgUser.id = res.fromId;
$.post("/index/setCount", { fromId: res.fromId, userId: userId, count: count }, function(resData) {
if (resData.flag) {
count = resData.data > 99 ? 99 : resData.data;
currenChattUser.count = count;
unReadMsgUser.count = count;
$(".msg-count-" + res.fromId).text(count);
$(".msg-count-" + res.fromId).show();
$(".msg-count-" + res.fromId).attr("title", count + "条信息未读");
}
}).catch(function(err) {
console.log(err);
})
}
if (toId === res.fromId) {
$(".chating-main-msg").append(chatMsg);
}
scrollIntoView();
// 获取 sessionStorage 中存放的系统缓存消息
let chatData = sessionStorage.getItem(res.fromId);
if (chatData) {
chatMsg = chatData + chatMsg;
}
// 将系统消息存放到 sessionStorage 中
sessionStorage.setItem(res.fromId, chatMsg);
}
}
var heartCheck = {
// 在15s内若没收到服务端消息,则认为连接断开,需要重新连接
timeout: 15000, // 心跳检测触发时间
timeoutObj: null,
serverTimeoutObj: null,
// 重新连接
reset: function() {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.start();
},
// 开启定时器
start: function() {
let self = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function() {
try {
webSocket.send("PING");
} catch (e) {
writeToScreen("连接服务器出错。。。");
}
//内嵌定时器
self.serverTimeoutObj = setTimeout(function() {
//writeToScreen("未收到服务器的响应,连接关闭。。。");
//webSocket.close();
reconnect();
}, self.timeout);
}, this.timeout);
}
};
/**
* 选择好友
* @param id 好友id
* @param name 好友名称
* @param obj
*/
function chatWith(id, name, obj) {
toName = name;
toId = id;
currenChattUser = { id: id, name: name, count: 0 };
$(".msg-count-" + id).hide();
$(obj).addClass("selected-li").siblings().removeClass("selected-li");
let chatNow = `正在和<span style="color: #db41ca;">${name}</span>聊天`;
$(".chating>font").html(chatNow);
$(".chating-main-msg").html("");
let chatData = sessionStorage.getItem(id);
if (chatData) {
//渲染聊天数据到聊天区
$(".chating-main-msg").html(chatData);
}
let idx = ids.indexOf(id);
if (idx != -1 && id == ids[idx]) {
$.post("/index/clearCount", { fromId: id, userId: userId }, function(data) {
$(".msg-count-" + id).text(data);
})
}
scrollIntoView();
}
/**
* 发送消息事件
*/
function sendMsg(ws) {
if (!toId || !toName) {
tips("sendMsg", "请选择好友...");
return;
}
let msg = $(".edit-msg").html();
if (!msg) {
tips("sendMsg", "请输入内容...");
return;
}
let jsonMessage = {
"fromName": username, //消息发送人姓名
"fromId": userId, //消息发送人id
"toName": toName, //消息接收人姓名
"toId": toId, //消息接收人id
"message": msg //发送的消息内容
};
// 发送数据给服务器
ws.send(JSON.stringify(jsonMessage));
// 显示发送数据
let img = `<span><img src='${imgUrl}' class="myself-msg-img"/></span>`;
let li = `<li class='myself-li'><span class="myself-msg-span">${msg}</span>${img}</li>`;
$(".chating-main-msg").append(li);
// 获取 sessionStorage 中的缓存消息
let chatData = sessionStorage.getItem(toId);
if (chatData) {
li = chatData + li;
}
// 将最新的消息存放到 sessionStorage 中
sessionStorage.setItem(toId, li);
//$(".edit-msg").val('');
$(".edit-msg").html('');
scrollIntoView();
}
/**
* 发送emoji表情
*/
function emojiFace() {
$(".face").on('click', function() {
// 初始化emoji插件
$(".edit-msg").emoji({
// 触发表情的按钮
button: '.face',
showTab: false,
animation: 'slide',
position: 'topLeft',
icons: [
{
name: 'QQ表情', //表情名,表情框中显示的名字
path: '../../lib/meoji/img/qq/', // 表情包所在的路径
maxNum: 91,
excludeNums: [41, 45, 54],
file: '.gif',
placeholder: '#qq_{alias}#'
},
{
name: '贴吧表情',
path: '../../lib/meoji/img/tieba/',
maxNum: 50,
excludeNums: [41, 45, 54],
file: '.jpg',
placeholder: '#tieba_{alias}#'
}
]
});
})
}
// 关闭连接
function closeFun(e) {
//let tips = `<span style='font-size:5px;color:black;'>${new Date()}</span><br/>`;
$(".sys-msg").html(`用户:${username}<span style="float:right;color:red;">离开了</span>`);
}
/**
* 滚动条显示在最底部
*/
function scrollIntoView() {
$(".chat-main")[0].scrollTop = $(".chat-main")[0].scrollHeight;
}
/**
* 消息提示
*/
function tips(clazz, msg) {
$(".fun_win_div_tips_msg").remove();
let html = `<p class="fun_win_div_tips_msg" style="z-index:10;margin: 5px -80px;">
<span style="padding:3px 8px;text-align:center;
box-shadow:0px 0px 2px 1px #6a5700;padding: 3px 8px;
text-align:center;background-color: #fff;">
${msg}
</span>
</p>`;
setTimeout(function() {
$(".fun_win_div_tips_msg").remove();
}, 1800);
$("." + clazz).after(html);
}