這一套代碼實現的邏輯是:配置一個注解(@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的方法,即可發送通知。
