SpringBoot 2.0.0.3 + JDK 1.8 + IDEA + Redis(spring-boot-starter-data-redis) + Nginx1.14
坑点:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @description: websocket类
* @create: 2019-11-21 13:56
**/
@Component
@Slf4j
@ServerEndpoint("/webService/websocket/{userId}")
public class WebSocketServer {
// 消息会话
private Session session;
//消息接收者id
private String userId;
// 每个客户的WebSocketServer 需要保证线程安全
private static CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
/**
* @Description: 建立连接时调用
* @param session
* @param userId
* @Date: 2019/11/21 15:11
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId")String userId){
log.info("【websocket消息推送模块】--用户" + userId +"连接服务。");
this.session = session;
this.userId = userId;
list.add(this);
}
/**
* @Description: 关闭连接时调用
* @param session
* @param userId
* @Date: 2019/11/21 15:11
*/
@OnClose
public void onClose(Session session,@PathParam("userId")String userId){
log.info("【websocket消息推送模块】--用户" + userId +"断开服务。");
list.remove(this);
}
/**
* @Description: 接收到前台推送的消息时调用
* @param message
* @param session
* @Date: 2019/11/21 15:15
*/
@OnMessage
public void onMessage(String message, Session session,@PathParam("userId")String userId){
//目前用不到
log.info("【websocket消息推送模块】--接收到用户" + userId +"推送的消息:" + message);
//可以根据userId,推送给具体人
sendInfo(message,userId);
}
@OnError
public void onError(Session session,Throwable error){
log.info("【websocket消息推送模块】--用户" + userId +"连接异常。");
error.printStackTrace();
}
/**
* @Description: 发送消息
* @param message
* @Date: 2019/11/21 15:20
*/
public void sendMessage(String message) throws IOException {
session.getBasicRemote().sendText(message);
}
/**
* @Description: 群发消息
* @param message
* @param userId
* @Author:
* @Date: 2019/11/21 15:35
*/
public static void sendInfo(String message, @PathParam("userId")String userId){
log.info("【websocket消息推送模块】--用户" + userId +"发送消息");
list.stream().forEach(item->{
if(item.userId.equalsIgnoreCase(userId.trim())){
try {
log.info("【websocket消息推送模块】--推送消息给用户" + userId);
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
注意:如果是以jar包方式运行需要加上以下配置。如果以war方式运行则不需要添加以下配置,会和tomcat冲突。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
另外因为服务器端使用了Nginx代理,所以需要在Nginx 中添加以下配置解决404问题。默认websocket 连接60秒会自动断开
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s; # 解决连接自动关闭问题。
如果对最大连接数和排队数有要求,可以配置Tomcat 中的server.xml
用到了 订阅/发布模式 。
redis 配置
@Configuration
@Slf4j
public class RedissonConfig {
@Autowired
private MessageReceiver messageReceiver;
// 消息处理器 集合
private static ConcurrentHashMap listenerMap = new ConcurrentHashMap<>();
/**
* @Description: 自定义redis 模板
* @param lettuceConnectionFactory
* @Author:
* @Date: 2019/11/9 16:53
*/
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){
//配置redisTemplate 序列化方式
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(lettuceConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//java 对象 和 json 之间转换的框架
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//使用enableDefaultTyping()枚举指定什么样的类型(类)默认输入应该使用。
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key 采用String序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* @Description: 注入消息监听器 支持自定义通道和自定义消息处理器.
* 用法:1.在枚举类中添加 通道名称
* 2.定义消息监听器和消息处理器。并注入到setListenerMap 中
* @param connectionFactory 连接工厂
* @Author:
* @Date: 2019/11/11 14:54
*/
@Bean
@DependsOn({"listenerAdapter"})
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
if(listenerMap == null || listenerMap.size() == 0){
log.info("Redis 注册消息监听器失败!");
throw new RRException("Redis 注册消息监听器失败!");
}
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
log.info("listenerMap == " + JSONObject.toJSONString(listenerMap));
//循环遍历,添加通道
Arrays.asList(Constant.RedisTopic.values()).stream().forEach(o -> {
if(listenerMap.containsKey(o.getName())){
container.addMessageListener(listenerMap.get(o.getName()),new PatternTopic(o.getName()));
}
});
//发布时,需要序列化对象
Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
seria.setObjectMapper(objectMapper);
container.setTopicSerializer(seria);
return container;
}
/**
* @Description: 消息处理器 不同topic主题通用同一消息处理器。如果新增消息处理器
* @param
* @Author:
* @Date: 2019/11/11 16:10
*/
@Bean("listenerAdapter")
public MessageListenerAdapter listenerAdapter(MessageReceiver receiver){
//定义消息处理器
MessageListenerAdapter adapter = new MessageListenerAdapter(receiver,"receiveMessage1");
//添加到listenerMap中
setListenerMap(Constant.RedisTopic.TOPIC_ONE.getName(),adapter);
return adapter;
}
/**
* @Description: 消息处理器 不同topic主题通用同一消息处理器。如果新增消息处理器
* @param
* @Author:
* @Date: 2019/11/11 16:10
*/
@Bean("listenerAdapter2")
public MessageListenerAdapter listenerAdapter2(MessageReceiver receiver){
//定义消息处理器
MessageListenerAdapter adapter = new MessageListenerAdapter(receiver,"receiveMessage2");
//添加到listenerMap中
setListenerMap(Constant.RedisTopic.TOPIC_TWO.getName(),adapter);
return adapter;
}
/**
* @Description: 设置消息处理器集合
* @param topicName
* @param adapter
* @Author:
* @Date: 2019/11/11 18:30
*/
public void setListenerMap(String topicName,MessageListenerAdapter adapter){
if(StringUtils.isBlank(topicName)){
log.info("Redis 设置消息处理器失败! -- 主题名称:topicName为空");
throw new RRException("Redis 设置消息处理器失败!");
}
if(adapter == null){
log.info("Redis 设置消息处理器失败! -- 消息处理器 MessageListenerAdapter:adapter为空");
throw new RRException("Redis 设置消息处理器失败!");
}
if(listenerMap == null){
listenerMap = new ConcurrentHashMap<>();
}
listenerMap.put(topicName,adapter);
}
}
配置消息接受器
/**
* @description: redis 消息处理器
* @author:
* @create: 2019-11-12 11:10
**/
@Component
@Slf4j
public class MessageReceiver {
@Autowired
private MessageContentService messageContentService;
@Autowired
private MessageUserService messageUserService;
private static CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
public void receiveMessage1(String messageId){
//调用webSocket 推送 消息 以及消息接受者
log.info("receiveMessage1接收到的消息为:" + messageId);
if(StringUtils.isNotBlank(messageId)){
list.add(messageId);
doSendMessage();
}
}
public synchronized void doSendMessage(){
//处理具体业务
}
}
这里用切面处理 注解:
import java.lang.annotation.*;
/**
* @Description: redis 消息订阅注解
* @Author:
* @Date: 2019/11/21 10:22
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisMessage {
/**
* 消息内容
*/
String content() default "";
/**
* 用户id,多个用,隔开
*/
String userId() default "";
}
切面:
/**
* @description:
* @author:
* @create: 2019-11-21 10:27
**/
@Component
@Aspect
@Slf4j
public class RedisMessageAspect {
@Autowired
private MessageContentService messageContentService;
@Autowired
private MessageUserService messageUserService;
@Autowired
private StringRedisTemplate template;
@Pointcut("@annotation(com.test.common.annotation.RedisMessage)")
public void defaoltPointCut(){
}
@AfterReturning(value = "defaoltPointCut()",returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result){
log.info("Redis 消息订阅 注解开始执行");
if(result != null){
if(result instanceof Map){
Map map = (Map)result;
String content = map.get(Constant.MessageReturn.CONTENT.getName()).toString();
// 推送的用户
if(map.get(Constant.MessageReturn.USER_ID.getName()) instanceof List){
List userIds = (List)map.get(Constant.MessageReturn.USER_ID.getName());
StringBuffer userId = new StringBuffer();
for (String str : userIds){
userId.append(str + Constant.Separator.REDIS_USER.getName());
}
if(StringUtils.isNotBlank(content) && StringUtils.isNotBlank(userId.toString())){
//把数据存入数据库
Long messageId = saveMessage(content, userId.toString());
if(messageId != null){
// 发布订阅
publishMessage(StringTools.stringOf(messageId));
}
}
}
}
}
log.info("Redis 消息订阅 注解结束");
}
/**
* @Description: 保存 消息、用户
* @param content
* @param userId
* @Author:
* @Date: 2019/11/21 10:38
*/
public Long saveMessage(String content,String userId){
//保存消息到数据库。或者redis
}
/**
* @Description: redis 发布消息
* @param message
* @Author: chenxue
* @Date: 2019/11/21 10:39
*/
public void publishMessage(String message){
template.convertAndSend(Constant.RedisTopic.TOPIC_ONE.getName(),message);
}
}