springboot+vue webSocket与自定义注解整合使用
这一套代码实现的逻辑是:配置一个注解(@Notice),用户可以在自己的service层使用该注解,无需修改service层逻辑,通过一些注解配置,实现调用websocket通知其他用户。
比如:我原先有一个方法是录入一条信息,我只需要在该方法上添加该注解,就可以实时通知别人有一条信息待处理:
// 在这个进入这个方法后,通过#UserInfoVo.developedUserId拿到入参,获取接收人的id,并发送给它消息(noticeMsg)
@Notice(receiver = "#UserInfoVO.developUserId", noticeMsg = "您有一条信息待处理。",isShowSender = true) @Override public RetMsg<Null> insertOneMsg(UserInfoVO mineUserInfoVO) {
//普通业务逻辑
。。。
}
根据具体业务需求,该注解的一些可配置参数:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Notice { /** * 消息类型 simple(default):简单模式只需在接口中配置接收人和消息即可 * complex:复杂模式需要实现IHandleNotice接口,配置不同的operateType对应不同的实现类 */ String noticeType() default "simple"; /** * 接收对象 simple模式配置 (可输入表达式) */ String receiver() default ""; /** * 消息内容 simple模式配置 */ String noticeMsg() default ""; /** * 是否显示发送方 simple模式配置 */ boolean isShowSender() default true; /** * 通知类型,在枚举类中添加 */ NoticeType operateType() default NoticeType.Default; /** * 是否开启通知(可输入表达式) */ String condition() default ""; }
1.springBoot中webSocket配置
1.1 WebSocketServer
/** * 这里就相当于一个ws协议的controller */ @Component @ServerEndpoint("/websocket/{userName}") public class WebSocketServer { //存放每个客户端建立的 链接 private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>(); /** * 一个应用建立连接时调用的方法 * @param session * @param userName */ @OnOpen public void onOpen(Session session, @PathParam(value="userName")String userName) { sessionPool.put(userName, session); System.out.println(userName+"【websocket消息】有新的连接,总数为:"+sessionPool.size()); } /** * 关闭时调用,删掉一个session */ @OnClose public void onClose(@PathParam(value = "userName") String userName) { sessionPool.remove(userName); System.out.println(userName+"【websocket消息】连接断开,总数为:"+sessionPool.size()); } @OnMessage public void onMessage(String message) { System.out.println("【websocket消息】收到客户端消息:"+message); } //发送消息 public void sendOneMessage(String userName, String message) throws IOException { System.out.println(userName+":发送【websocket消息】:"+message); Session session = sessionPool.get(userName); if (session != null) { synchronized(session) { session.getBasicRemote().sendText(message); } } } }
这里的 @ServerEndpoint 就是初始化和通信消息的链接
1.2 WebSocketServer
@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
注入一个ServerEndPointExporter
1.3 配置一个向后台获取websockt地址的接口,根据不同的环境返回不同的websocket地址:
@CrossOrigin @RestController @RequestMapping("/api/socket") public class SocketApiController { //websockt.localaddress=localhost:8080 @Value("${websockt.localaddress:}") private String localAddress; @GetMapping("/getWsAddress") public String getWsAddress() { if("".equals(localAddress)) return "";
//user_id可以从spring security上下文中获取,这里也可以随便设置一个测试
String user_id = "test"; String wsUrl = "ws://" + localAddress + "/websocket/" +user_id ; return wsUrl; } }
2.Vue中webSocket配置
建一个webSocketcomponent组件
<template> </template> <script> import { axios } from '@/utils/request' export default { name: 'WebSocketComponents', data () { return { socket: '' } }, methods: { initWebSocket() { this.socket.onerror = this.setErrorMessage this.socket.onopen = this.setOnopenMessage console.log("连接建立成功:" + this.wsUrl) this.socket.onmessage = this.setOnMessage this.socket.onclose = this.setOncloseMessage window.onbeforeunload = this.onbeforeunload }, setOnMessage(event) { const noticeMsg = JSON.parse(event.data); // this.msgs[Number(noticeMsg.msgNo)-1].msg = noticeMsg.msg // this.showNotice[Number(noticeMsg.msgNo)-1].show = !(noticeMsg.msg === undefined) this.$notification.info( { message: noticeMsg.noticeMsg , description: noticeMsg.noticeDescription}) this.$emit('noticeInit'); }, setErrorMessage () { console.log('WebSocket连接发生错误 状态码:' + this.socket.readyState) }, setOnopenMessage () { console.log('WebSocket连接成功 状态码:' + this.socket.readyState) }, setOnmessageMessage (event) { console.log('服务端返回:' + event.data) }, setOncloseMessage () { console.log('WebSocket连接关闭 状态码:' + this.socket.readyState) }, onbeforeunload () { this.closeWebSocket() }, closeWebSocket () { this.socket.close() }, },
//如果嵌入了该组件,向后台请求websocket地址 不为空即建立websocket连接 created() { const that = this; axios.get(`/socket/getWsAddress`).then( (response) => { if ("" == response) { return; } // Return a string that indicates how binary data from the WebSocket object is exposed to scripts that.socket = new WebSocket(response); that.initWebSocket();
//这里可以在websocket连接成功后,去调用父组件的初始化的方法 that.$emit('noticeInit'); }); } } </script>
3.实现注解调用websocket
3.1实现@notice注解的具体逻辑:
/** * 在被注解的方法后 去通知用户 * @author xjx * 简单模式发送:只需要在注解中配置接收人和发送的信息即可
复杂模式发送:实现IHandleNotice接口,配置不同的operateType,发送对应的消息 */ @Aspect @Component public class NoticeConfig { //表达式解析类 final ExpressionParser parser = new SpelExpressionParser(); //该类可以从反射的方法中拿到参数名称 final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); @Autowired private WebSocketServer webSocketServer;
//看后面策略模式代码 @Autowired private NoticeChooser noticeChooser; @Pointcut("@annotation(com.tongdatech.winterspring.zczx.webSocketConfig.NoticeAnnotation.Notice)" ) public void noticeConfig() { } @AfterReturning(pointcut = "noticeConfig()", returning = "returnObject") public void doNotice(JoinPoint joinPoint,Object returnObject) throws IOException{ if("".equals(localAddress)) return; Method method = ((MethodSignature)joinPoint.getSignature()).getMethod(); Object[] args = joinPoint.getArgs(); Notice noticeAnnotation = method.getAnnotation(Notice.class); /** * 判断是否发送通知 */ if(!noticeAnnotation.condition().equals("")) { if(!generateKeyBySpEL(noticeAnnotation.condition(),joinPoint,Boolean.class)) { return; } } /** * 简单模式发送 */ if("simple".equals(noticeAnnotation.noticeType())) {
//从注解的参数中获得收信人 String receiver = generateKeyBySpEL(noticeAnnotation.receiver(),joinPoint,String.class); String sender = "";
//判断通知是否带发送人名称 if(noticeAnnotation.isShowSender()) { sender = "来自:"+getUserName(); } NoticeMsg noticeMsg = new NoticeMsg(null,null,noticeAnnotation.noticeMsg(),sender,null,null); webSocketServer.sendOneMessage(receiver,JSON.toJSONString(noticeMsg)); return; } /** * 复杂模式发送
根据不同的operateType 调用不同的实现类
我们给IHandleNotice接口中,传入我们在@Notice注解的方法中获取到的参数和返回值,并获取它返回的 Map<接收人,消息> */ IHandleNotice iHandleNotice = noticeChooser.choose(noticeAnnotation.operateType()); Map<String, NoticeMsg> receiversAndMsgs = iHandleNotice.handelNotice(getUserId(), args, returnObject); for (Map.Entry<String, NoticeMsg> entry : receiversAndMsgs.entrySet()) { String jsonMsg = JSON.toJSONString(entry.getValue()); webSocketServer.sendOneMessage(entry.getKey(),jsonMsg); } } /** * 获取发送人用户信息,看情况,这里可以从security架构中,上下文中获取 * @return */ protected String getUserName() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
。。。
} /** * 解析表达式工具类 返回注解中配置的参数获取到的值 * @param spELString * @param joinPoint * @param clazz * @return */ private <T> T generateKeyBySpEL(String spELString, JoinPoint joinPoint, Class<T> clazz) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); String[] paramNames = nameDiscoverer.getParameterNames(method); Expression expression = parser.parseExpression(spELString); EvaluationContext context = new StandardEvaluationContext(); Object[] args = joinPoint.getArgs(); for(int i = 0 ; i < args.length ; i++) { context.setVariable(paramNames[i], args[i]); } return expression.getValue(context,clazz); }
3.1利用策略模式实现注解复杂情况发送:
消息实体类 noticeMsg:
public class NoticeMsg implements Serializable{ private static final long serialVersionUID = 1L; private String msgNo;//消息编号 对应前台消息的位置 private String msg;//消息 private String noticeMsg; private String noticeDescription; private String url; private Object object;//预留对象 public NoticeMsg(String msg, String url) { super(); this.msg = msg; this.url = url; } public NoticeMsg(String msg, String url,Object object) { super(); this.msg = msg; this.url = url; this.object = object; } public NoticeMsg(String msgNo, String msg, String noticeMsg, String noticeDescription, String url, Object object) { super(); this.msgNo = msgNo; this.msg = msg; this.noticeMsg = noticeMsg; this.noticeDescription = noticeDescription; this.url = url; this.object = object; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getNoticeMsg() { return noticeMsg; } public void setNoticeMsg(String noticeMsg) { this.noticeMsg = noticeMsg; } public String getNoticeDescription() { return noticeDescription; } public void setNoticeDescription(String noticeDescription) { this.noticeDescription = noticeDescription; } public Object getObject() { return object; } public void setObject(Object object) { this.object = object; } public String getMsgNo() { return msgNo; } public void setMsgNo(String msgNo) { this.msgNo = msgNo; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } @Override public String toString() { return "NoticeMsg [msgNo=" + msgNo + ", msg=" + msg + ", noticeMsg=" + noticeMsg + ", noticeDescription=" + noticeDescription + ", url=" + url + ", object=" + object + "]"; } }
先写一个枚举类,存放所有的策略(对应@Notice中operateType)
public enum NoticeType { Default,CountyPaiDan,ReAddr,AddCustomer }
定义一个策略接口,包含对策略的抽象和 自己所属的枚举标记:
public interface IHandleNotice { //获得 @Notice注解的方法中的参数,发送人 和 返回参数 返回 Map<接收人,信息> Map<String, NoticeMsg> handelNotice(String userId, Object[] paramObject, Object returnObject); //返回实现类所属枚举类中的值 public NoticeType noticeType(); }
写一些类去实现该接口:
@Service public class AddCustomerNoticeImpl implements IHandleNotice{ @Override public NoticeType noticeType() { // TODO Auto-generated method stub return NoticeType.AddCustomer; } @Override public Map<String, NoticeMsg> handelNotice(String userId, Object[] paramObject, Object returnObject) {
。。。 }
@Service public class ReaddrNoticeImpl implements IHandleNotice{ @Override public NoticeType noticeType() { // TODO Auto-generated method stub return NoticeType.ReAddr; } @Override public Map<String, NoticeMsg> handelNotice(String userId, Object[] paramObject, Object returnObject) { 。。。 }
我们将实现类在spring容器初始化后存入内存中 NoticeChooser:
/** * ApplicationContextAware 通过实现该接口可以拿到容器 * @author xjx * */ @Component public class NoticeChooser implements ApplicationContextAware{ private Map<NoticeType, IHandleNotice> noticeMap = new ConcurrentHashMap<>(); //成员变量来接收容器 private ApplicationContext applicationContext; public IHandleNotice choose(NoticeType noticeType) { return noticeMap.get(noticeType); } @PostConstruct public void initNoticeTypes() { //根据类型拿到容器 Map<String, IHandleNotice> noticeTypes = applicationContext.getBeansOfType(IHandleNotice.class); noticeTypes.forEach((String t, IHandleNotice handle) -> { noticeMap.put(handle.noticeType(), handle); }); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // TODO Auto-generated method stub this.applicationContext = applicationContext; } }
这样,我们在之前的代码:IHandleNotice iHandleNotice = noticeChooser.choose(noticeAnnotation.operateType());
通过注解中的operateType(),可以拿到对应的对IHandleNotice的实现,这样发送的通知就是注解配置的实现类。
最后,只要调用带@Notice的方法,即可发送通知。