WANG 6 年 前
コミット
61d72a2fe5
37 ファイル変更1974 行追加1523 行削除
  1. 47 27
      pom.xml
  2. 0 62
      src/main/java/cn/com/qmth/examcloud/web/boot/BootSecurityManager.java
  3. 0 334
      src/main/java/cn/com/qmth/examcloud/web/boot/ExamCloudApp.java
  4. 308 0
      src/main/java/cn/com/qmth/examcloud/web/bootstrap/AppBootstrap.java
  5. 115 0
      src/main/java/cn/com/qmth/examcloud/web/bootstrap/BootstrapSecurityUtil.java
  6. 120 0
      src/main/java/cn/com/qmth/examcloud/web/bootstrap/PropertyHolder.java
  7. 37 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelf.java
  8. 44 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelfHolder.java
  9. 141 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientConfiguration.java
  10. 115 45
      src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientSupport.java
  11. 56 0
      src/main/java/cn/com/qmth/examcloud/web/cloud/RibbonClientsConfiguration.java
  12. 43 0
      src/main/java/cn/com/qmth/examcloud/web/config/LogProperties.java
  13. 47 140
      src/main/java/cn/com/qmth/examcloud/web/interceptor/FirstInterceptor.java
  14. 0 124
      src/main/java/cn/com/qmth/examcloud/web/interceptor/SeqlockInterceptor.java
  15. 83 0
      src/main/java/cn/com/qmth/examcloud/web/redis/CustomRedisConfiguration.java
  16. 21 5
      src/main/java/cn/com/qmth/examcloud/web/redis/RedisClient.java
  17. 0 75
      src/main/java/cn/com/qmth/examcloud/web/redis/RedisClientImpl.java
  18. 132 0
      src/main/java/cn/com/qmth/examcloud/web/redis/SimpleRedisClient.java
  19. 0 70
      src/main/java/cn/com/qmth/examcloud/web/reports/BaseReport.java
  20. 0 33
      src/main/java/cn/com/qmth/examcloud/web/reports/ReportFileFilter.java
  21. 0 131
      src/main/java/cn/com/qmth/examcloud/web/reports/ReportLoggerFactory.java
  22. 0 36
      src/main/java/cn/com/qmth/examcloud/web/reports/ReportsCollector.java
  23. 0 139
      src/main/java/cn/com/qmth/examcloud/web/reports/ReportsController.java
  24. 46 0
      src/main/java/cn/com/qmth/examcloud/web/security/ResourceManager.java
  25. 108 18
      src/main/java/cn/com/qmth/examcloud/web/security/SpringCloudInterceptor.java
  26. 6 5
      src/main/java/cn/com/qmth/examcloud/web/support/ApiId.java
  27. 108 0
      src/main/java/cn/com/qmth/examcloud/web/support/ApiInfo.java
  28. 158 0
      src/main/java/cn/com/qmth/examcloud/web/support/ApiInfoHolder.java
  29. 90 56
      src/main/java/cn/com/qmth/examcloud/web/support/ControllerAspect.java
  30. 62 0
      src/main/java/cn/com/qmth/examcloud/web/support/ExamCloudController.java
  31. 43 0
      src/main/java/cn/com/qmth/examcloud/web/support/HttpMethodProcessor.java
  32. 0 42
      src/main/java/cn/com/qmth/examcloud/web/support/RemoteProcedureCallTester.java
  33. 0 69
      src/main/java/cn/com/qmth/examcloud/web/support/ResponseStatus.java
  34. 0 11
      src/main/java/cn/com/qmth/examcloud/web/support/StatusResponse.java
  35. 36 0
      src/main/java/cn/com/qmth/examcloud/web/support/StringTrimModule.java
  36. 0 96
      src/main/java/cn/com/qmth/examcloud/web/support/SystemController.java
  37. 8 5
      src/main/java/cn/com/qmth/examcloud/web/support/WithoutStackTrace.java

+ 47 - 27
pom.xml

@@ -8,6 +8,7 @@
 	</parent>
 	<artifactId>examcloud-web</artifactId>
 	<version>2019-SNAPSHOT</version>
+	<packaging>jar</packaging>
 
 	<dependencies>
 		<dependency>
@@ -21,65 +22,71 @@
 			<version>${examcloud.version}</version>
 		</dependency>
 
-		<dependency>
-			<groupId>org.springframework.cloud</groupId>
-			<artifactId>spring-cloud-starter</artifactId>
-		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-test</artifactId>
-			<scope>test</scope>
-		</dependency>
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-actuator</artifactId>
+			<artifactId>spring-boot-starter-data-jpa</artifactId>
+			<exclusions>
+				<exclusion>
+					<groupId>org.springframework.boot</groupId>
+					<artifactId>spring-boot-starter-logging</artifactId>
+				</exclusion>
+			</exclusions>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-web</artifactId>
-		</dependency>
-
-		<dependency>
-			<groupId>mysql</groupId>
-			<artifactId>mysql-connector-java</artifactId>
+			<exclusions>
+				<exclusion>
+					<groupId>org.springframework.boot</groupId>
+					<artifactId>spring-boot-starter-logging</artifactId>
+				</exclusion>
+			</exclusions>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-data-jpa</artifactId>
+			<artifactId>spring-boot-starter-data-rest</artifactId>
 		</dependency>
 		<dependency>
-			<groupId>org.hibernate</groupId>
-			<artifactId>hibernate-validator</artifactId>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
 		</dependency>
 		<dependency>
-			<groupId>org.springframework.cloud</groupId>
-			<artifactId>spring-cloud-starter-feign</artifactId>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-jdbc</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-test</artifactId>
-			<scope>test</scope>
+			<artifactId>spring-boot-starter-data-redis</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-autoconfigure</artifactId>
 		</dependency>
+
 		<dependency>
 			<groupId>org.springframework.cloud</groupId>
-			<artifactId>spring-cloud-starter-feign</artifactId>
+			<artifactId>spring-cloud-starter</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.cloud</groupId>
-			<artifactId>spring-cloud-starter-eureka</artifactId>
+			<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
 		</dependency>
 		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-web</artifactId>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-data-redis</artifactId>
+			<artifactId>spring-boot-configuration-processor</artifactId>
+			<optional>true</optional>
 		</dependency>
+
+		<dependency>
+			<groupId>com.h2database</groupId>
+			<artifactId>h2</artifactId>
+			<scope>runtime</scope>
+		</dependency>
+
 		<dependency>
 			<groupId>io.springfox</groupId>
 			<artifactId>springfox-swagger2</artifactId>
@@ -88,6 +95,19 @@
 			<groupId>io.springfox</groupId>
 			<artifactId>springfox-swagger-ui</artifactId>
 		</dependency>
+
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>druid</artifactId>
+			<version>${druid.version}</version>
+			<exclusions>
+				<exclusion>
+					<groupId>log4j</groupId>
+					<artifactId>log4j</artifactId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+
 	</dependencies>
 
 </project>

+ 0 - 62
src/main/java/cn/com/qmth/examcloud/web/boot/BootSecurityManager.java

@@ -1,62 +0,0 @@
-package cn.com.qmth.examcloud.web.boot;
-
-/**
- * 安全处理器
- *
- * @author WANGWEI
- * @date 2018年12月5日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-
-public class BootSecurityManager {
-
-	private static BootSecurityManager mgr;
-
-	private static final Object LOCK = new Object();
-
-	private String secretKey;
-
-	private String active;
-
-	/**
-	 * 构造函数
-	 */
-	private BootSecurityManager() {
-	}
-
-	/**
-	 * 获取单例
-	 *
-	 * @author WANGWEI
-	 * @return
-	 */
-	public static BootSecurityManager getInstance() {
-		if (null == mgr) {
-			synchronized (LOCK) {
-				if (null == mgr) {
-					mgr = new BootSecurityManager();
-				}
-				return mgr;
-			}
-		} else {
-			return mgr;
-		}
-	}
-
-	public String getSecretKey() {
-		return secretKey;
-	}
-
-	public void setSecretKey(String secretKey) {
-		this.secretKey = secretKey;
-	}
-
-	public String getActive() {
-		return active;
-	}
-
-	public void setActive(String active) {
-		this.active = active;
-	}
-
-}

+ 0 - 334
src/main/java/cn/com/qmth/examcloud/web/boot/ExamCloudApp.java

@@ -1,334 +0,0 @@
-package cn.com.qmth.examcloud.web.boot;
-
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Properties;
-import java.util.Set;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.util.EntityUtils;
-import org.springframework.boot.SpringApplication;
-import org.springframework.context.ConfigurableApplicationContext;
-
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-
-import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
-import cn.com.qmth.examcloud.commons.util.HttpClientPool;
-import cn.com.qmth.examcloud.commons.util.HttpClientUtil;
-import cn.com.qmth.examcloud.commons.util.JsonUtil;
-import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
-import cn.com.qmth.examcloud.commons.util.SecurityUtil;
-import cn.com.qmth.examcloud.commons.util.Util;
-
-/**
- * 启动器<br>
- * 
- * 配置文件优先级:参数>配置中心>本地配置
- *
- * @author WANGWEI
- * @date 2018年12月3日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-public class ExamCloudApp {
-
-	/**
-	 * 启动配置中心
-	 *
-	 * @author WANGWEI
-	 * @param source
-	 * @param password
-	 * @return
-	 */
-	public static ConfigurableApplicationContext runConfigServer(Object source, String... args) {
-		Properties props = new Properties();
-		PropertiesUtil.loadFromResource("application.properties", props);
-
-		String active = null;
-		String password = null;
-		if (null != args) {
-			for (String s : args) {
-				s = s.trim();
-				if (s.startsWith("--startup.password=")) {
-					password = s.substring(s.indexOf("=") + 1);
-				}
-				if (s.startsWith("--spring.profiles.active=")) {
-					active = s.substring(s.indexOf("=") + 1);
-				}
-			}
-		}
-
-		if (null == active) {
-			String value = props.getProperty("spring.profiles.active");
-			if (null != value) {
-				active = value.trim();
-			}
-		}
-		System.out.println("active=" + active);
-		if (StringUtils.isBlank(active)) {
-			throw new ExamCloudRuntimeException("active is not specified");
-		}
-
-		if (StringUtils.isBlank(password)) {
-			throw new ExamCloudRuntimeException("password is not specified");
-		}
-
-		PropertiesUtil.loadFromResource(active + "/application.properties", props);
-		PropertiesUtil.loadFromResource(active + "/application-config.properties", props);
-
-		String testPassword = props.getProperty("$encrypted.$testPassword");
-		if (StringUtils.isBlank(testPassword)) {
-			throw new ExamCloudRuntimeException(
-					"property[$encrypted.$testPassword] is not configured");
-		}
-		try {
-			SecurityUtil.decrypt(testPassword, password);
-		} catch (Exception e) {
-			throw new ExamCloudRuntimeException("password is wrong!");
-		}
-
-		SecurityUtil.decrypt(props, password);
-
-		Set<String> argSet = Sets.newLinkedHashSet();
-		for (Entry<Object, Object> p : props.entrySet()) {
-			PropertiesUtil.setProperty((String) p.getKey(), (String) p.getValue());
-			String arg = "--" + p.getKey() + "=" + p.getValue();
-			argSet.add(arg);
-		}
-
-		String[] newArgs = argSet.toArray(new String[argSet.size()]);
-
-		BootSecurityManager.getInstance().setSecretKey(password);
-		BootSecurityManager.getInstance().setActive(active);
-
-		return SpringApplication.run(source, newArgs);
-	}
-
-	/**
-	 * 启动
-	 *
-	 * @author WANGWEI
-	 * @param source
-	 * @param appSimpleName
-	 * @param args
-	 * @return
-	 */
-	public static ConfigurableApplicationContext run(Object source, String appSimpleName,
-			String... args) {
-
-		Properties props = new Properties();
-		PropertiesUtil.loadFromResource("application.properties", props);
-
-		String securityCode = null;
-		String active = null;
-		String host = null;
-		String port = null;
-		if (null != args) {
-			for (String s : args) {
-				s = s.trim();
-				if (s.startsWith("--startup.securityCode=")) {
-					securityCode = s.substring(s.indexOf("=") + 1);
-				}
-				if (s.startsWith("--spring.profiles.active=")) {
-					active = s.substring(s.indexOf("=") + 1);
-				}
-				if (s.startsWith("--config.server.host=")) {
-					host = s.substring(s.indexOf("=") + 1);
-				}
-				if (s.startsWith("--config.server.port=")) {
-					port = s.substring(s.indexOf("=") + 1);
-				}
-			}
-		}
-		if (null == active) {
-			String value = props.getProperty("spring.profiles.active");
-			if (null != value) {
-				active = value.trim();
-			}
-		}
-
-		System.out.println("active=" + active);
-		if (StringUtils.isBlank(active)) {
-			throw new ExamCloudRuntimeException("active is not specified");
-		}
-
-		if (null == host) {
-			host = props.getProperty("config.server.host");
-		}
-		System.out.println("config server host=" + host);
-
-		if (null == port) {
-			port = props.getProperty("config.server.port", "9999");
-		}
-		System.out.println("config server port=" + port);
-
-		if ("dev".equals(active)) {
-			securityCode = "SB";
-		}
-
-		if (null == securityCode) {
-			System.out.println("securityCode is null");
-			throw new ExamCloudRuntimeException("securityCode is not specified");
-		} else {
-			System.out.println("securityCode=" + securityCode);
-		}
-
-		if (StringUtils.isBlank(host)) {
-			throw new ExamCloudRuntimeException("host is not specified");
-		}
-
-		String url = "http://" + host + ":" + port + "/properties/" + appSimpleName + "/" + active;
-		Map<String, String> pairs = getProperties(url, securityCode);
-
-		SecurityUtil.decrypt(pairs, securityCode);
-
-		if (null != args) {
-			for (String arg : args) {
-				arg = arg.trim();
-				if (arg.startsWith("--")) {
-					String key = arg.substring(2, arg.indexOf("="));
-					String value = arg.substring(arg.indexOf("=") + 1);
-					pairs.put(key, value);
-				}
-			}
-		}
-
-		setSystemProperties(pairs);
-
-		Set<String> argSet = Sets.newLinkedHashSet();
-		for (Entry<String, String> p : pairs.entrySet()) {
-			PropertiesUtil.setProperty(p.getKey(), p.getValue());
-			String arg = "--" + p.getKey() + "=" + p.getValue();
-			argSet.add(arg);
-		}
-
-		BootSecurityManager.getInstance().setActive(active);
-
-		String[] newArgs = argSet.toArray(new String[argSet.size()]);
-
-		String sendStartupStatusUrl = "http://" + host + ":" + port + "/startupStatus/"
-				+ appSimpleName + "/" + active;
-		try {
-			ConfigurableApplicationContext context = SpringApplication.run(source, newArgs);
-			sendStartupStatus(sendStartupStatusUrl, securityCode, "success");
-
-			return context;
-		} catch (Exception e) {
-			sendStartupStatus(sendStartupStatusUrl, securityCode,
-					"failure\n" + Util.getStackTrace(e));
-			throw e;
-		}
-
-	}
-
-	/**
-	 * 设置系统属性
-	 *
-	 * @author WANGWEI
-	 * @param pairs
-	 */
-	private static void setSystemProperties(Map<String, String> pairs) {
-		for (Entry<String, String> p : pairs.entrySet()) {
-			String key = p.getKey();
-			String value = p.getValue();
-			if (key.endsWith("$log.level.default")) {
-				System.setProperty("logLevel", value);
-			} else if (key.endsWith("$log.rootPath")) {
-				System.setProperty("logRootPath", value);
-			}
-		}
-	}
-
-	/**
-	 * POST 表单请求
-	 *
-	 * @author WANGWEI
-	 * @param url
-	 * @return
-	 */
-	private static Map<String, String> getProperties(String url, String securityCode) {
-		Map<String, String> headers = Maps.newHashMap();
-		headers.put("securityCode", securityCode);
-		CloseableHttpResponse response = null;
-		String respBody = null;
-		try {
-			response = post(url, headers, null);
-			respBody = EntityUtils.toString(response.getEntity(), "UTF-8");
-		} catch (Exception e) {
-			throw new ExamCloudRuntimeException(e);
-		} finally {
-			HttpClientUtil.close(response);
-		}
-
-		if (org.apache.http.HttpStatus.SC_OK != response.getStatusLine().getStatusCode()) {
-			System.out.println("fail to get properties from config server");
-			throw new ExamCloudRuntimeException("fail to get properties from config server");
-		}
-
-		respBody = SecurityUtil.decrypt(respBody, securityCode);
-
-		@SuppressWarnings("unchecked")
-		Map<String, String> map = JsonUtil.fromJson(respBody, Map.class);
-		return map;
-	}
-
-	/**
-	 * 发送启动状态
-	 *
-	 * @author WANGWEI
-	 * @param url
-	 * @param securityCode
-	 * @param body
-	 */
-	private static void sendStartupStatus(String url, String securityCode, String body) {
-		Map<String, String> headers = Maps.newHashMap();
-		headers.put("securityCode", securityCode);
-		CloseableHttpResponse response = null;
-		try {
-			response = post(url, headers, body);
-			EntityUtils.toString(response.getEntity(), "UTF-8");
-		} catch (Exception e) {
-			throw new ExamCloudRuntimeException(e);
-		} finally {
-			HttpClientUtil.close(response);
-		}
-	}
-
-	/**
-	 * POST
-	 *
-	 * @author WANGWEI
-	 * @param url
-	 * @param headers
-	 * @param body
-	 * @return
-	 */
-	private static CloseableHttpResponse post(String url, Map<String, String> headers,
-			String body) {
-
-		CloseableHttpClient httpclient = HttpClientPool.getHttpClient();
-		HttpPost post = new HttpPost(url);
-		post.setConfig(RequestConfig.custom().setConnectTimeout(10000).build());
-		post.setConfig(RequestConfig.custom().setConnectionRequestTimeout(1000 * 60 * 2).build());
-
-		for (Map.Entry<String, String> entry : headers.entrySet()) {
-			post.addHeader(entry.getKey(), entry.getValue());
-		}
-
-		try {
-			if (null != body) {
-				post.setEntity(new StringEntity(body, "UTF-8"));
-			}
-			CloseableHttpResponse response = httpclient.execute(post);
-			return response;
-		} catch (Exception e) {
-			throw new ExamCloudRuntimeException(e);
-		}
-	}
-
-}

+ 308 - 0
src/main/java/cn/com/qmth/examcloud/web/bootstrap/AppBootstrap.java

@@ -0,0 +1,308 @@
+package cn.com.qmth.examcloud.web.bootstrap;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.ThreadContext;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.http.HttpStatus;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.HttpMethod;
+import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.OKHttpUtil;
+import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
+import cn.com.qmth.examcloud.web.cloud.AppSelf;
+import cn.com.qmth.examcloud.web.cloud.AppSelfHolder;
+import okhttp3.Response;
+
+/**
+ * 系统启动器
+ *
+ * @author WANGWEI
+ * @date 2019年3月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class AppBootstrap {
+
+	private static ExamCloudLog log = ExamCloudLogFactory.getLog(AppBootstrap.class);
+
+	private static Map<String, String> properties;
+
+	private static String active;
+
+	private static String startupCode;
+
+	private static Long appId;
+
+	private static String secretKey;
+
+	private static String appCode;
+
+	private static String configCenterHost;
+
+	private static String configCenterPort;
+
+	/**
+	 * 启动方法
+	 *
+	 * @author WANGWEI
+	 * @param primarySource
+	 * @param args
+	 * @return
+	 */
+	public static synchronized ConfigurableApplicationContext run(Class<?> primarySource,
+			String... args) {
+
+		ThreadContext.put("TRACE_ID", Thread.currentThread().getName());
+		log.info("Starting...");
+
+		checkBootstrapParams(args);
+
+		sendStartupRequest2ConfigCenter();
+
+		if (null != args) {
+			for (String arg : args) {
+				arg = arg.trim();
+				if (arg.startsWith("--")) {
+					String key = arg.substring(2, arg.indexOf("="));
+					String value = arg.substring(arg.indexOf("=") + 1);
+					properties.put(key, value);
+				}
+			}
+		}
+
+		properties.put("spring.profiles.active", active);
+
+		Set<String> argSet = Sets.newLinkedHashSet();
+		for (Entry<String, String> p : properties.entrySet()) {
+			if (StringUtils.isBlank(p.getValue())) {
+				continue;
+			}
+			PropertyHolder.setProperty(p.getKey(), p.getValue());
+			String arg = "--" + p.getKey() + "=" + p.getValue();
+			argSet.add(arg);
+		}
+
+		AppSelfHolder.set(buildAppSelf());
+
+		String[] newArgs = argSet.toArray(new String[argSet.size()]);
+		ConfigurableApplicationContext context = null;
+		try {
+			context = SpringApplication.run(primarySource, newArgs);
+			noticeConfigCenter("success");
+		} catch (Exception e) {
+			noticeConfigCenter("failure:" + e.getMessage());
+			log.error("fail to run spring app.", e);
+			System.exit(-1);
+		}
+
+		return context;
+	}
+
+	/**
+	 * 创建 {@link AppSelf} 实例
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private static AppSelf buildAppSelf() {
+		return new AppSelf() {
+
+			@Override
+			public Long getAppId() {
+				return appId;
+			}
+
+			@Override
+			public String getAppCode() {
+				return appCode;
+			}
+
+			@Override
+			public String getSecretKey() {
+				return secretKey;
+			}
+		};
+	}
+
+	/**
+	 * 通知配置中心
+	 *
+	 * @author WANGWEI
+	 * @param message
+	 */
+	private static void noticeConfigCenter(String message) {
+		String url = "http://" + configCenterHost + ":" + configCenterPort + "/configCenter/notice";
+
+		Map<String, String> req = Maps.newHashMap();
+		req.put("active", active);
+		req.put("appCode", appCode);
+		req.put("startupCode", startupCode);
+		req.put("message", message);
+
+		Response resp = null;
+		try {
+			Map<String, String> headers = Maps.newHashMap();
+			headers.put("TRACE_ID", startupCode);
+			resp = OKHttpUtil.call(HttpMethod.POST, url.toString(), headers, req);
+		} catch (Exception e) {
+			log.error("fail to notice config center.", e);
+			System.exit(-1);
+		} finally {
+			IOUtils.closeQuietly(resp);
+		}
+	}
+
+	/**
+	 * 向配置中心发送启动请求
+	 *
+	 * @author WANGWEI
+	 */
+	private static void sendStartupRequest2ConfigCenter() {
+		StringBuilder url = new StringBuilder(
+				"http://" + configCenterHost + ":" + configCenterPort + "/configCenter/startup");
+
+		Map<String, String> req = Maps.newHashMap();
+		req.put("active", active);
+		req.put("appCode", appCode);
+		req.put("startupCode", startupCode);
+
+		Response resp = null;
+		try {
+			Map<String, String> headers = Maps.newHashMap();
+			headers.put("TRACE_ID", startupCode);
+			resp = OKHttpUtil.call(HttpMethod.POST, url.toString(), headers, req);
+			if (resp.code() != HttpStatus.OK.value()) {
+				throw new ExamCloudRuntimeException("fail to get configuration");
+			}
+			String body = resp.body().string();
+
+			String encryptedAppId = resp.header("App-Id");
+			String encryptedSecretKey = resp.header("Secret-Key");
+
+			appId = StringUtil.toLong(BootstrapSecurityUtil.decrypt(encryptedAppId, startupCode));
+			secretKey = BootstrapSecurityUtil.decrypt(encryptedSecretKey, startupCode);
+
+			String decryptedBody = BootstrapSecurityUtil.decrypt(body, startupCode);
+			@SuppressWarnings("unchecked")
+			Map<String, String> map = JsonUtil.fromJson(decryptedBody, Map.class);
+			properties = map;
+
+		} catch (Exception e) {
+			log.error("fail to send startup request to config center.", e);
+			System.exit(-1);
+		} finally {
+			IOUtils.closeQuietly(resp);
+		}
+
+	}
+
+	/**
+	 * 检查启动参数
+	 *
+	 * @author WANGWEI
+	 * @param args
+	 * @return
+	 */
+	private static void checkBootstrapParams(String... args) {
+
+		Properties props = new Properties();
+		PropertiesUtil.loadFromResource("application.properties", props);
+
+		if (null != args) {
+			for (String s : args) {
+				s = s.trim();
+				if (s.startsWith("--spring.profiles.active=")) {
+					active = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--ocean.startup.startupCode=")) {
+					startupCode = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--ocean.startup.configCenterHost=")) {
+					configCenterHost = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--ocean.startup.configCenterPort=")) {
+					configCenterPort = s.substring(s.indexOf("=") + 1);
+				} else if (s.startsWith("--ocean.startup.appCode=")) {
+					appCode = s.substring(s.indexOf("=") + 1);
+				}
+			}
+		}
+
+		// active
+		if (null == active) {
+			String value = props.getProperty("spring.profiles.active");
+			if (StringUtils.isNotBlank(value)) {
+				active = value.trim();
+			}
+		}
+		log.info("active=" + active);
+		if (StringUtils.isBlank(active)) {
+			log.error("property[spring.profiles.active] is not specified");
+			System.exit(-1);
+		}
+
+		// startupCode
+		if (null == startupCode) {
+			String value = props.getProperty("ocean.startup.startupCode");
+			if (StringUtils.isNotBlank(value)) {
+				startupCode = value.trim();
+			}
+		}
+		log.info("startupCode=" + startupCode);
+		if (StringUtils.isBlank(startupCode)) {
+			log.error("property[ocean.startup.startupCode] is not specified");
+			System.exit(-1);
+		}
+
+		// appCode
+		if (null == appCode) {
+			String value = props.getProperty("ocean.startup.appCode");
+			if (StringUtils.isNotBlank(value)) {
+				appCode = value.trim();
+			}
+		}
+		log.info("appCode=" + appCode);
+		if (StringUtils.isBlank(appCode)) {
+			log.error("property[ocean.startup.appCode] is not specified");
+			System.exit(-1);
+		}
+
+		// configCenterHost
+		if (null == configCenterHost) {
+			String value = props.getProperty("ocean.startup.configCenterHost");
+			if (StringUtils.isNotBlank(value)) {
+				configCenterHost = value.trim();
+			}
+		}
+		log.info("configCenterHost=" + configCenterHost);
+		if (StringUtils.isBlank(configCenterHost)) {
+			log.error("property[ocean.startup.configCenterHost] is not specified");
+			System.exit(-1);
+		}
+
+		// configCenterPort
+		if (null == configCenterPort) {
+			String value = props.getProperty("ocean.startup.configCenterPort");
+			if (StringUtils.isNotBlank(value)) {
+				configCenterPort = value.trim();
+			}
+		}
+		log.info("configCenterPort=" + configCenterPort);
+		if (null == configCenterPort) {
+			log.error("property[ocean.startup.configCenterPort] is not specified");
+			System.exit(-1);
+		}
+
+	}
+
+}

+ 115 - 0
src/main/java/cn/com/qmth/examcloud/web/bootstrap/BootstrapSecurityUtil.java

@@ -0,0 +1,115 @@
+package cn.com.qmth.examcloud.web.bootstrap;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.util.AES;
+
+/**
+ * 安全工具
+ *
+ * @author WANGWEI
+ * @date 2018年12月4日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class BootstrapSecurityUtil {
+
+	private static final String RANDOM = "9451utb@91&vrmio!90iu!89F2QN1V ALAII!K33I*DFA^sA";
+
+	private static final String ENCRYPTED_HEAD = "$$.";
+
+	/**
+	 * 加密
+	 *
+	 * @author WANGWEI
+	 * @param s
+	 * @param secretKey
+	 */
+	public static String encrypt(String s, String secretKey) {
+		AES aes = new AES(secretKey + RANDOM);
+		String encrypted = aes.encrypt(s);
+		return encrypted;
+	}
+
+	/**
+	 * 解密
+	 *
+	 * @author WANGWEI
+	 * @param s
+	 * @param secretKey
+	 */
+	public static String decrypt(String s, String secretKey) {
+		AES aes = new AES(secretKey + RANDOM);
+		String decrypted = aes.decrypt(s);
+		return decrypted;
+	}
+
+	/**
+	 * 解密
+	 *
+	 * @author WANGWEI
+	 * @param props
+	 * @param secretKey
+	 */
+	public static void decrypt(Properties props, String secretKey) {
+		AES aes = new AES(secretKey + RANDOM);
+		Map<String, String> map = Maps.newHashMap();
+		Set<String> set = Sets.newHashSet();
+		for (Entry<Object, Object> entry : props.entrySet()) {
+			Object key = entry.getKey();
+			Object value = entry.getValue();
+			if (key instanceof String && value instanceof String) {
+				if (((String) key).startsWith(ENCRYPTED_HEAD)) {
+					String decryptedValue = aes.decrypt((String) value);
+					String newKey = ((String) key).substring(ENCRYPTED_HEAD.length());
+					map.put(newKey, decryptedValue);
+					set.add((String) key);
+				}
+			}
+		}
+
+		for (String s : set) {
+			props.remove(s);
+		}
+
+		for (Entry<String, String> entry : map.entrySet()) {
+			props.put(entry.getKey(), entry.getValue());
+		}
+	}
+
+	/**
+	 * 解密
+	 *
+	 * @author WANGWEI
+	 * @param props
+	 * @param secretKey
+	 */
+	public static void decrypt(Map<String, String> props, String secretKey) {
+		AES aes = new AES(secretKey + RANDOM);
+		Map<String, String> map = Maps.newHashMap();
+		Set<String> set = Sets.newHashSet();
+		for (Entry<String, String> entry : props.entrySet()) {
+			String key = entry.getKey();
+			String value = entry.getValue();
+			if (key.startsWith(ENCRYPTED_HEAD)) {
+				String decrypted = aes.decrypt((String) value);
+				map.put(key.substring(ENCRYPTED_HEAD.length() + 1), decrypted);
+				set.add(key);
+			}
+		}
+
+		for (String s : set) {
+			props.remove(s);
+		}
+
+		for (Entry<String, String> entry : map.entrySet()) {
+			props.put(entry.getKey(), entry.getValue());
+		}
+	}
+
+}

+ 120 - 0
src/main/java/cn/com/qmth/examcloud/web/bootstrap/PropertyHolder.java

@@ -0,0 +1,120 @@
+package cn.com.qmth.examcloud.web.bootstrap;
+
+import java.util.Properties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+
+/**
+ * 云配置
+ *
+ * @author WANGWEI
+ * @date 2019年3月25日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class PropertyHolder {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(PropertyHolder.class);
+
+	private static final Properties PROPS = new Properties();
+
+	/**
+	 * 设置属性
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param value
+	 */
+	public static void setProperty(String key, String value) {
+		PROPS.setProperty(key, value);
+	}
+
+	/**
+	 * @param key
+	 * @return
+	 */
+	public static String getString(String key) {
+		String value = PROPS.getProperty(key);
+		if (StringUtils.isNotBlank(value)) {
+			return value.trim();
+		} else {
+			if (LOG.isDebugEnabled()) {
+				LOG.debug("No  property key named [" + key + "] is configured.");
+			}
+			return null;
+		}
+	}
+
+	/**
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public static String getString(String key, String defaultValue) {
+		String value = getString(key);
+		if (null != value) {
+			return value;
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public static int getInt(String key, int defaultValue) {
+		String value = getString(key);
+		if (null != value) {
+			try {
+				return Integer.parseInt(value);
+			} catch (NumberFormatException e) {
+				PROPS.setProperty(key, String.valueOf(defaultValue));
+				return defaultValue;
+			}
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public static long getLong(String key, long defaultValue) {
+		String value = getString(key);
+		if (null != value) {
+			try {
+				return Long.parseLong(value);
+			} catch (NumberFormatException e) {
+				return defaultValue;
+			}
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * 获取boolean
+	 *
+	 * @author WANGWEI
+	 * @param key
+	 * @param defaultVale
+	 * @return
+	 */
+	public static boolean getBoolean(String key, boolean defaultVale) {
+		String value = getString(key);
+		if (null == value) {
+			return defaultVale;
+		}
+		if (value.equals("true")) {
+			return true;
+		} else if (value.equals("false")) {
+			return false;
+		} else {
+			return defaultVale;
+		}
+	}
+
+}

+ 37 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelf.java

@@ -0,0 +1,37 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+/**
+ * APP self
+ *
+ * @author WANGWEI
+ * @date 2019年2月18日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public interface AppSelf {
+
+	/**
+	 * 获取当前APP ID
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	Long getAppId();
+
+	/**
+	 * 获取当前APP识别码<br>
+	 * (通常为1到3位大写字母和数字组合)
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	String getAppCode();
+
+	/**
+	 * 获取当前APP 密钥
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	String getSecretKey();
+
+}

+ 44 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/AppSelfHolder.java

@@ -0,0 +1,44 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * AppSelf holder
+ *
+ * @author WANGWEI
+ * @date 2019年2月21日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class AppSelfHolder {
+
+	private static AppSelf appSelf;
+
+	/**
+	 * 获取
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static AppSelf get() {
+		if (null == appSelf) {
+			appSelf = SpringContextHolder.getBean(AppSelf.class);
+			if (null == appSelf) {
+				throw new ExamCloudRuntimeException("no AppSelf");
+			}
+		}
+
+		return appSelf;
+	}
+
+	/**
+	 * 设置
+	 *
+	 * @author WANGWEI
+	 * @param appSelf
+	 */
+	public static void set(AppSelf appSelf) {
+		AppSelfHolder.appSelf = appSelf;
+	}
+
+}

+ 141 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientConfiguration.java

@@ -0,0 +1,141 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import java.io.IOException;
+
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.client.loadbalancer.LoadBalanced;
+import org.springframework.cloud.netflix.ribbon.RibbonClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.DefaultResponseErrorHandler;
+import org.springframework.web.client.RestTemplate;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * cloud 配置
+ *
+ * @author WANGWEI
+ * @date 2019年1月28日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Configuration
+@RibbonClients(defaultConfiguration = RibbonClientsConfiguration.class)
+public class CloudClientConfiguration {
+
+	private static final ExamCloudLog DEBUG_LOG = ExamCloudLogFactory
+			.getLog(CloudClientConfiguration.class);
+
+	@Bean
+	@LoadBalanced
+	@Autowired
+	public RestTemplate buildRestTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
+
+		RestTemplate restTemplate = new RestTemplate();
+		restTemplate.setRequestFactory(clientHttpRequestFactory);
+
+		restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
+			@Override
+			public boolean hasError(ClientHttpResponse response) throws IOException {
+				return false;
+			}
+		});
+
+		return restTemplate;
+	}
+
+	@Bean
+	public ClientHttpRequestFactory buildClientHttpRequestFactory() {
+
+		String httpRequestFactory = PropertyHolder.getString("examcloud.rpc.httpRequestFactory",
+				"OkHttp3");
+
+		if (httpRequestFactory.equals("Simple")) {
+			return buildSimpleClientHttpRequestFactory();
+		} else if (httpRequestFactory.equals("HttpComponents")) {
+			return buildHttpComponentsClientHttpRequestFactory();
+		}
+		if (httpRequestFactory.equals("OkHttp3")) {
+			return buildOkHttp3ClientHttpRequestFactory();
+		} else {
+			DEBUG_LOG.info(
+					"value of property[examcloud.rpc.httpRequestFactory] is wrong. will use default value [OkHttp3] .");
+			return buildOkHttp3ClientHttpRequestFactory();
+		}
+	}
+
+	/**
+	 * Simple
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private ClientHttpRequestFactory buildSimpleClientHttpRequestFactory() {
+		int connectTimeout = PropertyHolder.getInt("examcloud.rpc.connectTimeout", 2000);
+		int readTimeout = PropertyHolder.getInt("examcloud.rpc.readTimeout", 60000);
+
+		SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
+		clientHttpRequestFactory.setReadTimeout(readTimeout);
+		clientHttpRequestFactory.setConnectTimeout(connectTimeout);
+		return clientHttpRequestFactory;
+	}
+
+	/**
+	 * OkHttp3
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private ClientHttpRequestFactory buildOkHttp3ClientHttpRequestFactory() {
+		int connectTimeout = PropertyHolder.getInt("examcloud.rpc.connectTimeout", 2000);
+		int readTimeout = PropertyHolder.getInt("examcloud.rpc.readTimeout", 60000);
+		int writeTimeout = PropertyHolder.getInt("examcloud.rpc.writeTimeout", 60000);
+
+		OkHttp3ClientHttpRequestFactory clientHttpRequestFactory = new OkHttp3ClientHttpRequestFactory();
+		clientHttpRequestFactory.setConnectTimeout(connectTimeout);
+		clientHttpRequestFactory.setReadTimeout(readTimeout);
+		clientHttpRequestFactory.setWriteTimeout(writeTimeout);
+		return clientHttpRequestFactory;
+	}
+
+	/**
+	 * HttpComponents
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private ClientHttpRequestFactory buildHttpComponentsClientHttpRequestFactory() {
+		Boolean poolEnable = PropertyHolder.getBoolean("examcloud.rpc.pool.enable", true);
+
+		int maxTotal = PropertyHolder.getInt("examcloud.rpc.pool.maxTotal", 200);
+		int defaultMaxPerRoute = PropertyHolder.getInt("examcloud.rpc.pool.defaultMaxPerRoute", 10);
+		int connectTimeout = PropertyHolder.getInt("examcloud.rpc.connectTimeout", 2000);
+		int readTimeout = PropertyHolder.getInt("examcloud.rpc.readTimeout", 60000);
+
+		HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
+
+		if (poolEnable) {
+			PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager();
+			poolingConnectionManager.setMaxTotal(maxTotal);
+			poolingConnectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
+
+			httpClientBuilder.setConnectionManager(poolingConnectionManager);
+		}
+
+		HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
+		clientHttpRequestFactory.setHttpClient(httpClientBuilder.build());
+		clientHttpRequestFactory.setConnectTimeout(connectTimeout);
+		clientHttpRequestFactory.setReadTimeout(readTimeout);
+		return clientHttpRequestFactory;
+	}
+
+}

+ 115 - 45
src/main/java/cn/com/qmth/examcloud/web/cloud/CloudClientSupport.java

@@ -19,17 +19,21 @@ import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
 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.ByteUtil;
 import cn.com.qmth.examcloud.commons.util.JsonUtil;
+import cn.com.qmth.examcloud.commons.util.SHA256;
 import cn.com.qmth.examcloud.commons.util.StringUtil;
 import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
-import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.config.LogProperties;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
 import cn.com.qmth.examcloud.web.support.StatusResponse;
 
 /**
  * 云服务客户端基类
- * 
- * @author WANGWEI
  *
+ * @author WANGWEI
+ * @date 2019年1月25日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
  */
 public abstract class CloudClientSupport {
 
@@ -39,11 +43,77 @@ public abstract class CloudClientSupport {
 	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
 			.getLog("INTERFACE_LOGGER");
 
-	protected abstract RestTemplate getRestTemplate();
+	private RestTemplate restTemplate;
+
+	private LogProperties logProperties;
+
+	private static String[] excludeFields = new String[]{"password", ".*Password"};
+
+	private LogProperties getLogProperties() {
+		if (null == logProperties) {
+			logProperties = SpringContextHolder.getBean(LogProperties.class);
+		}
+		return logProperties;
+	}
+
+	/**
+	 * 获取请求映射前缀
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	protected abstract String getRequestMappingPrefix();
+
+	/**
+	 * 获取端口
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	protected abstract Integer getPort();
+
+	/**
+	 * 获取 RestTemplate
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	private RestTemplate getRestTemplate() {
+		if (null == restTemplate) {
+			restTemplate = SpringContextHolder.getBean(RestTemplate.class);
+		}
+		return restTemplate;
+	}
 
-	protected abstract RedisClient getRedisClient();
+	/**
+	 * 构建url
+	 *
+	 * @author WANGWEI
+	 * @param appName
+	 * @param requestMapping
+	 * @return
+	 */
+	private String buildUrl(String appName, String requestMapping) {
+		StringBuilder sb = new StringBuilder();
+		sb.append("http://");
+		sb.append(appName);
+		sb.append(":").append(getPort());
+		String rmp = getRequestMappingPrefix();
+		if (rmp.startsWith("/")) {
+			sb.append(rmp);
+		} else {
+			sb.append("/").append(rmp);
+		}
+		if ('/' == sb.charAt(sb.length() - 1) && requestMapping.endsWith("/")) {
+			sb.deleteCharAt(sb.length() - 1).append(requestMapping);
+		} else if ('/' != sb.charAt(sb.length() - 1) && !requestMapping.endsWith("/")) {
+			sb.append("/").append(requestMapping);
+		} else {
+			sb.append(requestMapping);
+		}
 
-	protected abstract String getUrlPrefix();
+		return sb.toString();
+	}
 
 	/**
 	 * 获取响应体
@@ -67,20 +137,6 @@ public abstract class CloudClientSupport {
 		}
 	}
 
-	/**
-	 * 构建url
-	 * 
-	 * @param urlSuffix
-	 * @return
-	 */
-	private String buildUrl(String urlSuffix) {
-		String prefix = getUrlPrefix();
-		prefix = prefix.endsWith("/") ? prefix : prefix + "/";
-		urlSuffix = urlSuffix.startsWith("/") ? urlSuffix.substring(1) : urlSuffix;
-		String url = prefix + urlSuffix;
-		return url;
-	}
-
 	/**
 	 * exchange
 	 *
@@ -96,15 +152,21 @@ public abstract class CloudClientSupport {
 		long startTime = System.currentTimeMillis();
 
 		HttpHeaders requestHeaders = new HttpHeaders();
-		requestHeaders.add("TRACE_ID", ThreadLocalUtil.getTraceID());
-		requestHeaders.add("$spring_cloud_client", "_");
-		getRedisClient().set("$_RMI_:" + ThreadLocalUtil.getTraceID(), System.currentTimeMillis(),
-				10);
+		requestHeaders.add("Trace-Id", ThreadLocalUtil.getTraceId());
+		requestHeaders.add("timestamp", String.valueOf(startTime));
+		requestHeaders.add("App-Id", String.valueOf(AppSelfHolder.get().getAppId()));
+		requestHeaders.add("App-Code", String.valueOf(AppSelfHolder.get().getAppCode()));
+
+		String joinStr = StringUtil.join(AppSelfHolder.get().getAppId(),
+				AppSelfHolder.get().getAppCode(), startTime, AppSelfHolder.get().getSecretKey());
+		byte[] bytes = SHA256.encode(joinStr);
+		String accessToken = ByteUtil.toHexAscii(bytes);
+		requestHeaders.add("Access-Token", accessToken);
 
 		if (INTERFACE_LOG.isInfoEnabled()) {
 			INTERFACE_LOG.info("[CALL-IN]. url=" + url);
 			if (body instanceof JsonSerializable) {
-				INTERFACE_LOG.info("[CALL-REQ]. request=" + JsonUtil.toJson(body));
+				INTERFACE_LOG.info("[CALL-REQ]. request=" + JsonUtil.toJson(body, excludeFields));
 			}
 		}
 
@@ -116,19 +178,26 @@ public abstract class CloudClientSupport {
 					requestEntity, String.class);
 			respBody = getRespBody(respEntity, responseType);
 
-			String msg = StringUtil.join("[CALL-OK]. url=" + url,
-					" ; cost " + (System.currentTimeMillis() - startTime)) + " ms.";
-			INTERFACE_LOG.info(msg);
-			INTERFACE_LOG.info("[CALL-RESP]. response=" + respEntity.getBody());
+			if (INTERFACE_LOG.isDebugEnabled() && getLogProperties().isNormalResponseLogEnable()) {
+				String respEntityBody = respEntity.getBody();
+				int responseJsonMaxSize = getLogProperties().getResponseLogJsonMaxSize();
+				if (respEntityBody.length() > responseJsonMaxSize) {
+					INTERFACE_LOG.debug("[CALL-RESP]. response= too large");
+				} else {
+					INTERFACE_LOG.debug("[CALL-RESP]. response=" + respEntityBody);
+				}
+			}
+			if (INTERFACE_LOG.isInfoEnabled()) {
+				INTERFACE_LOG.info(StringUtil.join("[CALL-OK]. url=" + url,
+						" ; cost " + (System.currentTimeMillis() - startTime), " ms."));
+			}
 		} catch (StatusException e) {
-			String msg = StringUtil.join("[CALL-FAIL]. url=" + url,
-					" ; cost " + (System.currentTimeMillis() - startTime)) + " ms.";
-			INTERFACE_LOG.error(msg);
+			INTERFACE_LOG.error(StringUtil.join("[CALL-FAIL]. url=" + url,
+					" ; cost " + (System.currentTimeMillis() - startTime), " ms."));
 			INTERFACE_LOG.error("[CALL-RESP]. response=" + e.toJson());
 			throw e;
 		} catch (Exception e) {
-			String msg = StringUtil.join("[CALL-FATAL]. url=" + url, " ;", e.getMessage());
-			INTERFACE_LOG.error(msg);
+			INTERFACE_LOG.error(StringUtil.join("[CALL-FATAL]. url=" + url, " ;", e.getMessage()));
 			throw e;
 		}
 
@@ -139,24 +208,25 @@ public abstract class CloudClientSupport {
 	 * post请求
 	 *
 	 * @author WANGWEI
-	 * @param urlSuffix
+	 * @param requestMapping
 	 * @param body
 	 * @param responseType
 	 * @return
 	 */
-	protected <T> T post(String urlSuffix, BaseRequest body, Class<T> responseType) {
-		String url = buildUrl(urlSuffix);
+	protected <T> T post(String appName, String requestMapping, BaseRequest body,
+			Class<T> responseType) {
+		String url = buildUrl(appName, requestMapping);
 		return exchange(url, HttpMethod.POST, body, responseType);
 	}
 
 	/**
 	 * post请求
 	 * 
-	 * @param urlSuffix
+	 * @param requestMapping
 	 * @param body
 	 */
-	protected void post(String urlSuffix, BaseRequest body) {
-		String url = buildUrl(urlSuffix);
+	protected void post(String appName, String requestMapping, BaseRequest body) {
+		String url = buildUrl(appName, requestMapping);
 		exchange(url, HttpMethod.POST, body, null);
 	}
 
@@ -164,16 +234,16 @@ public abstract class CloudClientSupport {
 	 * 文件表单提交
 	 *
 	 * @author WANGWEI
-	 * @param urlSuffix
+	 * @param requestMapping
 	 * @param params
 	 * @param file
 	 * @param responseType
 	 * @return
 	 * @throws Exception
 	 */
-	public <T> T postForm(String urlSuffix, Map<String, String> params, File file,
-			Class<T> responseType) {
-		String url = buildUrl(urlSuffix);
+	public <T> T postForm(String appName, String requestMapping, Map<String, String> params,
+			File file, Class<T> responseType) {
+		String url = buildUrl(appName, requestMapping);
 
 		MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
 		FileSystemResource resource = new FileSystemResource(file);
@@ -187,4 +257,4 @@ public abstract class CloudClientSupport {
 		return exchange(url, HttpMethod.POST, param, responseType);
 	}
 
-}
+}

+ 56 - 0
src/main/java/cn/com/qmth/examcloud/web/cloud/RibbonClientsConfiguration.java

@@ -0,0 +1,56 @@
+package cn.com.qmth.examcloud.web.cloud;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.netflix.loadbalancer.IPing;
+import com.netflix.loadbalancer.IRule;
+import com.netflix.loadbalancer.PingUrl;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.web.bootstrap.PropertyHolder;
+
+/**
+ * RibbonClients 配置
+ *
+ * @author WANGWEI
+ * @date 2019年1月28日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+@Configuration
+public class RibbonClientsConfiguration {
+
+	private static final ExamCloudLog DEBUG_LOG = ExamCloudLogFactory
+			.getLog(CloudClientConfiguration.class);
+
+	@Bean
+	public IRule ribbonRule() {
+
+		String rule = PropertyHolder.getString("examcloud.rpc.loadbalance.rule", "BestAvailableRule");
+
+		if (rule.equals("RoundRobinRule")) {
+			return new com.netflix.loadbalancer.RoundRobinRule();
+		} else if (rule.equals("ZoneAvoidanceRule")) {
+			return new com.netflix.loadbalancer.ZoneAvoidanceRule();
+		} else if (rule.equals("RetryRule")) {
+			return new com.netflix.loadbalancer.RetryRule();
+		} else if (rule.equals("RandomRule")) {
+			return new com.netflix.loadbalancer.RandomRule();
+		} else if (rule.equals("BestAvailableRule")) {
+			return new com.netflix.loadbalancer.BestAvailableRule();
+		} else if (rule.equals("WeightedResponseTimeRule")) {
+			return new com.netflix.loadbalancer.WeightedResponseTimeRule();
+		} else {
+			DEBUG_LOG.info(
+					"value of property[examcloud.rpc.balance.rule] is wrong. will use default value [BestAvailableRule] .");
+			return new com.netflix.loadbalancer.WeightedResponseTimeRule();
+		}
+	}
+
+	@Bean
+	public IPing ribbonPing() {
+		return new PingUrl();
+	}
+
+}

+ 43 - 0
src/main/java/cn/com/qmth/examcloud/web/config/LogProperties.java

@@ -0,0 +1,43 @@
+package cn.com.qmth.examcloud.web.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 日志属性
+ *
+ * @author WANGWEI
+ * @date 2019年3月20日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+@ConfigurationProperties("ocean.web.log")
+public class LogProperties {
+
+	/**
+	 * 是否记录正常相应信息
+	 */
+	private boolean normalResponseLogEnable = true;
+
+	/**
+	 * 正常响应信息json长度限制
+	 */
+	private int responseLogJsonMaxSize = 200;
+
+	public boolean isNormalResponseLogEnable() {
+		return normalResponseLogEnable;
+	}
+
+	public void setNormalResponseLogEnable(boolean normalResponseLogEnable) {
+		this.normalResponseLogEnable = normalResponseLogEnable;
+	}
+
+	public int getResponseLogJsonMaxSize() {
+		return responseLogJsonMaxSize;
+	}
+
+	public void setResponseLogJsonMaxSize(int responseLogJsonMaxSize) {
+		this.responseLogJsonMaxSize = responseLogJsonMaxSize;
+	}
+
+}

+ 47 - 140
src/main/java/cn/com/qmth/examcloud/web/interceptor/FirstInterceptor.java

@@ -1,37 +1,31 @@
 package cn.com.qmth.examcloud.web.interceptor;
 
-import java.io.File;
-import java.io.IOException;
 import java.util.Enumeration;
-import java.util.List;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.apache.commons.collections.CollectionUtils;
-import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.slf4j.MDC;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.apache.logging.log4j.ThreadContext;
 import org.springframework.http.HttpStatus;
-import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.HandlerInterceptor;
 import org.springframework.web.servlet.ModelAndView;
 
 import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 
-import cn.com.qmth.examcloud.commons.helpers.FileChangeWatchdog;
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
-import cn.com.qmth.examcloud.commons.logging.SLF4JImpl;
 import cn.com.qmth.examcloud.commons.util.JsonUtil;
-import cn.com.qmth.examcloud.commons.util.PathUtil;
 import cn.com.qmth.examcloud.commons.util.StringUtil;
 import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.support.ApiId;
+import cn.com.qmth.examcloud.web.support.ApiInfo;
+import cn.com.qmth.examcloud.web.support.ApiInfoHolder;
 import cn.com.qmth.examcloud.web.support.ServletUtil;
 import cn.com.qmth.examcloud.web.support.StatusResponse;
 
@@ -39,171 +33,86 @@ import cn.com.qmth.examcloud.web.support.StatusResponse;
  * 首发拦截器
  *
  * @author WANGWEI
- * @date 2018年5月22日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @date 2018年10月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
  */
 public class FirstInterceptor implements HandlerInterceptor {
 
 	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(FirstInterceptor.class);
 
-	/**
-	 * 接口日志
-	 */
-	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
+	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
 			.getLog("INTERFACE_LOGGER");
 
-	private static final Set<String> RESOURCE_NAME_SET = Sets.newConcurrentHashSet();
-
-	/**
-	 * 拦截的请求
-	 */
-	private Set<String> badRequests;
-
-	/**
-	 * 热加载配置文件
-	 *
-	 * @author WANGWEI
-	 * @param resourceName
-	 */
-	public synchronized void configureAndWatch(String resourceName) {
-		if (RESOURCE_NAME_SET.contains(resourceName)) {
-			return;
-		}
-		String path = PathUtil.getResoucePath(resourceName);
-		loadFromFile(path);
-
-		InterceptConfFileChangeWatchdog dog = new InterceptConfFileChangeWatchdog(path);
-		dog.setDelay(30000);
-		dog.start();
-		RESOURCE_NAME_SET.add(resourceName);
-	}
-
-	/**
-	 * 观察线程
-	 *
-	 * @author WANGWEI
-	 */
-	private class InterceptConfFileChangeWatchdog extends FileChangeWatchdog {
-
-		protected InterceptConfFileChangeWatchdog(String path) {
-			super(path);
-		}
-
-		@Override
-		protected void doOnChange() {
-			try {
-				loadFromFile(path);
-			} catch (Exception e) {
-				LOG.info("Fail to load from file [" + path + "].", e);
-			}
-		}
-	}
-
-	private void loadFromFile(String path) {
-		try {
-			Set<String> newBadRequests = Sets.newHashSet();
-			List<String> lines = FileUtils.readLines(new File(path), "UTF-8");
-			for (String cur : lines) {
-				if (StringUtils.isNotBlank(cur)) {
-					newBadRequests.add(cur.trim());
-				}
-			}
-			badRequests = newBadRequests;
-			LOG.info("exclusions=" + JsonUtil.toJson(badRequests));
-		} catch (IOException e) {
-			LOG.error("fail to load config from file. filePath=" + path, e);
-		}
-	}
-
 	@Override
 	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
 			Object handler) throws Exception {
 
-		String traceID = request.getHeader("TRACE_ID");
-		if (StringUtils.isBlank(traceID)) {
-			traceID = ThreadLocalUtil.next();
+		String traceId = request.getHeader("Trace-Id");
+		if (StringUtils.isBlank(traceId)) {
+			traceId = ThreadLocalUtil.next();
 		} else {
-			ThreadLocalUtil.setTraceID(traceID);
+			ThreadLocalUtil.setTraceId(traceId);
 		}
 
-		// 设置MDC
-		if (LOG instanceof SLF4JImpl) {
-			MDC.put("TRACE_ID", traceID);
-			MDC.put("KEY", "$$$$$");
-		}
+		// 设置log4j线程上下文
+		ThreadContext.put("TRACE_ID", traceId);
+		ThreadContext.put("CALLER", "$$$$$");
 
 		String path = request.getServletPath();
 		String method = request.getMethod();
 
-		String reqPath = method + ":" + path;
-
-		LOG.debug("[preHandle]. reqPath = " + reqPath);
-
-		if (CollectionUtils.isNotEmpty(badRequests)) {
-			if (badRequests.contains(reqPath)) {
-				response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
-				ServletUtil.returnJsonWithoutLog(new StatusResponse("406", "Not Acceptable."),
-						response);
-				return false;
-			}
-		}
-
+		Set<String> headerNames = new HashSet<String>(20);
 		if (INTERFACE_LOG.isDebugEnabled()) {
 			Map<String, String> headers = Maps.newHashMap();
 			Enumeration<String> e = request.getHeaderNames();
 			while (e.hasMoreElements()) {
 				String name = (String) e.nextElement();
 				String value = request.getHeader(name);
-				if (name.startsWith("cookie")) {
-					continue;
-				}
 				headers.put(name, value);
+				headerNames.add(name);
 			}
 			INTERFACE_LOG.debug(StringUtil.join("[preHandle]. path=\"", path, "\"; method=[",
 					method, "]", "; headers=", JsonUtil.toJson(headers)));
 		}
 
+		if (path.equals("/error")) {
+			response.setStatus(HttpStatus.NOT_FOUND.value());
+			ServletUtil.returnJson(new StatusResponse(String.valueOf(HttpStatus.NOT_FOUND.value()),
+					String.valueOf(HttpStatus.NOT_FOUND.getReasonPhrase())), response);
+			return false;
+		}
+
 		if (handler instanceof HandlerMethod) {
 			HandlerMethod handlerMethod = (HandlerMethod) handler;
-			Object controllerObject = handlerMethod.getBean();
-			RequestMapping mappingOfClass = AnnotationUtils
-					.findAnnotation(controllerObject.getClass(), RequestMapping.class);
-			RequestMapping mappingOfMethod = handlerMethod
-					.getMethodAnnotation(RequestMapping.class);
-
-			String[] allPathOfClass = null;
-			String[] allPathOfMethod = null;
+			ApiId apiId = handlerMethod.getMethodAnnotation(ApiId.class);
 
-			if (null != mappingOfClass) {
-				allPathOfClass = mappingOfClass.path();
+			ApiInfo apiInfo = null;
+			if (null != apiId) {
+				apiInfo = ApiInfoHolder.getApiInfo(apiId.value());
+			} else {
+				apiInfo = ApiInfoHolder.getApiInfo(handlerMethod.getMethod());
 			}
-			if (null != mappingOfMethod) {
-				allPathOfMethod = mappingOfMethod.path();
-			}
-
-			String pathsOfClass = null;
-			String pathsOfMethod = null;
+			String mapping = apiInfo.getMapping();
+			request.setAttribute(HttpServletRequestAttribute.$_API_INFO.name(), apiInfo);
 
-			if (null != allPathOfClass) {
-				pathsOfClass = StringUtils.join(allPathOfClass, ",");
+			if (INTERFACE_LOG.isDebugEnabled()) {
+				INTERFACE_LOG.debug("[preHandle]. mapping = " + mapping);
 			}
-			if (null != allPathOfMethod) {
-				pathsOfMethod = StringUtils.join(allPathOfMethod, ",");
+			request.setAttribute(HttpServletRequestAttribute.$_MAPPING.name(), mapping);
+
+		} else {
+			if (INTERFACE_LOG.isDebugEnabled()) {
+				INTERFACE_LOG.debug("not handle by HandlerMethod.");
 			}
 
-			String mappingPath = StringUtils.join("[", pathsOfClass, "][", pathsOfMethod, "][",
-					method, "]");
+			String mapping = StringUtils.join("_[", path, "][", method, "]");
 
 			if (INTERFACE_LOG.isDebugEnabled()) {
-				INTERFACE_LOG.debug("[preHandle]. mapping = " + mappingPath);
+				INTERFACE_LOG.debug("[preHandle]. mapping = " + mapping);
 			}
 
-			request.setAttribute("$mappingPath", mappingPath);
-			request.setAttribute("$ctrClass", controllerObject.getClass());
-
-		} else {
-			INTERFACE_LOG.error("cry.....");
-			return false;
+			request.setAttribute(HttpServletRequestAttribute.$_MAPPING.name(), mapping);
+			return true;
 		}
 
 		return true;
@@ -219,10 +128,8 @@ public class FirstInterceptor implements HandlerInterceptor {
 	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
 			Object handler, Exception ex) throws Exception {
 		LOG.debug("afterCompletion... ...");
-		// 清理MDC
-		if (LOG instanceof SLF4JImpl) {
-			MDC.clear();
-		}
+		// 清理log4j线程上下文
+		ThreadContext.clearAll();
 	}
 
-}
+}

+ 0 - 124
src/main/java/cn/com/qmth/examcloud/web/interceptor/SeqlockInterceptor.java

@@ -1,124 +0,0 @@
-package cn.com.qmth.examcloud.web.interceptor;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.web.method.HandlerMethod;
-import org.springframework.web.servlet.HandlerInterceptor;
-import org.springframework.web.servlet.ModelAndView;
-
-import cn.com.qmth.examcloud.api.commons.security.bean.User;
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
-import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
-import cn.com.qmth.examcloud.web.redis.RedisClient;
-import cn.com.qmth.examcloud.web.support.ServletUtil;
-import cn.com.qmth.examcloud.web.support.StatusResponse;
-
-/**
- * 顺序锁拦截器
- *
- * @author WANGWEI
- * @date 2018年11月12日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-public class SeqlockInterceptor implements HandlerInterceptor {
-
-	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(SeqlockInterceptor.class);
-
-	/**
-	 * redis client
-	 */
-	private RedisClient redisClient;
-
-	private static final String LOCK_PREFIX = "$_lock:";
-
-	/**
-	 * 构造函数
-	 *
-	 * @param redisClient
-	 * @param exclusions
-	 */
-	public SeqlockInterceptor(RedisClient redisClient) {
-		super();
-		this.redisClient = redisClient;
-	}
-
-	@Override
-	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
-			Object handler) throws Exception {
-		LOG.debug("preHandle... ...");
-
-		User user = null;
-		Object userAttribute = request.getAttribute("$accessUser");
-		if (null == userAttribute) {
-			return true;
-		} else {
-			user = (User) userAttribute;
-		}
-
-		if (handler instanceof HandlerMethod) {
-			HandlerMethod handlerMethod = (HandlerMethod) handler;
-			Seqlock seqlock = handlerMethod.getMethodAnnotation(Seqlock.class);
-			SessionSeqlock sessionSeqlock = handlerMethod.getMethodAnnotation(SessionSeqlock.class);
-			if (null != seqlock) {
-				String mappingPath = (String) request.getAttribute("$mappingPath");
-				String key = LOCK_PREFIX + mappingPath;
-
-				if (redisClient.setIfAbsent(key, ThreadLocalUtil.getTraceID(), 60 * 5)) {
-					if (LOG.isDebugEnabled()) {
-						LOG.debug("global locked");
-					}
-					return true;
-				} else {
-					response.setStatus(HttpStatus.CONFLICT.value());
-					ServletUtil.returnJson(new StatusResponse("409", "请稍后重试... ..."), response);
-					return false;
-				}
-
-			} else if (null != sessionSeqlock) {
-				String mappingPath = (String) request.getAttribute("$mappingPath");
-				String key = LOCK_PREFIX + user.getUserId() + ":" + mappingPath;
-
-				if (redisClient.setIfAbsent(key, ThreadLocalUtil.getTraceID(), 60 * 5)) {
-					if (LOG.isDebugEnabled()) {
-						LOG.debug("sesssion locked");
-					}
-					return true;
-				} else {
-					response.setStatus(HttpStatus.CONFLICT.value());
-					ServletUtil.returnJson(new StatusResponse("409", "请稍后重试... ..."), response);
-					return false;
-				}
-			}
-		}
-
-		return true;
-	}
-
-	@Override
-	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
-			ModelAndView modelAndView) throws Exception {
-		LOG.debug("postHandle... ...");
-	}
-
-	@Override
-	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
-			Object handler, Exception ex) throws Exception {
-		LOG.debug("afterCompletion... ...");
-
-		String mappingPath = (String) request.getAttribute("$mappingPath");
-		String key = LOCK_PREFIX + mappingPath;
-		redisClient.delete(key);
-
-		User user = null;
-		Object userAttribute = request.getAttribute("$accessUser");
-		if (null != userAttribute) {
-			user = (User) userAttribute;
-			String sessionKey = LOCK_PREFIX + user.getUserId() + ":" + mappingPath;
-			redisClient.delete(sessionKey);
-		}
-	}
-
-}

+ 83 - 0
src/main/java/cn/com/qmth/examcloud/web/redis/CustomRedisConfiguration.java

@@ -0,0 +1,83 @@
+package cn.com.qmth.examcloud.web.redis;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+
+@Configuration
+public class CustomRedisConfiguration {
+
+	private static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(CustomRedisConfiguration.class);
+
+	@Bean
+	public RedisTemplate<String, Object> redisTemplate(
+			@Autowired(required = false) RedisConnectionFactory redisConnectionFactory) {
+
+		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+
+		if (null == redisConnectionFactory) {
+			LOG.info("no instance of RedisConnectionFactory.");
+			redisTemplate.setConnectionFactory(new MarkedJedisConnectionFactory());
+			return redisTemplate;
+		}
+
+		LOG.info("start to create a custom instance of RedisTemplate... ...");
+
+		redisTemplate.setConnectionFactory(redisConnectionFactory);
+		Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(
+				Object.class);
+		ObjectMapper objectMapper = new ObjectMapper();
+		objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+		objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+		jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
+		redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+		redisTemplate.setKeySerializer(new StringRedisSerializer());
+		redisTemplate.afterPropertiesSet();
+
+		LOG.info("custom instance of RedisTemplate is created.");
+
+		return redisTemplate;
+	}
+
+	@Bean
+	public RedisClient redisClient(
+			@Autowired(required = true) RedisTemplate<String, Object> redisTemplate) {
+
+		LOG.info("start to create a custom instance of RedisClient... ...");
+
+		RedisClient redisClient = new SimpleRedisClient(redisTemplate);
+		RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
+		if (connectionFactory instanceof MarkedJedisConnectionFactory) {
+			redisClient.setEnable(false);
+			LOG.info("disabled instance of RedisClient is created. please be careful !");
+		} else {
+			redisClient.set("test", "test");
+			LOG.info("enabled instance of RedisClient is created.");
+		}
+
+		return redisClient;
+	}
+
+	/**
+	 * 标识工厂
+	 *
+	 * @author WANGWEI
+	 * @date 2019年3月22日
+	 * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+	 */
+	private class MarkedJedisConnectionFactory extends JedisConnectionFactory {
+	}
+
+}

+ 21 - 5
src/main/java/cn/com/qmth/examcloud/web/redis/RedisClient.java

@@ -1,14 +1,30 @@
 package cn.com.qmth.examcloud.web.redis;
 
-import java.io.Serializable;
-
 /**
- * 类注释
+ * redis client
  *
  * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
  */
 public interface RedisClient {
 
+	/**
+	 * 是否可用
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public boolean isEnable();
+
+	/**
+	 * 设置是否可用
+	 *
+	 * @author WANGWEI
+	 * @param enable
+	 */
+	public void setEnable(boolean enable);
+
 	/**
 	 * 方法注释
 	 *
@@ -16,7 +32,7 @@ public interface RedisClient {
 	 * @param key
 	 * @param value
 	 */
-	public void set(String key, Serializable value);
+	public void set(String key, Object value);
 
 	/**
 	 * 方法注释
@@ -26,7 +42,7 @@ public interface RedisClient {
 	 * @param value
 	 * @param timeout
 	 */
-	public void set(String key, Serializable value, int timeout);
+	public void set(String key, Object value, int timeout);
 
 	/**
 	 * 方法注释

+ 0 - 75
src/main/java/cn/com/qmth/examcloud/web/redis/RedisClientImpl.java

@@ -1,75 +0,0 @@
-package cn.com.qmth.examcloud.web.redis;
-
-import java.io.Serializable;
-import java.util.concurrent.TimeUnit;
-
-import org.springframework.data.redis.core.RedisTemplate;
-
-/**
- * 类注释
- *
- * @author WANGWEI
- */
-public final class RedisClientImpl implements RedisClient {
-
-	private RedisTemplate<String, Object> redisTemplate;
-
-	public RedisClientImpl(RedisTemplate<String, Object> redisTemplate) {
-		super();
-		this.redisTemplate = redisTemplate;
-	}
-
-	@Override
-	public void set(String key, Serializable value) {
-		redisTemplate.opsForValue().set(key, value);
-	}
-
-	@Override
-	public void set(String key, Serializable value, int timeout) {
-		set(key, value);
-		expire(key, timeout);
-	}
-
-	@Override
-	public void expire(String key, int timeout) {
-		redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
-	}
-
-	@Override
-	public <T> T get(String key, Class<T> c, int timeout) {
-		Object object = redisTemplate.opsForValue().get(key);
-		@SuppressWarnings("unchecked")
-		T t = (T) object;
-		expire(key, timeout);
-		return t;
-	}
-
-	@Override
-	public <T> T get(String key, Class<T> c) {
-		Object object = redisTemplate.opsForValue().get(key);
-		@SuppressWarnings("unchecked")
-		T t = (T) object;
-		return t;
-	}
-
-	@Override
-	public void delete(String key) {
-		redisTemplate.opsForValue().set(key, null);
-		expire(key, 0);
-	}
-
-	@Override
-	public void convertAndSend(String channel, Object message) {
-		redisTemplate.convertAndSend(channel, message);
-	}
-
-	@Override
-	public Boolean setIfAbsent(String key, String value, int timeout) {
-		Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value);
-		if (b) {
-			expire(key, timeout);
-		}
-		return b;
-	}
-
-}

+ 132 - 0
src/main/java/cn/com/qmth/examcloud/web/redis/SimpleRedisClient.java

@@ -0,0 +1,132 @@
+package cn.com.qmth.examcloud.web.redis;
+
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.redis.core.RedisTemplate;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+
+/**
+ * redis client
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public final class SimpleRedisClient implements RedisClient {
+
+	private static final ExamCloudLog REDIS_LOG = ExamCloudLogFactory.getLog("REDIS_LOGGER");
+
+	private RedisTemplate<String, Object> redisTemplate;
+
+	private boolean enable = true;
+
+	public SimpleRedisClient(RedisTemplate<String, Object> redisTemplate) {
+		super();
+		this.redisTemplate = redisTemplate;
+	}
+
+	private void beforeMethod() {
+		if (!enable) {
+			throw new ExamCloudRuntimeException("RedisClient is not enabled");
+		}
+	}
+
+	private void afterMethod(String method, long startTimeMillis) {
+		if (REDIS_LOG.isDebugEnabled()) {
+			String s = String.format("[SimpleRedisClient.%s] cost %d ms.", method,
+					System.currentTimeMillis() - startTimeMillis);
+			REDIS_LOG.debug(s);
+		}
+	}
+
+	@Override
+	public void set(String key, Object value) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.opsForValue().set(key, value);
+		afterMethod("set(String key, Object value)", s);
+	}
+
+	@Override
+	public void set(String key, Object value, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		set(key, value);
+		expire(key, timeout);
+		afterMethod("set(String key, Object value, int timeout)", s);
+	}
+
+	@Override
+	public void expire(String key, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
+		afterMethod("expire(String key, int timeout)", s);
+	}
+
+	@Override
+	public <T> T get(String key, Class<T> c, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		Object object = redisTemplate.opsForValue().get(key);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		expire(key, timeout);
+		afterMethod("get(String key, Class<T> c, int timeout)", s);
+		return t;
+	}
+
+	@Override
+	public <T> T get(String key, Class<T> c) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		Object object = redisTemplate.opsForValue().get(key);
+		@SuppressWarnings("unchecked")
+		T t = (T) object;
+		afterMethod("get(String key, Class<T> c)", s);
+		return t;
+	}
+
+	@Override
+	public void delete(String key) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.opsForValue().set(key, null);
+		expire(key, 0);
+		afterMethod("delete(String key)", s);
+	}
+
+	@Override
+	public void convertAndSend(String channel, Object message) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		redisTemplate.convertAndSend(channel, message);
+		afterMethod("convertAndSend(String channel, Object message)", s);
+	}
+
+	@Override
+	public Boolean setIfAbsent(String key, String value, int timeout) {
+		long s = System.currentTimeMillis();
+		beforeMethod();
+		Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value);
+		if (b) {
+			expire(key, timeout);
+		}
+		afterMethod("setIfAbsent(String key, String value, int timeout)", s);
+		return b;
+	}
+
+	@Override
+	public boolean isEnable() {
+		return enable;
+	}
+
+	@Override
+	public void setEnable(boolean enable) {
+		this.enable = enable;
+	}
+
+}

+ 0 - 70
src/main/java/cn/com/qmth/examcloud/web/reports/BaseReport.java

@@ -1,70 +0,0 @@
-package cn.com.qmth.examcloud.web.reports;
-
-import java.util.Date;
-
-import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
-
-/**
- * 报表数据体基类
- *
- * @author WANGWEI
- * @date 2018年11月13日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-public abstract class BaseReport implements JsonSerializable {
-
-	private static final long serialVersionUID = -6666294808887127563L;
-
-	/**
-	 * 采集时间
-	 */
-	private Date reportTime;
-
-	/**
-	 * 是否忽略采集异常
-	 */
-	private Boolean ignoreException;
-
-	/**
-	 * 采集异常
-	 */
-	private Boolean hasException;
-
-	/**
-	 * 采集主机
-	 */
-	private String reportHost;
-
-	public Date getReportTime() {
-		return reportTime;
-	}
-
-	public void setReportTime(Date reportTime) {
-		this.reportTime = reportTime;
-	}
-
-	public Boolean getIgnoreException() {
-		return ignoreException;
-	}
-
-	public void setIgnoreException(Boolean ignoreException) {
-		this.ignoreException = ignoreException;
-	}
-
-	public Boolean getHasException() {
-		return hasException;
-	}
-
-	public void setHasException(Boolean hasException) {
-		this.hasException = hasException;
-	}
-
-	public String getReportHost() {
-		return reportHost;
-	}
-
-	public void setReportHost(String reportHost) {
-		this.reportHost = reportHost;
-	}
-
-}

+ 0 - 33
src/main/java/cn/com/qmth/examcloud/web/reports/ReportFileFilter.java

@@ -1,33 +0,0 @@
-package cn.com.qmth.examcloud.web.reports;
-
-import java.io.File;
-import java.io.FilenameFilter;
-
-/**
- * 日志文件过滤器
- *
- * @author WANGWEI
- * @date 2018年11月14日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-public class ReportFileFilter implements FilenameFilter {
-
-	private Class<?> reportClass;
-
-	/**
-	 * 构造函数
-	 *
-	 * @param reportClass
-	 */
-	public ReportFileFilter(Class<?> reportClass) {
-		super();
-		this.reportClass = reportClass;
-	}
-
-	@Override
-	public boolean accept(File dir, String name) {
-		String regex = reportClass.getSimpleName() + "\\.\\d{12}\\.\\d+\\.txt";
-		return name.matches(regex);
-	}
-
-}

+ 0 - 131
src/main/java/cn/com/qmth/examcloud/web/reports/ReportLoggerFactory.java

@@ -1,131 +0,0 @@
-package cn.com.qmth.examcloud.web.reports;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import org.slf4j.LoggerFactory;
-
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.rolling.RollingFileAppender;
-import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
-import ch.qos.logback.core.util.FileSize;
-import ch.qos.logback.core.util.OptionHelper;
-import cn.com.qmth.examcloud.web.config.SystemConfig;
-
-/**
- * 报表日志工厂
- *
- * @author WANGWEI
- * @date 2018年11月13日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-public class ReportLoggerFactory {
-
-	private static final Map<String, Logger> LOGGERS_HOLDER = new HashMap<>();
-
-	private static final Object LOCK = new Object();
-
-	/**
-	 * 刷新
-	 *
-	 * @author WANGWEI
-	 */
-	public static void refresh() {
-
-		for (Entry<String, Logger> entry : LOGGERS_HOLDER.entrySet()) {
-			Logger value = entry.getValue();
-			value.info(" ");
-		}
-
-	}
-
-	/**
-	 * 获取Logger
-	 *
-	 * @author WANGWEI
-	 * @param reportClass
-	 * @return
-	 */
-	public static Logger getLogger(Class<? extends BaseReport> reportClass) {
-		String name = reportClass.getName();
-		Logger logger = LOGGERS_HOLDER.get(name);
-		if (null != logger) {
-			return logger;
-		}
-		synchronized (LOCK) {
-			logger = LOGGERS_HOLDER.get(name);
-			if (null != logger) {
-				return logger;
-			}
-			logger = build(reportClass);
-			LOGGERS_HOLDER.put(name, logger);
-		}
-		return logger;
-	}
-
-	/**
-	 * 构建
-	 *
-	 * @author WANGWEI
-	 * @param report
-	 * @return
-	 */
-	private static Logger build(Class<? extends BaseReport> reportClass) {
-		String name = reportClass.getName();
-		String simpleName = reportClass.getSimpleName();
-		LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
-
-		Logger logger = context.getLogger("REP-" + name);
-		logger.setAdditive(false);
-		RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
-
-		appender.setContext(context);
-		appender.setName("REP-" + name);
-		appender.setFile(OptionHelper.substVars(
-				getReportsLogRootDirectory() + "/" + name + "/" + simpleName + ".log", context));
-		appender.setAppend(true);
-		appender.setPrudent(false);
-		SizeAndTimeBasedRollingPolicy<ILoggingEvent> policy = new SizeAndTimeBasedRollingPolicy<>();
-		String fp = OptionHelper.substVars(getReportsLogRootDirectory() + "/" + name + "/"
-				+ simpleName + ".%d{yyyyMMddHHmm}.%i.txt", context);
-
-		policy.setMaxFileSize(FileSize.valueOf("32MB"));
-		policy.setFileNamePattern(fp);
-		policy.setMaxHistory(1024);
-		policy.setTotalSizeCap(FileSize.valueOf("32GB"));
-		policy.setParent(appender);
-		policy.setContext(context);
-		policy.start();
-
-		PatternLayoutEncoder encoder = new PatternLayoutEncoder();
-		encoder.setContext(context);
-		encoder.setPattern("%m");
-		encoder.start();
-
-		appender.setRollingPolicy(policy);
-		appender.setEncoder(encoder);
-		appender.start();
-
-		logger.addAppender(appender);
-
-		return logger;
-	}
-
-	/**
-	 * 获取报告根目录
-	 *
-	 * @author WANGWEI
-	 * @return
-	 */
-	private static String getReportsLogRootDirectory() {
-		String dataDir = SystemConfig.getDataDir();
-		if (!dataDir.endsWith("/")) {
-			dataDir += "/";
-		}
-		return dataDir + "reports";
-	}
-}

+ 0 - 36
src/main/java/cn/com/qmth/examcloud/web/reports/ReportsCollector.java

@@ -1,36 +0,0 @@
-package cn.com.qmth.examcloud.web.reports;
-
-import java.util.List;
-
-import com.google.common.collect.Lists;
-
-import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
-
-/**
- * 报表数据收集器工具
- *
- * @author WANGWEI
- * @date 2018年11月13日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-public class ReportsCollector {
-
-	/**
-	 * 采集数据
-	 *
-	 * @author WANGWEI
-	 * @param report
-	 */
-	public static void collect(BaseReport report) {
-		ThreadLocalUtil.set("$_HAS_COLLECTED", true);
-		String key = "$_REPORT_COLLECTIONS";
-		@SuppressWarnings("unchecked")
-		List<BaseReport> reportList = (List<BaseReport>) ThreadLocalUtil.get(key);
-		if (null == reportList) {
-			reportList = Lists.newArrayList();
-			ThreadLocalUtil.set(key, reportList);
-		}
-		reportList.add(report);
-	}
-
-}

+ 0 - 139
src/main/java/cn/com/qmth/examcloud/web/reports/ReportsController.java

@@ -1,139 +0,0 @@
-package cn.com.qmth.examcloud.web.reports;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import cn.com.qmth.examcloud.commons.exception.StatusException;
-import cn.com.qmth.examcloud.web.config.SystemConfig;
-import cn.com.qmth.examcloud.web.redis.RedisClient;
-import cn.com.qmth.examcloud.web.support.ControllerSupport;
-import io.swagger.annotations.ApiOperation;
-
-/**
- * 报表采集和回调
- *
- * @author WANGWEI
- * @date 2018年7月4日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-@RestController
-@RequestMapping("reports")
-public class ReportsController extends ControllerSupport {
-
-	@Value("${spring.application.name}")
-	String application;
-
-	@Autowired
-	RedisClient redisClient;
-
-	/**
-	 * 获取报告文件
-	 *
-	 * @author WANGWEI
-	 * @throws IOException
-	 */
-	@ApiOperation(value = "获取报告文件")
-	@GetMapping("getReportFile")
-	public ResponseEntity<?> getReportFile(@RequestParam(required = true) String className)
-			throws IOException {
-
-		if (StringUtils.isBlank(className)) {
-			throw new StatusException("580", "className is blank");
-		}
-		Class<?> c = null;
-		try {
-			c = Class.forName(className);
-		} catch (ClassNotFoundException e) {
-			throw new StatusException("581", "className is wrong");
-		}
-		if (!c.isAssignableFrom(BaseReport.class)) {
-			throw new StatusException("582", "className is wrong");
-		}
-
-		String dirPath = getReportsLogRootDirectory() + "/" + className;
-
-		File dir = new File(dirPath);
-
-		if ((!dir.exists()) || (!dir.isDirectory())) {
-			new ResponseEntity<>(HttpStatus.NO_CONTENT);
-		}
-
-		String[] fileNames = dir.list(new ReportFileFilter(c));
-
-		if (null == fileNames || 0 == fileNames.length) {
-			new ResponseEntity<>(HttpStatus.NO_CONTENT);
-		}
-		List<String> fileNameList = Arrays.asList(fileNames);
-		Collections.sort(fileNameList);
-
-		String fileName = fileNameList.get(0);
-		String filePath = dirPath + "/" + fileName;
-
-		File file = new File(filePath);
-		HttpHeaders headers = new HttpHeaders();
-		headers.setContentDispositionFormData("attachment", fileName);
-		headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
-		return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file), headers,
-				HttpStatus.OK);
-	}
-
-	/**
-	 * 删除报告文件
-	 *
-	 * @author WANGWEI
-	 * @throws IOException
-	 */
-	@GetMapping("delReportFile")
-	public void delReportFile(@RequestParam(required = true) String className,
-			@RequestParam(required = true) String fileName) throws IOException {
-
-		if (StringUtils.isBlank(className)) {
-			throw new StatusException("580", "className is blank");
-		}
-		Class<?> c = null;
-		try {
-			c = Class.forName(className);
-		} catch (ClassNotFoundException e) {
-			throw new StatusException("581", "className is wrong");
-		}
-		if (!c.isAssignableFrom(BaseReport.class)) {
-			throw new StatusException("582", "className is wrong");
-		}
-
-		String dirPath = getReportsLogRootDirectory() + "/" + className;
-		String filePath = dirPath + "/" + fileName;
-
-		FileUtils.forceDelete(new File(filePath));
-	}
-
-	/**
-	 * 获取报告根目录
-	 *
-	 * @author WANGWEI
-	 * @return
-	 */
-	private String getReportsLogRootDirectory() {
-		String dataDir = SystemConfig.getDataDir();
-		if (!dataDir.endsWith("/")) {
-			dataDir += "/";
-		}
-		return dataDir + "reports";
-	}
-
-}

+ 46 - 0
src/main/java/cn/com/qmth/examcloud/web/security/ResourceManager.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.examcloud.web.security;
+
+import cn.com.qmth.examcloud.api.commons.security.bean.AccessApp;
+import cn.com.qmth.examcloud.api.commons.security.bean.User;
+import springfox.documentation.service.ApiInfo;
+
+/**
+ * 资源管理器
+ *
+ * @author WANGWEI
+ * @date 2019年2月15日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public interface ResourceManager {
+
+	/**
+	 * 获取APP 密钥
+	 *
+	 * @author WANGWEI
+	 * @param appId
+	 * @return
+	 */
+	AccessApp getAccessApp(Long appId);
+
+	/**
+	 * 请求的接口是否裸奔
+	 *
+	 * @author WANGWEI
+	 * @param apiInfo
+	 * @param mapping
+	 * @return
+	 */
+	boolean isNaked(ApiInfo apiInfo, String mapping);
+
+	/**
+	 * 分区用户是否有权限访问接口
+	 *
+	 * @author WANGWEI
+	 * @param user
+	 * @param apiInfo
+	 * @param mapping
+	 * @return
+	 */
+	boolean hasPermission(User user, ApiInfo apiInfo, String mapping);
+
+}

+ 108 - 18
src/main/java/cn/com/qmth/examcloud/web/security/SpringCloudInterceptor.java

@@ -3,15 +3,19 @@ package cn.com.qmth.examcloud.web.security;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.ThreadContext;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.servlet.HandlerInterceptor;
 import org.springframework.web.servlet.ModelAndView;
 
 import cn.com.qmth.examcloud.api.commons.CloudService;
+import cn.com.qmth.examcloud.api.commons.security.bean.AccessApp;
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
-import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
-import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.commons.util.ByteUtil;
+import cn.com.qmth.examcloud.commons.util.SHA256;
+import cn.com.qmth.examcloud.commons.util.StringUtil;
 import cn.com.qmth.examcloud.web.support.ServletUtil;
 import cn.com.qmth.examcloud.web.support.StatusResponse;
 
@@ -27,16 +31,7 @@ public final class SpringCloudInterceptor implements HandlerInterceptor {
 	private static final ExamCloudLog LOG = ExamCloudLogFactory
 			.getLog(SpringCloudInterceptor.class);
 
-	/**
-	 * 接口日志
-	 */
-	protected static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
-			.getLog("INTERFACE_LOGGER");
-
-	/**
-	 * redis client
-	 */
-	private RedisClient redisClient;
+	private ResourceManager resourceManager;
 
 	@Override
 	public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
@@ -48,14 +43,109 @@ public final class SpringCloudInterceptor implements HandlerInterceptor {
 			return true;
 		}
 
-		Long timestamp = redisClient.get("$_RMI_:" + ThreadLocalUtil.getTraceID(), Long.class);
+		String appId = request.getHeader("App-Id");
+		String appCode = request.getHeader("App-Code");
+		String timestamp = request.getHeader("timestamp");
+		String accessToken = request.getHeader("Access-Token");
+		if (StringUtils.isBlank(appId)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' is blank"), response);
+			return false;
+		}
+		Long appIdLong = null;
+		try {
+			appIdLong = Long.parseLong(appId);
+		} catch (Exception e) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' must be a long"), response);
+			return false;
+		}
+		if (StringUtils.isBlank(appCode)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Code' is blank"), response);
+			return false;
+		}
+		if (StringUtils.isBlank(timestamp)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Code' is blank"), response);
+			return false;
+		}
+		Long timestampLong = null;
+		try {
+			timestampLong = Long.parseLong(timestamp);
+		} catch (Exception e) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'timestamp' must be a long"), response);
+			return false;
+		}
+
+		if (StringUtils.isBlank(accessToken)) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'Access-Token' is blank"), response);
+			return false;
+		}
+
+		AccessApp accessApp = resourceManager.getAccessApp(appIdLong);
+		if (null == accessApp) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' is wrong"), response);
+			return false;
+		}
+		if (!appCode.equals(accessApp.getAppCode())) {
+			// 401
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil
+					.returnJson(new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+							"'App-Id' & 'App-Code' are wrong"), response);
+			return false;
+		}
+
+		if (null != accessApp.getTimeRange()) {
+			long currentTimeMillis = System.currentTimeMillis();
+			if (Math.abs(currentTimeMillis - timestampLong) > accessApp.getTimeRange()) {
+				response.setStatus(HttpStatus.UNAUTHORIZED.value());
+				ServletUtil.returnJson(
+						new StatusResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),
+								"'timestamp' is out"),
+						response);
+				return false;
+			}
+		}
+
+		String secretKey = accessApp.getSecretKey();
+		String joinStr = StringUtil.join(appId, appCode, timestamp, secretKey);
+		byte[] bytes = SHA256.encode(joinStr);
+		String hexAscii = ByteUtil.toHexAscii(bytes);
 
-		if (null == timestamp) {
-			response.setStatus(HttpStatus.REQUEST_TIMEOUT.value());
-			ServletUtil.returnJson(new StatusResponse("408", "Request Timeout"), response);
+		if (!hexAscii.equalsIgnoreCase(accessToken)) {
+			response.setStatus(HttpStatus.UNAUTHORIZED.value());
+			ServletUtil.returnJson(new StatusResponse(
+					String.valueOf(HttpStatus.UNAUTHORIZED.value()), "access failure"), response);
 			return false;
 		}
 
+		ThreadContext.put("CALLER", "APP:" + accessApp.getAppCode());
 		return true;
 	}
 
@@ -71,8 +161,8 @@ public final class SpringCloudInterceptor implements HandlerInterceptor {
 		LOG.debug("afterCompletion... ...");
 	}
 
-	public void setRedisClient(RedisClient redisClient) {
-		this.redisClient = redisClient;
+	public void setResourceManager(ResourceManager resourceManager) {
+		this.resourceManager = resourceManager;
 	}
 
 }

+ 6 - 5
src/main/java/cn/com/qmth/examcloud/web/interceptor/Seqlock.java → src/main/java/cn/com/qmth/examcloud/web/support/ApiId.java

@@ -1,4 +1,4 @@
-package cn.com.qmth.examcloud.web.interceptor;
+package cn.com.qmth.examcloud.web.support;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
@@ -7,15 +7,16 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * 请求顺序锁(全局) 注解
+ * 接口ID 注解
  *
  * @author WANGWEI
- * @date 2018年11月12
+ * @date 2019年3月15
  * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
  */
-@Target({ElementType.METHOD})
+@Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
-public @interface Seqlock {
+public @interface ApiId {
 
+	int value();
 }

+ 108 - 0
src/main/java/cn/com/qmth/examcloud/web/support/ApiInfo.java

@@ -0,0 +1,108 @@
+package cn.com.qmth.examcloud.web.support;
+
+import java.io.Serializable;
+
+/**
+ * 接口
+ *
+ * @author WANGWEI
+ * @date 2018年5月14日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+
+public class ApiInfo implements Serializable {
+
+	private static final long serialVersionUID = 1553810211239843543L;
+
+	/**
+	 * ID
+	 */
+	private Integer id;
+
+	/**
+	 * 映射
+	 */
+	private String mapping;
+
+	/**
+	 * mapping路径
+	 */
+	private String requestPath;
+
+	/**
+	 * http方法
+	 */
+	private String httpMethod;
+
+	/**
+	 * 接口描述
+	 */
+	private String description;
+
+	/**
+	 * 请求处理类
+	 */
+	private transient Class<?> beanType;
+
+	/**
+	 * 接口日志忽略堆栈
+	 */
+	private boolean withoutStackTrace;
+
+	public Integer getId() {
+		return id;
+	}
+
+	public void setId(Integer id) {
+		this.id = id;
+	}
+
+	public String getMapping() {
+		return mapping;
+	}
+
+	public void setMapping(String mapping) {
+		this.mapping = mapping;
+	}
+
+	public String getRequestPath() {
+		return requestPath;
+	}
+
+	public void setRequestPath(String requestPath) {
+		this.requestPath = requestPath;
+	}
+
+	public String getHttpMethod() {
+		return httpMethod;
+	}
+
+	public void setHttpMethod(String httpMethod) {
+		this.httpMethod = httpMethod;
+	}
+
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
+	public Class<?> getBeanType() {
+		return beanType;
+	}
+
+	public void setBeanType(Class<?> beanType) {
+		this.beanType = beanType;
+	}
+
+	public boolean isWithoutStackTrace() {
+		return withoutStackTrace;
+	}
+
+	public void setWithoutStackTrace(boolean withoutStackTrace) {
+		this.withoutStackTrace = withoutStackTrace;
+	}
+
+}

+ 158 - 0
src/main/java/cn/com/qmth/examcloud/web/support/ApiInfoHolder.java

@@ -0,0 +1,158 @@
+package cn.com.qmth.examcloud.web.support;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+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 org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
+import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import io.swagger.annotations.ApiOperation;
+
+@Component
+@Order(100)
+public class ApiInfoHolder implements ApplicationRunner {
+
+	private static final Map<Integer, ApiInfo> INDEX_BY_API_ID = Maps.newConcurrentMap();
+
+	private static final Map<Method, ApiInfo> INDEX_BY_METHOD = Maps.newConcurrentMap();
+
+	private static Set<ApiInfo> apiInfoSet = Sets.newHashSet();
+
+	@Autowired
+	private RequestMappingHandlerMapping requestMappingHandlerMapping;
+
+	/**
+	 * 通过apiId获取ApiInfo
+	 *
+	 * @author WANGWEI
+	 * @param interfaceId
+	 * @return
+	 */
+	public static ApiInfo getApiInfo(Integer interfaceId) {
+		return INDEX_BY_API_ID.get(interfaceId);
+	}
+
+	/**
+	 * 通过方法获取ApiInfo
+	 *
+	 * @author WANGWEI
+	 * @param method
+	 * @return
+	 */
+	public static ApiInfo getApiInfo(Method method) {
+		return INDEX_BY_METHOD.get(method);
+	}
+
+	/**
+	 * 获取@ApiId注解的ApiInfo集合
+	 *
+	 * @author WANGWEI
+	 * @return
+	 */
+	public static Set<ApiInfo> getApiInfoSet() {
+		return apiInfoSet;
+	}
+
+	@Override
+	public void run(ApplicationArguments args) throws Exception {
+
+		Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping
+				.getHandlerMethods();
+
+		Set<Integer> apiIdSet = Sets.newHashSet();
+
+		for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
+			RequestMappingInfo requestMappingInfo = entry.getKey();
+			HandlerMethod handlerMethod = entry.getValue();
+
+			Class<?> beanType = handlerMethod.getBeanType();
+
+			RequestMapping requestMappingAnnotationOfClass = AnnotationUtils
+					.findAnnotation(beanType, RequestMapping.class);
+
+			RequestMapping requestMappingAnnotationOfMethod = handlerMethod
+					.getMethodAnnotation(RequestMapping.class);
+
+			ApiId apiId = handlerMethod.getMethodAnnotation(ApiId.class);
+			WithoutStackTrace withoutStackTrace = handlerMethod
+					.getMethodAnnotation(WithoutStackTrace.class);
+
+			ApiOperation apiOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
+
+			RequestMethodsRequestCondition requestMethodsRequestCondition = requestMappingInfo
+					.getMethodsCondition();
+			Set<RequestMethod> requestMethodSet = requestMethodsRequestCondition.getMethods();
+
+			String[] mappingURIsOfClass = null;
+			String[] mappingURIsOfMethod = null;
+
+			if (null != requestMappingAnnotationOfClass) {
+				mappingURIsOfClass = requestMappingAnnotationOfClass.path();
+			}
+			if (null != requestMappingAnnotationOfMethod) {
+				mappingURIsOfMethod = requestMappingAnnotationOfMethod.path();
+			}
+
+			String mappingIdentifyOfClass = null;
+			String mappingIdentifyOfMethod = null;
+
+			if (null != mappingURIsOfClass) {
+				mappingIdentifyOfClass = StringUtils.join(mappingURIsOfClass, ",");
+			}
+			if (null != mappingURIsOfMethod) {
+				mappingIdentifyOfMethod = StringUtils.join(mappingURIsOfMethod, ",");
+			}
+
+			String methods = StringUtils.join(requestMethodSet, ",");
+
+			String mapping = StringUtils.join("[", mappingIdentifyOfClass, "][",
+					mappingIdentifyOfMethod, "][", methods, "]");
+
+			PatternsRequestCondition patternsRequestCondition = requestMappingInfo
+					.getPatternsCondition();
+
+			String requestPath = StringUtils.join(patternsRequestCondition.getPatterns(), ",");
+
+			ApiInfo apiInfo = new ApiInfo();
+			if (null != apiOperation) {
+				apiInfo.setDescription(apiOperation.value());
+			}
+			apiInfo.setHttpMethod(methods);
+			apiInfo.setMapping(mapping);
+			apiInfo.setRequestPath(requestPath);
+			apiInfo.setBeanType(beanType);
+			if (null != withoutStackTrace) {
+				apiInfo.setWithoutStackTrace(withoutStackTrace.value());
+			}
+
+			if (null != apiId) {
+				if (apiIdSet.contains(apiId.value())) {
+					throw new ExamCloudRuntimeException("duplicate apiId. apiId=" + apiId.value());
+				}
+				apiIdSet.add(apiId.value());
+				apiInfo.setId(apiId.value());
+				INDEX_BY_API_ID.put(apiInfo.getId(), apiInfo);
+				apiInfoSet.add(apiInfo);
+			}
+			INDEX_BY_METHOD.put(handlerMethod.getMethod(), apiInfo);
+		}
+	}
+
+}

+ 90 - 56
src/main/java/cn/com/qmth/examcloud/web/support/ControllerAspect.java

@@ -1,21 +1,20 @@
 package cn.com.qmth.examcloud.web.support;
 
 import java.util.Collection;
-import java.util.Date;
-import java.util.List;
 import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
 
-import ch.qos.logback.classic.Logger;
 import cn.com.qmth.examcloud.api.commons.exchange.BaseResponse;
 import cn.com.qmth.examcloud.api.commons.exchange.JsonSerializable;
 import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
@@ -24,29 +23,47 @@ import cn.com.qmth.examcloud.commons.util.JsonUtil;
 import cn.com.qmth.examcloud.commons.util.ObjectUtil;
 import cn.com.qmth.examcloud.commons.util.StringUtil;
 import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
-import cn.com.qmth.examcloud.web.reports.BaseReport;
-import cn.com.qmth.examcloud.web.reports.ReportLoggerFactory;
+import cn.com.qmth.examcloud.web.config.LogProperties;
+import cn.com.qmth.examcloud.web.enums.HttpServletRequestAttribute;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
 
 /**
  * spring mvc controller aspect.
  *
- * @author WANG WEI
- *
+ * @author WANGWEI
+ * @date 2019年1月30日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
  */
 @Component
 @Aspect
 public class ControllerAspect {
 
-	/**
-	 * 接口日志
-	 */
+	private static final ExamCloudLog DEBUG_LOG = ExamCloudLogFactory
+			.getLog(ControllerAspect.class);
+
 	private static final ExamCloudLog INTERFACE_LOG = ExamCloudLogFactory
 			.getLog("INTERFACE_LOGGER");
 
+	@Autowired(required = false)
+	HttpMethodProcessor httpMethodProcessor;
+
+	@Autowired
+	LogProperties logProperties;
+
+	@Autowired(required = false)
+	RedisClient redisClient;
+
+	/**
+	 * 构造函数
+	 *
+	 */
 	public ControllerAspect() {
-		System.out.println("=================ControllerAspect=================");
+		super();
+		DEBUG_LOG.info("Aspect class [ControllerAspect]  is active!");
 	}
 
+	private static String[] excludeFields = new String[]{"password", ".*Password"};
+
 	/**
 	 * handlerMethods
 	 *
@@ -79,16 +96,18 @@ public class ControllerAspect {
 		String path = request.getServletPath();
 		String method = request.getMethod();
 
-		// 请求信息
-		String inMsg = StringUtil.join("[HTTP-IN]. path=\"", path, "\", method=[", method, "]");
-		INTERFACE_LOG.info(inMsg);
+		if (INTERFACE_LOG.isInfoEnabled()) {
+			INTERFACE_LOG
+					.info(StringUtil.join("[HTTP-IN]. path=\"", path, "\", method=[", method, "]"));
+		}
 
 		Object[] args = joinPoint.getArgs();
 
 		if (INTERFACE_LOG.isInfoEnabled()) {
 			if (null != args) {
 				if (1 == args.length && args[0] instanceof JsonSerializable) {
-					INTERFACE_LOG.info("[HTTP-REQ]. request=" + JsonUtil.toJson(args[0]));
+					INTERFACE_LOG
+							.info("[HTTP-REQ]. request=" + JsonUtil.toJson(args[0], excludeFields));
 				} else if (1 <= args.length) {
 					StringBuilder sb = new StringBuilder();
 					sb.append("[HTTP-REQ]. args: ");
@@ -111,55 +130,51 @@ public class ControllerAspect {
 		}
 
 		Object ret = null;
-		boolean ok = false;
 		try {
-
+			if (null != httpMethodProcessor) {
+				httpMethodProcessor.beforeMethod(request, args);
+			}
 			// 执行
 			ret = joinPoint.proceed();
-			ok = true;
-			long cost = System.currentTimeMillis() - startTime;
-
-			String outMsg = StringUtil.join("[HTTP-OK]. path=\"", path, "\", method=[", method,
-					"] ; cost ", cost, " ms.");
-
-			INTERFACE_LOG.info(outMsg);
-
-			if (null != ret && BaseResponse.class.isAssignableFrom(ret.getClass())) {
-				BaseResponse baseResponse = (BaseResponse) ret;
-				baseResponse.setCost(cost);
-			}
-
 		} catch (Throwable e) {
-			String exceptionMsg = StringUtil.join("[HTTP-FAIL]. path=\"", path, "\", method=[",
-					method, "] ; cost ", System.currentTimeMillis() - startTime, " ms.");
-			INTERFACE_LOG.error(exceptionMsg);
+			if (null != httpMethodProcessor) {
+				try {
+					httpMethodProcessor.onException(request, args, e);
+				} catch (Exception ex) {
+					INTERFACE_LOG.error("[AFTER-METHOD].onException()", ex);
+				}
+			}
+			INTERFACE_LOG.error(StringUtil.join("[HTTP-FAIL]. path=\"", path, "\", method=[",
+					method, "] ; cost ", System.currentTimeMillis() - startTime, " ms."));
 			throw new RuntimeException(e);
 		} finally {
-			Boolean hasCollected = (Boolean) ThreadLocalUtil.get("$_HAS_COLLECTED");
-			if (null != hasCollected && hasCollected) {
-				String key = "$_REPORT_COLLECTIONS";
-				@SuppressWarnings("unchecked")
-				List<BaseReport> reportList = (List<BaseReport>) ThreadLocalUtil.get(key);
-				for (BaseReport report : reportList) {
-					Boolean ignoreException = report.getIgnoreException();
-					if (null == ignoreException) {
-						ignoreException = false;
-					}
-					if (ok || ignoreException) {
-						Logger logger = ReportLoggerFactory.getLogger(report.getClass());
-						report.setReportTime(new Date());
-						report.setHasException(!ok);
-						logger.info(JsonUtil.toJson(report) + "\n");
-					}
+			if (null != redisClient && redisClient.isEnable()) {
+				Object customSequenceLock = request
+						.getAttribute(HttpServletRequestAttribute.$_CUSTOM_SEQUENCE_LOCK.name());
+				if (null != customSequenceLock) {
+					redisClient.delete((String) customSequenceLock);
 				}
 			}
 		}
 
-		if (INTERFACE_LOG.isInfoEnabled()) {
+		if (null != httpMethodProcessor) {
+			try {
+				httpMethodProcessor.onSuccess(request, args, ret);
+			} catch (Exception ex) {
+				INTERFACE_LOG.error("[AFTER-METHOD].onSuccess()", ex);
+			}
+		}
+
+		if (null != ret && BaseResponse.class.isAssignableFrom(ret.getClass())) {
+			BaseResponse baseResponse = (BaseResponse) ret;
+			baseResponse.setCost(System.currentTimeMillis() - startTime);
+		}
+
+		if (INTERFACE_LOG.isDebugEnabled() && logProperties.isNormalResponseLogEnable()) {
 			boolean jsonSerializable = false;
 			if (null == ret) {
-				INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + HttpStatus.OK);
-				INTERFACE_LOG.info("[HTTP-RESP]. response=void");
+				INTERFACE_LOG.debug("[HTTP-RESP]. status=" + HttpStatus.OK);
+				INTERFACE_LOG.debug("[HTTP-RESP]. response=void");
 			} else if (ret instanceof ResponseEntity) {
 				ResponseEntity<?> re = (ResponseEntity<?>) ret;
 				Object body = re.getBody();
@@ -173,9 +188,15 @@ public class ControllerAspect {
 					} else if (body instanceof String) {
 						jsonSerializable = true;
 					}
-					INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + re.getStatusCodeValue());
+					INTERFACE_LOG.debug("[HTTP-RESP]. status=" + re.getStatusCodeValue());
 					if (jsonSerializable) {
-						INTERFACE_LOG.info("[HTTP-RESP]. response=" + JsonUtil.toJson(body));
+						String json = JsonUtil.toJson(body);
+						int responseJsonMaxSize = logProperties.getResponseLogJsonMaxSize();
+						if (json.length() > responseJsonMaxSize) {
+							INTERFACE_LOG.debug("[HTTP-RESP]. response= too large");
+						} else {
+							INTERFACE_LOG.debug("[HTTP-RESP]. response=" + json);
+						}
 					}
 				}
 			} else {
@@ -188,13 +209,26 @@ public class ControllerAspect {
 				} else if (ret instanceof String) {
 					jsonSerializable = true;
 				}
-				INTERFACE_LOG.info("[HTTP-RESP]. StatusCode=" + HttpStatus.OK);
+				INTERFACE_LOG.info("[HTTP-RESP]. status=" + HttpStatus.OK.value());
 				if (jsonSerializable) {
-					INTERFACE_LOG.info("[HTTP-RESP]. response=" + JsonUtil.toJson(ret));
+					String json = JsonUtil.toJson(ret);
+					int responseJsonMaxSize = logProperties.getResponseLogJsonMaxSize();
+					if (json.length() > responseJsonMaxSize) {
+						INTERFACE_LOG.debug("[HTTP-RESP]. response= too large");
+					} else {
+						INTERFACE_LOG.debug("[HTTP-RESP]. response=" + json);
+					}
 				}
 			}
 		}
 
+		HttpServletResponse response = ServletUtil.getResponse();
+		response.setHeader("Trace-Id", ThreadLocalUtil.getTraceId());
+		response.setHeader("cost", String.valueOf(System.currentTimeMillis() - startTime));
+
+		INTERFACE_LOG.info(StringUtil.join("[HTTP-OK]. path=\"", path, "\", method=[", method,
+				"] ; cost ", System.currentTimeMillis() - startTime, " ms."));
+
 		return ret;
 	}
 

+ 62 - 0
src/main/java/cn/com/qmth/examcloud/web/support/ExamCloudController.java

@@ -0,0 +1,62 @@
+package cn.com.qmth.examcloud.web.support;
+
+import java.util.Collections;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+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.commons.logging.ExamCloudLog;
+import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
+import cn.com.qmth.examcloud.commons.util.DateUtil;
+
+/**
+ * examcloud controller
+ *
+ * @author WANGWEI
+ * @date 2018年8月23日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@RestController
+public class ExamCloudController {
+
+	protected static final ExamCloudLog LOG = ExamCloudLogFactory.getLog(ExamCloudController.class);
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.GET)
+	public String get() {
+		return DateUtil.chinaNow();
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.HEAD)
+	public ResponseEntity<?> head() {
+		return new ResponseEntity<Object>(HttpStatus.NO_CONTENT);
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.OPTIONS)
+	public HttpEntity<?> options() {
+		HttpHeaders headers = new HttpHeaders();
+		headers.setAllow(Collections.singleton(HttpMethod.GET));
+		return new ResponseEntity<Object>(headers, HttpStatus.OK);
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.POST)
+	public String post() {
+		return DateUtil.chinaNow();
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.PUT)
+	public String put() {
+		return DateUtil.chinaNow();
+	}
+
+	@RequestMapping(value = {"/", ""}, method = RequestMethod.DELETE)
+	public String delete() {
+		return DateUtil.chinaNow();
+	}
+
+}

+ 43 - 0
src/main/java/cn/com/qmth/examcloud/web/support/HttpMethodProcessor.java

@@ -0,0 +1,43 @@
+package cn.com.qmth.examcloud.web.support;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * spring mvc 方法前校验器
+ *
+ * @author WANGWEI
+ * @date 2019年2月22日
+ * @Copyright (c) 2018-2020 WANGWEI [QQ:522080330] All Rights Reserved.
+ */
+public interface HttpMethodProcessor {
+
+	/**
+	 * 方法前处理
+	 *
+	 * @author WANGWEI
+	 * @param request
+	 * @param args
+	 */
+	void beforeMethod(HttpServletRequest request, Object[] args);
+
+	/**
+	 * 方法执行未抛出异常时执行
+	 *
+	 * @author WANGWEI
+	 * @param request
+	 * @param args
+	 * @param ret
+	 */
+	void onSuccess(HttpServletRequest request, Object[] args, Object ret);
+
+	/**
+	 * 方法执行抛出异常时执行
+	 *
+	 * @author WANGWEI
+	 * @param request
+	 * @param args
+	 * @param e
+	 */
+	void onException(HttpServletRequest request, Object[] args, Throwable e);
+
+}

+ 0 - 42
src/main/java/cn/com/qmth/examcloud/web/support/RemoteProcedureCallTester.java

@@ -1,42 +0,0 @@
-package cn.com.qmth.examcloud.web.support;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestTemplate;
-
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLog;
-import cn.com.qmth.examcloud.commons.logging.ExamCloudLogFactory;
-
-/**
- * 远程调用测试
- *
- * @author WANGWEI
- * @date 2018年11月29日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-@Component
-public class RemoteProcedureCallTester {
-
-	protected ExamCloudLog log = ExamCloudLogFactory.getLog(RemoteProcedureCallTester.class);
-
-	@Autowired
-	RestTemplate restTemplate;
-
-	public void testRestTemplate(String... appNames) {
-		for (String appName : appNames) {
-			try {
-				log.info("[test RestTemplate].  appName=" + appName);
-				ResponseEntity<String> entity = restTemplate.getForEntity("http://" + appName,
-						String.class);
-				log.info("[test RestTemplate].  statusCode=" + entity.getStatusCodeValue());
-			} catch (Exception e) {
-				log.error("[test RestTemplate]. \n" + "WARN:\n"
-						+ "*********************************************************************\n"
-						+ e.getMessage() + "\n"
-						+ "*********************************************************************\n");
-			}
-		}
-	}
-
-}

+ 0 - 69
src/main/java/cn/com/qmth/examcloud/web/support/ResponseStatus.java

@@ -1,69 +0,0 @@
-package cn.com.qmth.examcloud.web.support;
-
-/**
- * 全局响应状态
- *
- * @author WANGWEI
- * @date 2018年5月26日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-public enum ResponseStatus {
-
-	/**
-	 * 成功
-	 */
-	OK("200", "成功"),
-
-	/**
-	 * 系统异常
-	 */
-	SERVER_ERROR("500", "系统异常"),
-
-	/**
-	 * 违反数据完整性约束
-	 */
-	DATA_INTEGRITY_VIOLATION("520", "违反数据完整性约束"),
-
-	/**
-	 * 致命错误
-	 */
-	FATAL_ERROR("555", "系统繁忙");
-
-	// ===========================================================================
-
-	/**
-	 * 码
-	 */
-	private String code;
-
-	/**
-	 * 描述
-	 */
-	private String desc;
-
-	/**
-	 * 构造函数
-	 *
-	 * @param code
-	 * @param desc
-	 */
-	private ResponseStatus(String code, String desc) {
-		this.code = code;
-		this.desc = desc;
-	}
-
-	/**
-	 * @return the code
-	 */
-	public String getCode() {
-		return code;
-	}
-
-	/**
-	 * @return the desc
-	 */
-	public String getDesc() {
-		return desc;
-	}
-
-}

+ 0 - 11
src/main/java/cn/com/qmth/examcloud/web/support/StatusResponse.java

@@ -28,17 +28,6 @@ public class StatusResponse implements JsonSerializable {
 		super();
 	}
 
-	/**
-	 * 构造函数
-	 *
-	 * @param responseStatus
-	 */
-	public StatusResponse(ResponseStatus responseStatus) {
-		super();
-		this.code = responseStatus.getCode();
-		this.desc = responseStatus.getDesc();
-	}
-
 	/**
 	 * 构造函数
 	 *

+ 36 - 0
src/main/java/cn/com/qmth/examcloud/web/support/StringTrimModule.java

@@ -0,0 +1,36 @@
+package cn.com.qmth.examcloud.web.support;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ * trim string
+ *
+ * @author WANGWEI
+ * @date 2019年2月21日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class StringTrimModule extends SimpleModule {
+
+	private static final long serialVersionUID = 4963398126059703783L;
+
+	public StringTrimModule() {
+		addDeserializer(String.class, new StdScalarDeserializer<String>(String.class) {
+
+			private static final long serialVersionUID = 8976897962538438373L;
+
+			@Override
+			public String deserialize(JsonParser jsonParser, DeserializationContext ctx)
+					throws IOException, JsonProcessingException {
+				return jsonParser.getValueAsString().trim();
+			}
+		});
+	}
+}

+ 0 - 96
src/main/java/cn/com/qmth/examcloud/web/support/SystemController.java

@@ -1,96 +0,0 @@
-package cn.com.qmth.examcloud.web.support;
-
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-import javax.servlet.ServletInputStream;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.commons.compress.utils.IOUtils;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import cn.com.qmth.examcloud.commons.util.DateUtil;
-import cn.com.qmth.examcloud.commons.util.PropertiesUtil;
-import cn.com.qmth.examcloud.web.interceptor.Seqlock;
-import cn.com.qmth.examcloud.web.interceptor.SessionSeqlock;
-import io.swagger.annotations.ApiOperation;
-
-/**
- * 测试状态.勿修改或删除
- *
- * @author WANGWEI
- * @date 2018年8月23日
- * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
- */
-@RestController("System")
-@RequestMapping
-public class SystemController extends ControllerSupport {
-
-	@Value("${$test}")
-	private String test;
-
-	@ApiOperation(value = "测试")
-	@GetMapping()
-	public String get() {
-		log.debug("@Value   $test=" + test);
-		log.debug("PropertiesUtil    $test=" + PropertiesUtil.getString("$test"));
-		return DateUtil.getNowISO();
-	}
-
-	@ApiOperation(value = "测试")
-	@PostMapping()
-	public String post() {
-		return DateUtil.getNowISO();
-	}
-
-	@ApiOperation(value = "测试")
-	@PutMapping()
-	public String put() {
-		return DateUtil.getNowISO();
-	}
-
-	@ApiOperation(value = "测试")
-	@DeleteMapping()
-	public String delete() {
-		return DateUtil.getNowISO();
-	}
-
-	@ApiOperation(value = "测试")
-	@PutMapping("uploadFile")
-	public String uploadFile(HttpServletRequest req, HttpServletResponse resp) {
-		ServletInputStream in = null;
-		try {
-			in = req.getInputStream();
-			IOUtils.copy(in, System.out);
-		} catch (IOException e) {
-			e.printStackTrace();
-		} finally {
-			IOUtils.closeQuietly(in);
-		}
-		return DateUtil.getNowISO();
-	}
-
-	@ApiOperation(value = "@Seqlock 测试")
-	@GetMapping("seqlock")
-	@Seqlock
-	public String seqlock() {
-		cn.com.qmth.examcloud.commons.util.Util.sleep(TimeUnit.SECONDS, 1);
-		return DateUtil.getNowISO();
-	}
-
-	@ApiOperation(value = "@SessionSeqlock 测试")
-	@GetMapping("sessionSeqlock")
-	@SessionSeqlock
-	public String sessionSeqlock() {
-		cn.com.qmth.examcloud.commons.util.Util.sleep(TimeUnit.SECONDS, 1);
-		return DateUtil.getNowISO();
-	}
-
-}

+ 8 - 5
src/main/java/cn/com/qmth/examcloud/web/interceptor/SessionSeqlock.java → src/main/java/cn/com/qmth/examcloud/web/support/WithoutStackTrace.java

@@ -1,4 +1,4 @@
-package cn.com.qmth.examcloud.web.interceptor;
+package cn.com.qmth.examcloud.web.support;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
@@ -7,15 +7,18 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * 请求顺序锁(用户) 注解
+ * 接口注解 <br>
+ * 忽略接口日志堆栈<br>
  *
  * @author WANGWEI
- * @date 2018年11月12
+ * @date 2019年3月15
  * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
  */
-@Target({ElementType.METHOD})
+@Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
-public @interface SessionSeqlock {
+public @interface WithoutStackTrace {
+
+	boolean value() default true;
 
 }