Browse Source

first commit

wangwei 5 years ago
commit
4a59661ad3
30 changed files with 956 additions and 0 deletions
  1. 12 0
      .gitignore
  2. 1 0
      README.md
  3. 30 0
      assembly.xml
  4. 4 0
      jenkins-prod.sh
  5. 18 0
      jenkins-test.sh
  6. 26 0
      pom.xml
  7. 1 0
      shell/start.args
  8. 38 0
      shell/start.sh
  9. 1 0
      shell/start.vmoptions
  10. 18 0
      shell/stop.sh
  11. 1 0
      shell/version
  12. 12 0
      src/main/java/cn/com/qmth/examcloud/ws/Tester.java
  13. 52 0
      src/main/java/cn/com/qmth/examcloud/ws/WebSocketApp.java
  14. 21 0
      src/main/java/cn/com/qmth/examcloud/ws/api/controller/WebSocketController.java
  15. 9 0
      src/main/java/cn/com/qmth/examcloud/ws/api/provider/WebSocketCloudServiceProvider.java
  16. 22 0
      src/main/java/cn/com/qmth/examcloud/ws/core/Message.java
  17. 22 0
      src/main/java/cn/com/qmth/examcloud/ws/core/MessageHandler.java
  18. 97 0
      src/main/java/cn/com/qmth/examcloud/ws/core/MessageHandlerHolder.java
  19. 16 0
      src/main/java/cn/com/qmth/examcloud/ws/core/MessageIn.java
  20. 76 0
      src/main/java/cn/com/qmth/examcloud/ws/core/MessageOut.java
  21. 108 0
      src/main/java/cn/com/qmth/examcloud/ws/core/SessionHolder.java
  22. 17 0
      src/main/java/cn/com/qmth/examcloud/ws/core/WebSocketConfig.java
  23. 12 0
      src/main/java/cn/com/qmth/examcloud/ws/core/WebSocketHelper.java
  24. 249 0
      src/main/java/cn/com/qmth/examcloud/ws/core/WebSocketServerEndpoint.java
  25. 18 0
      src/main/java/cn/com/qmth/examcloud/ws/handler/TestHandler.java
  26. 8 0
      src/main/resources/application.properties
  27. 1 0
      src/main/resources/classpath.location
  28. 0 0
      src/main/resources/limited.properties
  29. 66 0
      src/main/resources/log4j2.xml
  30. 0 0
      src/main/resources/security.properties

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+.project
+.classpath
+.settings
+*.iml
+*.jar
+target/
+.idea/
+*test/
+logs/
+classes/
+
+

+ 1 - 0
README.md

@@ -0,0 +1 @@
+

+ 30 - 0
assembly.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+	<id>distribution</id>
+	<formats>
+		<format>zip</format>
+	</formats>
+	<fileSets>
+		<fileSet>
+			<directory>${project.basedir}/src/main/resources</directory>
+			<outputDirectory>/config</outputDirectory>
+		</fileSet>
+		<fileSet>
+			<directory>${project.basedir}/shell</directory>
+			<excludes>
+				<exclude>start.args</exclude>
+				<exclude>start.vmoptions</exclude>
+			</excludes>
+			<outputDirectory>/</outputDirectory>
+			<fileMode>0777</fileMode>
+		</fileSet>
+	</fileSets>
+	<dependencySets>
+		<dependencySet>
+			<useProjectArtifact>true</useProjectArtifact>
+			<outputDirectory>lib</outputDirectory>
+			<scope>runtime</scope>
+		</dependencySet>
+	</dependencySets>
+</assembly>

+ 4 - 0
jenkins-prod.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+pwd
+
+cp examcloud-basic-starter/target/examcloud-basic-distribution.zip ~/packages

+ 18 - 0
jenkins-test.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+pwd
+
+rm -rf ~/project/examcloud/examcloud-basic-distribution.zip
+rm -rf ~/project/examcloud/examcloud-basic/lib/
+
+cp examcloud-basic-starter/target/examcloud-basic-distribution.zip ~/project/examcloud/
+
+cd  ~/project/examcloud/
+unzip -o examcloud-basic-distribution.zip
+
+cd examcloud-basic
+echo "--spring.profiles.active=test --examcloud.startup.configCenterHost=localhost" > start.args
+echo "-server -Xms512m -Xmx512m  -XX:-UseGCOverheadLimit" > start.vmoptions
+
+bash stop.sh
+BUILD_ID=DONTKILLME
+bash start.sh jenkins

+ 26 - 0
pom.xml

@@ -0,0 +1,26 @@
+<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>cn.com.qmth.examcloud</groupId>
+		<artifactId>examcloud-parent</artifactId>
+		<version>2019</version>
+	</parent>
+	<artifactId>examcloud-ws-starter</artifactId>
+	<version>2019-SNAPSHOT</version>
+	<packaging>jar</packaging>
+
+	<dependencies>
+		<dependency>
+			<groupId>cn.com.qmth.examcloud</groupId>
+			<artifactId>examcloud-web</artifactId>
+			<version>${examcloud.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-websocket</artifactId>
+		</dependency>
+	</dependencies>
+
+</project>

+ 1 - 0
shell/start.args

@@ -0,0 +1 @@
+--spring.profiles.active=test

+ 38 - 0
shell/start.sh

@@ -0,0 +1,38 @@
+#!/bin/bash
+
+FILE_PATH=$(cd `dirname $0`; pwd)
+APP_VERSION=`cat $FILE_PATH/version`
+APP_MAIN_JAR="examcloud-basic-starter-"$APP_VERSION"-SNAPSHOT.jar"
+
+JAVA_OPTS=`cat $FILE_PATH/start.vmoptions`
+APP_ARGS=`cat $FILE_PATH/start.args`
+
+PID_LIST=`ps -ef|grep $APP_MAIN_JAR|grep java|awk '{print $2}'`
+
+if [ ! -z "$PID_LIST" ]; then
+    echo "[ERROR] : APP is already running!"
+    exit -1
+fi
+
+if [ "$1" ];then
+    echo "startupCode:"$1;
+else
+    echo "[ERROR] : no arguments"
+    exit -1
+fi
+
+APP_ARGS=$APP_ARGS" --examcloud.startup.startupCode="$1
+
+echo "java options:"
+echo "$JAVA_OPTS"
+echo "args:"
+echo "$APP_ARGS"
+    
+nohup java $JAVA_OPTS -jar $FILE_PATH/lib/$APP_MAIN_JAR $APP_ARGS >/dev/null 2>&1 &
+
+echo "starting......"
+
+exit 0
+
+
+

+ 1 - 0
shell/start.vmoptions

@@ -0,0 +1 @@
+-server -Xms512m -Xmx512m

+ 18 - 0
shell/stop.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+
+FILE_PATH=$(cd `dirname $0`; pwd)
+APP_VERSION=`cat $FILE_PATH/version`
+APP_MAIN_JAR="examcloud-basic-starter-"$APP_VERSION"-SNAPSHOT.jar"
+
+PID_LIST=`ps -ef|grep $APP_MAIN_JAR|grep java|awk '{print $2}'`
+
+if [ ! -z "$PID_LIST" ]; then
+    echo "Runnable jar is $APP_MAIN_JAR."
+    for PID in $PID_LIST 
+    do
+        kill -9 $PID
+    done
+    echo "stopped !"
+fi
+
+exit 0

+ 1 - 0
shell/version

@@ -0,0 +1 @@
+2020

+ 12 - 0
src/main/java/cn/com/qmth/examcloud/ws/Tester.java

@@ -0,0 +1,12 @@
+package cn.com.qmth.examcloud.ws;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class Tester {
+
+	public void test() {
+
+	}
+
+}

+ 52 - 0
src/main/java/cn/com/qmth/examcloud/ws/WebSocketApp.java

@@ -0,0 +1,52 @@
+package cn.com.qmth.examcloud.ws;
+
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+import cn.com.qmth.examcloud.web.bootstrap.AppBootstrap;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+@SpringBootApplication
+@Configuration
+@EnableDiscoveryClient
+@ComponentScan(basePackages = {"cn.com.qmth"})
+@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
+public class WebSocketApp {
+
+	static {
+		String runtimeLevel = System.getProperty("log.commonLevel");
+		if (null == runtimeLevel) {
+			System.setProperty("log.commonLevel", "DEBUG");
+		}
+
+	}
+
+	/**
+	 * main
+	 *
+	 * @author WANGWEI
+	 * @param args
+	 * @throws Exception
+	 */
+	public static void main(String[] args) throws Exception {
+
+		AppBootstrap.run(WebSocketApp.class, args);
+
+		test();
+	}
+
+	/**
+	 * 测试方法
+	 *
+	 * @author WANGWEI
+	 */
+	private static void test() {
+		Tester tester = SpringContextHolder.getBean(Tester.class);
+		tester.test();
+	}
+
+}

+ 21 - 0
src/main/java/cn/com/qmth/examcloud/ws/api/controller/WebSocketController.java

@@ -0,0 +1,21 @@
+package cn.com.qmth.examcloud.ws.api.controller;
+
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import cn.com.qmth.examcloud.web.support.ControllerSupport;
+import cn.com.qmth.examcloud.ws.core.MessageOut;
+
+@RestController
+@RequestMapping("api/ctr/ws")
+public class WebSocketController extends ControllerSupport {
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.POST)
+	public MessageOut post(@RequestBody MessageOut messageOut) {
+		
+		return messageOut;
+	}
+
+}

+ 9 - 0
src/main/java/cn/com/qmth/examcloud/ws/api/provider/WebSocketCloudServiceProvider.java

@@ -0,0 +1,9 @@
+package cn.com.qmth.examcloud.ws.api.provider;
+
+import cn.com.qmth.examcloud.api.commons.CloudService;
+
+public class WebSocketCloudServiceProvider implements CloudService {
+
+	private static final long serialVersionUID = 3834780892191700611L;
+
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/Message.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 接口ID 注解
+ *
+ * @author WANGWEI
+ * @date 2019年3月15日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Target({ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Message {
+
+	String value() default "";
+}

+ 22 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/MessageHandler.java

@@ -0,0 +1,22 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 接口ID 注解
+ *
+ * @author WANGWEI
+ * @date 2019年3月15日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MessageHandler {
+
+	String value() default "";
+}

+ 97 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/MessageHandlerHolder.java

@@ -0,0 +1,97 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * 消息处理器 管理器
+ *
+ * @author WANGWEI
+ * @date 2019年11月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+@Order(100)
+public class MessageHandlerHolder implements ApplicationRunner {
+
+	private static Map<String, Method> methodMap = Maps.newConcurrentMap();
+
+	private static Map<String, Object> beanMap = Maps.newConcurrentMap();
+
+	public static Method getMethod(String path) {
+		return methodMap.get(path);
+	}
+
+	public static Object getBean(String path) {
+		return beanMap.get(path);
+	}
+
+	@Override
+	public void run(ApplicationArguments args) throws Exception {
+
+		Map<String, Object> map = SpringContextHolder.getApplicationContext()
+				.getBeansWithAnnotation(MessageHandler.class);
+
+		for (Object bean : map.values()) {
+			MessageHandler messageHandlerOfClass = AnnotationUtils.findAnnotation(bean.getClass(),
+					MessageHandler.class);
+			String valueOfClass = messageHandlerOfClass.value();
+
+			Method[] methods = bean.getClass().getDeclaredMethods();
+
+			for (Method method : methods) {
+				MessageHandler messageHandlerOfMethod = method.getAnnotation(MessageHandler.class);
+				if (null == messageHandlerOfMethod) {
+					continue;
+				}
+				String valueOfMethod = messageHandlerOfMethod.value();
+
+				String path = join(valueOfClass, valueOfMethod);
+				methodMap.put(path, method);
+				beanMap.put(path, bean);
+			}
+
+		}
+
+	}
+
+	private String join(String valueOfClass, String valueOfMethod) {
+		valueOfClass = valueOfClass.trim();
+		valueOfMethod = valueOfMethod.trim();
+
+		if (valueOfClass.startsWith("/")) {
+			valueOfClass = valueOfClass.substring(1);
+		}
+		if (valueOfClass.endsWith("/")) {
+			valueOfClass = valueOfClass.substring(0, valueOfClass.length() - 1);
+		}
+		if (valueOfMethod.startsWith("/")) {
+			valueOfMethod = valueOfMethod.substring(1);
+		}
+		if (valueOfMethod.endsWith("/")) {
+			valueOfMethod = valueOfMethod.substring(0, valueOfMethod.length() - 1);
+		}
+
+		if (StringUtils.isNotEmpty(valueOfClass) && StringUtils.isNotEmpty(valueOfMethod)) {
+			return valueOfClass + "/" + valueOfMethod;
+		} else if (StringUtils.isNotEmpty(valueOfClass)) {
+			return valueOfClass;
+		} else if (StringUtils.isNotEmpty(valueOfMethod)) {
+			return valueOfMethod;
+		} else {
+			return "";
+		}
+	}
+
+}

+ 16 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/MessageIn.java

@@ -0,0 +1,16 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 客户端消息
+ *
+ * @author WANGWEI
+ * @date 2019年11月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class MessageIn implements JsonSerializable {
+
+	private static final long serialVersionUID = -3461721988846081124L;
+
+}

+ 76 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/MessageOut.java

@@ -0,0 +1,76 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
+
+/**
+ * 服务端消息
+ *
+ * @author WANGWEI
+ * @date 2019年11月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class MessageOut implements JsonSerializable {
+
+	private static final long serialVersionUID = -2255327250550304236L;
+
+	private String path;
+
+	private int status = 200;
+
+	private String statusCode;
+
+	private String statusDesc;
+
+	private String eventId;
+
+	private Object content;
+
+	public String getPath() {
+		return path;
+	}
+
+	public void setPath(String path) {
+		this.path = path;
+	}
+
+	public int getStatus() {
+		return status;
+	}
+
+	public void setStatus(int status) {
+		this.status = status;
+	}
+
+	public String getStatusCode() {
+		return statusCode;
+	}
+
+	public void setStatusCode(String statusCode) {
+		this.statusCode = statusCode;
+	}
+
+	public String getStatusDesc() {
+		return statusDesc;
+	}
+
+	public void setStatusDesc(String statusDesc) {
+		this.statusDesc = statusDesc;
+	}
+
+	public String getEventId() {
+		return eventId;
+	}
+
+	public void setEventId(String eventId) {
+		this.eventId = eventId;
+	}
+
+	public Object getContent() {
+		return content;
+	}
+
+	public void setContent(Object content) {
+		this.content = content;
+	}
+
+}

+ 108 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/SessionHolder.java

@@ -0,0 +1,108 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import java.util.Map;
+
+import javax.websocket.Session;
+
+import org.apache.commons.io.IOUtils;
+
+import com.google.common.collect.Maps;
+
+/**
+ * websocket session管理
+ *
+ * @author WANGWEI
+ * @date 2019年11月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class SessionHolder {
+
+	private static final Map<String, Session> ID_SESESION_MAP = Maps.newConcurrentMap();
+
+	private static final Map<Session, String> SESESION_ID_MAP = Maps.newConcurrentMap();
+
+	private static final Map<Session, String> SESSION_KEY_MAP = Maps.newConcurrentMap();
+
+	private static final Map<Session, String> SESSION_TOKEN_MAP = Maps.newConcurrentMap();
+
+	/**
+	 * 重新设置session
+	 *
+	 * @author WANGWEI
+	 * @param path
+	 * @param key
+	 * @param token
+	 * @param session
+	 */
+	public static void setSession(String path, String key, String token, Session session) {
+		String sessionId = key + ":" + path;
+		Session oldSession = ID_SESESION_MAP.get(sessionId);
+		if (null != oldSession && !oldSession.equals(session)) {
+			SESESION_ID_MAP.remove(oldSession);
+			SESSION_KEY_MAP.remove(oldSession);
+			SESSION_TOKEN_MAP.remove(oldSession);
+			IOUtils.closeQuietly(oldSession);
+		}
+
+		ID_SESESION_MAP.put(sessionId, session);
+		SESESION_ID_MAP.put(session, sessionId);
+
+		SESSION_KEY_MAP.put(session, key);
+		SESSION_TOKEN_MAP.put(session, token);
+	}
+
+	/**
+	 * 删除 session
+	 *
+	 * @author WANGWEI
+	 * @param session
+	 */
+	public static void delSession(Session session) {
+		String sessionId = SESESION_ID_MAP.get(session);
+		if (null != sessionId) {
+			ID_SESESION_MAP.remove(sessionId);
+		}
+
+		SESESION_ID_MAP.remove(session);
+		SESSION_KEY_MAP.remove(session);
+		SESSION_TOKEN_MAP.remove(session);
+
+		IOUtils.closeQuietly(session);
+	}
+
+	/**
+	 * 获取 session
+	 *
+	 * @author WANGWEI
+	 * @param path
+	 * @param key
+	 * @return
+	 */
+	public Session getSession(String path, String key) {
+		String sessionId = key + ":" + path;
+		Session session = ID_SESESION_MAP.get(sessionId);
+		return session;
+	}
+
+	/**
+	 * 获取 key
+	 *
+	 * @author WANGWEI
+	 * @param session
+	 * @return
+	 */
+	public static String getKey(Session session) {
+		return SESSION_KEY_MAP.get(session);
+	}
+
+	/**
+	 * 获取 token
+	 *
+	 * @author WANGWEI
+	 * @param session
+	 * @return
+	 */
+	public static String getToken(Session session) {
+		return SESSION_TOKEN_MAP.get(session);
+	}
+}

+ 17 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/WebSocketConfig.java

@@ -0,0 +1,17 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig {
+
+	@Bean
+	public ServerEndpointExporter serverEndpointExporter() {
+		return new ServerEndpointExporter();
+	}
+
+}

+ 12 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/WebSocketHelper.java

@@ -0,0 +1,12 @@
+package cn.com.qmth.examcloud.ws.core;
+
+/**
+ * WebSocket helper
+ *
+ * @author WANGWEI
+ * @date 2019年11月22日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class WebSocketHelper {
+
+}

+ 249 - 0
src/main/java/cn/com/qmth/examcloud/ws/core/WebSocketServerEndpoint.java

@@ -0,0 +1,249 @@
+package cn.com.qmth.examcloud.ws.core;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * websocket ServerEndpoint
+ *
+ * @author WANGWEI
+ * @date 2019年11月27日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@ServerEndpoint("/{path}")
+@Component
+public class WebSocketServerEndpoint {
+
+	private static final ExamCloudLog WS_LOG = ExamCloudLogFactory.getLog("WS_LOGGER");
+
+	private static RedisClient redisClient;
+
+	private static RedisClient getRedisClient() {
+		if (null != redisClient) {
+			return redisClient;
+		}
+		redisClient = SpringContextHolder.getBean(RedisClient.class);
+		return redisClient;
+	}
+
+	@OnOpen
+	public void onOpen(Session session, @PathParam("path") String path) {
+
+		if (WS_LOG.isDebugEnabled()) {
+			WS_LOG.debug("[onOpen]. path=" + path + "");
+		}
+
+		MessageOut out = new MessageOut();
+		out.setStatus(200);
+		out.setPath(path);
+
+		Method method = MessageHandlerHolder.getMethod(path);
+		if (null == method) {
+			out.setStatus(500);
+			out.setStatusCode("500");
+			out.setStatusDesc("path is wrong.");
+			sendText(session, path, out);
+			IOUtils.closeQuietly(session);
+			return;
+		}
+
+		String key = getRequestParameter(session, "key");
+		String token = getRequestParameter(session, "token");
+
+		if (StringUtils.isBlank(key)) {
+			WS_LOG.error("[onOpen-FAIL]. path=" + path + ". key is blank");
+			IOUtils.closeQuietly(session);
+			return;
+		}
+		if (StringUtils.isBlank(token)) {
+			WS_LOG.error("[onOpen-FAIL]. path=" + path + ". token is blank");
+			IOUtils.closeQuietly(session);
+			return;
+		}
+
+		User user = getRedisClient().get(key, User.class);
+
+		if (null == user) {
+			out.setStatus(403);
+			out.setStatusCode("403");
+			out.setStatusDesc("no login");
+			sendText(session, path, out);
+			IOUtils.closeQuietly(session);
+			return;
+		} else if (!token.equals(user.getToken())) {
+			out.setStatus(403);
+			out.setStatusCode("403");
+			out.setStatusDesc("token is wrong");
+			sendText(session, path, out);
+			IOUtils.closeQuietly(session);
+			return;
+		}
+
+		out.setStatusCode("200");
+		out.setStatusDesc("success");
+		sendText(session, path, out);
+
+		SessionHolder.setSession(path, user.getKey(), user.getToken(), session);
+	}
+
+	/**
+	 * 发送消息
+	 *
+	 * @author WANGWEI
+	 * @param session
+	 * @param path
+	 * @param message
+	 */
+	private void sendText(Session session, String path, MessageOut out) {
+		try {
+			String message = JsonUtil.toJson(out);
+			if (WS_LOG.isDebugEnabled()) {
+				WS_LOG.error("[sendText]. path=" + path + "; message=" + message);
+			}
+			session.getBasicRemote().sendText(message);
+		} catch (Exception e) {
+			WS_LOG.error("[sendText-FAIL]. path=" + path + "", e);
+			IOUtils.closeQuietly(session);
+			return;
+		}
+	}
+
+	/**
+	 * 获取请求参数
+	 *
+	 * @author WANGWEI
+	 * @param session
+	 * @param key
+	 * @return
+	 */
+	private String getRequestParameter(Session session, String key) {
+		Map<String, List<String>> params = session.getRequestParameterMap();
+		List<String> list = params.get(key);
+
+		if (CollectionUtils.isEmpty(list)) {
+			return null;
+		}
+		String value = StringUtils.join(list, ",");
+		return value;
+	}
+
+	@OnClose
+	public void onClose(Session session, @PathParam("path") String path) {
+		WS_LOG.debug("[onClose]. path=" + path);
+		SessionHolder.delSession(session);
+	}
+
+	@OnMessage
+	public void onMessage(Session session, @PathParam("path") String path, String message) {
+		WS_LOG.debug("[onMessage]. path=" + path + ". message=" + message);
+		String key = SessionHolder.getKey(session);
+		String token = SessionHolder.getToken(session);
+
+		User user = getRedisClient().get(key, User.class);
+
+		MessageOut out = new MessageOut();
+		out.setStatus(200);
+		out.setPath(path);
+
+		if (null == user) {
+			out.setStatus(403);
+			out.setStatusCode("403");
+			out.setStatusDesc("no login");
+			sendText(session, path, out);
+			IOUtils.closeQuietly(session);
+			return;
+		} else if (!token.equals(user.getToken())) {
+			out.setStatus(403);
+			out.setStatusCode("403");
+			out.setStatusDesc("token is wrong");
+			sendText(session, path, out);
+			IOUtils.closeQuietly(session);
+			return;
+		}
+
+		Object result = null;
+		try {
+
+			Method method = MessageHandlerHolder.getMethod(path);
+			Object bean = MessageHandlerHolder.getBean(path);
+
+			Annotation[][] an2 = method.getParameterAnnotations();
+
+			boolean hasMessageParam = false;
+			int messageParamIndex = 0;
+			OUTER : for (int i = 0; i < an2.length; i++) {
+				Annotation[] an1 = an2[i];
+				for (Annotation an : an1) {
+					if (an.annotationType().equals(Message.class)) {
+						hasMessageParam = true;
+						messageParamIndex = i;
+						break OUTER;
+					}
+				}
+			}
+
+			Class<?>[] parameterTypes = method.getParameterTypes();
+			Object[] args = new Object[parameterTypes.length];
+			for (int i = 0; i < parameterTypes.length; i++) {
+				Class<?> curType = parameterTypes[i];
+				if (hasMessageParam && i == messageParamIndex) {
+					args[i] = JsonUtil.fromJson(message, curType);
+					continue;
+				}
+				if (curType.equals(User.class)) {
+					args[i] = user;
+					continue;
+				}
+			}
+
+			result = method.invoke(bean, args);
+
+			out.setStatusCode("200");
+			out.setStatusDesc("success");
+			out.setContent(result);
+
+		} catch (StatusException e) {
+			WS_LOG.error("[onMessage-FAIL]. path=" + path + "", e);
+			out.setStatus(500);
+			out.setStatusCode(e.getCode());
+			out.setStatusDesc(e.getDesc());
+		} catch (Exception e) {
+			WS_LOG.error("[onMessage-FAIL]. path=" + path + "", e);
+			out.setStatus(500);
+			out.setStatusCode("500");
+			out.setStatusDesc("系统异常");
+		}
+
+		sendText(session, path, out);
+	}
+
+	@OnError
+	public void onError(Session session, @PathParam("path") String path, Throwable t) {
+		WS_LOG.error("[onError]. path=" + path, t);
+		SessionHolder.delSession(session);
+	}
+
+}

+ 18 - 0
src/main/java/cn/com/qmth/examcloud/ws/handler/TestHandler.java

@@ -0,0 +1,18 @@
+package cn.com.qmth.examcloud.ws.handler;
+
+import org.springframework.stereotype.Component;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import cn.com.qmth.examcloud.ws.core.Message;
+import cn.com.qmth.examcloud.ws.core.MessageHandler;
+
+@Component
+@MessageHandler("test")
+public class TestHandler {
+
+	@MessageHandler
+	public String test(User user, @Message String message) {
+		return "fuck you";
+	}
+
+}

+ 8 - 0
src/main/resources/application.properties

@@ -0,0 +1,8 @@
+spring.profiles.active=test
+
+examcloud.startup.startupCode=8010
+examcloud.startup.configCenterHost=127.0.0.1
+examcloud.startup.configCenterPort=9999
+examcloud.startup.appCode=WS
+
+

+ 1 - 0
src/main/resources/classpath.location

@@ -0,0 +1 @@
+classpath 定位文件

+ 0 - 0
src/main/resources/limited.properties


+ 66 - 0
src/main/resources/log4j2.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN" monitorInterval="30">
+
+	<Properties>
+		<Property name="commonLevel" value="${sys:log.commonLevel}" />
+	</Properties>
+
+	<Appenders>
+		<!-- 控制台 日志 -->
+		<Console name="Console" target="SYSTEM_OUT">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+		</Console>
+		<!-- debug 日志 -->
+		<RollingFile name="DEBUG_APPENDER" fileName="./logs/debug/debug.log"
+			filePattern="./logs/debug/debug-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100 MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/debug" maxDepth="1">
+					<IfFileName glob="debug-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+		<!-- WebSocket 日志 -->
+		<RollingFile name="WS_APPENDER" fileName="./logs/debug/debug.log"
+			filePattern="./logs/debug/debug-%d{yyyy.MM.dd.HH}-%i.log">
+			<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}| %level | %X{TRACE_ID} - %X{CALLER} | %m | %l%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" />
+				<SizeBasedTriggeringPolicy size="100 MB" />
+			</Policies>
+			<DefaultRolloverStrategy max="10000">
+				<Delete basePath="./logs/debug" maxDepth="1">
+					<IfFileName glob="debug-*.log">
+						<IfAccumulatedFileSize exceeds="2 GB" />
+					</IfFileName>
+				</Delete>
+			</DefaultRolloverStrategy>
+		</RollingFile>
+	</Appenders>
+
+	<Loggers>
+		<Logger name="cn.com.qmth" level="${commonLevel}" additivity="false">
+			<AppenderRef ref="DEBUG_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="WS_LOGGER" level="DEBUG" additivity="false">
+			<AppenderRef ref="WS_APPENDER" />
+			<AppenderRef ref="Console" />
+		</Logger>
+
+		<Logger name="cn.com.qmth.examcloud.web.actuator.ApiStatusInfoHolder" level="ERROR" />
+
+		<Root level="INFO">
+			<AppenderRef ref="Console" />
+			<AppenderRef ref="DEBUG_APPENDER" />
+		</Root>
+	</Loggers>
+
+</Configuration>

+ 0 - 0
src/main/resources/security.properties