Parcourir la source

完善更多案例

zhzhenqin il y a 10 mois
Parent
commit
65d791c3f6

+ 0 - 1
feign_client/JobSchedulerServiceImpl.java

@@ -56,7 +56,6 @@ import feign.gson.GsonDecoder;
  * Date: 2019/3/26
  * Time: 18:43
  * Vendor: primeton.com
- * To change this template use File | Settings | File Templates.
  *
  * </pre>
  *

+ 86 - 10
httpclient/RestApiUtils.java

@@ -4,6 +4,7 @@ package com.yiidata.intergration.api.utils;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.datasophon.api.utils.HttpUtils;
 import com.google.common.io.ByteStreams;
 import com.google.common.net.HttpHeaders;
 import lombok.extern.slf4j.Slf4j;
@@ -23,6 +24,7 @@ import org.apache.http.config.RegistryBuilder;
 import org.apache.http.config.SocketConfig;
 import org.apache.http.conn.socket.ConnectionSocketFactory;
 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.DefaultHostnameVerifier;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.entity.mime.MultipartEntityBuilder;
@@ -53,13 +55,14 @@ import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 
 
 /**
  *
  * 封装 RestApi 调用
  *
- *
  * <pre>
  *
  * Created by zhaopx.
@@ -92,14 +95,17 @@ public class RestApiUtils {
             //设置协议http和https对应的处理socket链接工厂的对象
             Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                     .register("http", PlainConnectionSocketFactory.INSTANCE)
-                    .register("https", new SSLConnectionSocketFactory(createIgnoreVerifySSL()))
+                    .register("https", new SSLConnectionSocketFactory(createIgnoreVerifySSL(), null, null, new DefaultHostnameVerifier()))
                     .build();
 
             PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
             httpClient = HttpClients.custom()
                     .setConnectionManager(cm)
                     .setDefaultCookieStore(cookieStore)
-                    .setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(180000).build())
+                    .setDefaultRequestConfig(RequestConfig.custom()
+                            .setConnectTimeout(65000)
+                            .setSocketTimeout(30000)
+                            .setConnectionRequestTimeout(30000).build())
                     .setRetryHandler(new DefaultHttpRequestRetryHandler(3, false))
                     .build();
         } catch (Exception e) {
@@ -108,7 +114,6 @@ public class RestApiUtils {
     }
 
 
-
     /**
      * 绕过验证
      *
@@ -117,7 +122,7 @@ public class RestApiUtils {
      * @throws KeyManagementException
      */
     public static SSLContext createIgnoreVerifySSL() throws NoSuchAlgorithmException, KeyManagementException {
-        SSLContext sc = SSLContext.getInstance("SSLv3");
+        SSLContext sc = SSLContext.getInstance("TLSv1.2");
 
         // 实现一个X509TrustManager接口,用于绕过验证,不用修改里面的方法
         X509TrustManager trustManager = new X509TrustManager() {
@@ -145,6 +150,7 @@ public class RestApiUtils {
 
     /**
      * 调用一次远程 api
+     *
      * @param url
      * @return
      * @throws IOException
@@ -156,6 +162,7 @@ public class RestApiUtils {
 
     /**
      * 调用一次远程 api
+     *
      * @param url
      * @return
      * @throws IOException
@@ -179,6 +186,7 @@ public class RestApiUtils {
 
     /**
      * 通过 Post 请求调用Rest 接口
+     *
      * @param url
      * @param json
      * @param not200ThrowError 为 true 时,当返回不是 200,则抛出异常
@@ -192,6 +200,7 @@ public class RestApiUtils {
 
     /**
      * POST 请求,执行远程
+     *
      * @param url
      * @param jsonStr
      * @param not200ThrowError
@@ -205,6 +214,7 @@ public class RestApiUtils {
 
     /**
      * POST 请求执行远程链接
+     *
      * @param url
      * @param jsonStr 请求 Body 体
      * @param header 请求头
@@ -252,6 +262,7 @@ public class RestApiUtils {
     //---------
     /**
      * 调用一次远程 api
+     *
      * @param url
      * @return
      * @throws IOException
@@ -263,6 +274,7 @@ public class RestApiUtils {
 
     /**
      * 调用一次远程 api
+     *
      * @param url
      * @return
      * @throws IOException
@@ -286,6 +298,7 @@ public class RestApiUtils {
 
     /**
      * 通过 Post 请求调用Rest 接口
+     *
      * @param url
      * @param json
      * @param not200ThrowError 为 true 时,当返回不是 200,则抛出异常
@@ -298,6 +311,7 @@ public class RestApiUtils {
 
     /**
      * 通过 Post 请求调用Rest 接口
+     *
      * @param url
      * @param json
      * @param not200ThrowError 为 true 时,当返回不是 200,则抛出异常
@@ -322,19 +336,25 @@ public class RestApiUtils {
         try {
             resp = httpClient.execute(get);
             log.info("execute[get] url {} return code: {}", url, resp.getStatusLine().getStatusCode());
-            HttpEntity entity = resp.getEntity();
-            String result = EntityUtils.toString(entity);
+            final String contentType = Optional.ofNullable(resp.getFirstHeader("Content-Type")).map(Header::getValue).orElse("text/html");
+            final HttpEntity entity = resp.getEntity();
+            String result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
             EntityUtils.consume(entity);
             if(not200ThrowError && resp.getStatusLine().getStatusCode() != 200) {
                 throw new IOException(result);
             }
-            Object jsonResult = JSON.parse(result);
+            // 判断一下返回 类型
             JSONObject jsonObject = new JSONObject(2);
+            if(!contentType.contains("json")) {
+                jsonObject.put("result", result);
+            } else {
+                Object jsonResult = JSON.parse(result);
             if(jsonResult instanceof JSONArray) {
                 jsonObject.put("result", jsonResult);
             } else {
                 jsonObject = (JSONObject) jsonResult;
             }
+            }
             jsonObject.put("status_code", resp.getStatusLine().getStatusCode());
             return jsonObject;
         } finally {
@@ -353,7 +373,7 @@ public class RestApiUtils {
      * @return 返回 下载的文件路径
      */
     public static String download(String url, File downloadDir) throws IOException {
-        return download(url, new HashMap<>(), downloadDir);
+        return download(url, new HashMap<>(), downloadDir, new NoProcessCall());
     }
 
     /**
@@ -364,6 +384,17 @@ public class RestApiUtils {
      * @return 返回 下载的文件路径
      */
     public static String download(String url, Map<String, String> headers, File downloadDir) throws IOException {
+        return download(url, headers, downloadDir, new NoProcessCall());
+    }
+
+    /**
+     * 根据url下载文件,保存到filepath中
+     *
+     * @param url
+     * @param downloadDir
+     * @return 返回 下载的文件路径
+     */
+    public static String download(String url, Map<String, String> headers, File downloadDir, ProcessCall call) throws IOException {
         if(!downloadDir.exists()) {
             if(!downloadDir.mkdirs()) {
                 throw new IOException(downloadDir.getAbsolutePath() + " not exists, do can not mkdir.");
@@ -407,10 +438,27 @@ public class RestApiUtils {
         HttpEntity entity = response.getEntity();
         File filepath = new File(downloadDir, fileName);
 
+        final long fileSize = entity.getContentLength();
+        call.fileSize(fileSize);
+        call.process(0.0f);
         try(InputStream is = entity.getContent(); FileOutputStream fileout = new FileOutputStream(filepath);) {
-            ByteStreams.copy(is, fileout);
+            byte[] buf = new byte[ 4 * 1024]; //4KB 缓冲区
+            long total = 0;
+            while (true) {
+                int r = is.read(buf);
+                if (r == -1) {
+                    break;
+                }
+                fileout.write(buf, 0, r);
+                total += r;
+                if(total % 2048 == 1024) {
+                    // 每 4M 推送一次进度
+                    call.process(fileSize > 0 ? (float) total / fileSize : 0.0f);
+                }
+            }
             fileout.flush();
         }
+        call.process(1.0f);
         return filepath.getAbsolutePath();
     }
 
@@ -560,9 +608,37 @@ public class RestApiUtils {
 
     /**
      * 关闭 RPC 调用
+     *
      * @throws IOException
      */
     public static void shutdown() throws IOException {
         httpClient.close();
     }
+
+    /**
+     * 下载进度回调
+     */
+    public static interface ProcessCall {
+
+        /**
+         * 下载的文件大小
+         * @param size
+         */
+        default void fileSize(long size) {};
+
+        /**
+         * 进度推送
+         * @param process
+         */
+        public void process(float process);
+    }
+
+    static class NoProcessCall implements ProcessCall {
+
+        @Override
+        public void process(float process) {
+
+        }
+    }
 }
+

+ 87 - 0
java-commons-cache/RedisConfig.java

@@ -0,0 +1,87 @@
+package com.yiidata.intergration.common.cache;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+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.RedisStandaloneConfiguration;
+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.RedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2025/1/21
+ * Time: 15:33
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@Slf4j
+@Configuration
+@ConditionalOnProperty(name = "cache.type", havingValue = "redis", matchIfMissing = false)
+public class RedisConfig {
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory(ICache cache) {
+        Redised redised = (Redised) cache;
+
+        RedisStandaloneConfiguration clusterConfiguration = new RedisStandaloneConfiguration();
+        clusterConfiguration.setHostName(redised.getHost());
+        clusterConfiguration.setPort(redised.getPort());
+        clusterConfiguration.setPassword(redised.getPassword());
+        clusterConfiguration.setDatabase(redised.getDb());
+
+        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(clusterConfiguration);
+        log.info("init redis JedisConnectionFactory...");
+        return jedisConnectionFactory;
+    }
+
+    /**
+     * RedisTemplate配置
+     * @param jedisConnectionFactory
+     * @return
+     */
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory jedisConnectionFactory) {
+        log.info(" --- redis config init --- ");
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jacksonSerializer();
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
+        redisTemplate.setConnectionFactory(jedisConnectionFactory);
+        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
+
+        // key序列化
+        redisTemplate.setKeySerializer(stringSerializer);
+        // value序列化
+        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+        // Hash key序列化
+        redisTemplate.setHashKeySerializer(stringSerializer);
+        // Hash value序列化
+        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+
+    private Jackson2JsonRedisSerializer jacksonSerializer() {
+        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);
+        return jackson2JsonRedisSerializer;
+    }
+
+
+}

+ 99 - 25
java-commons-cache/Redised.java

@@ -1,12 +1,14 @@
-package com.yiidata.intergration.web.modules.sys.cache;
+package com.yiidata.intergration.common.cache;
 
-import com.primeton.damp.cache.serde.JdkSerializer;
-import com.primeton.damp.cache.serde.Serializer;
-import com.primeton.damp.cache.serde.StringSerializer;
+import com.yiidata.intergration.common.cache.serde.JdkSerializer;
+import com.yiidata.intergration.common.cache.serde.Serializer;
+import com.yiidata.intergration.common.cache.serde.StringSerializer;
 import org.apache.commons.lang.StringUtils;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -35,8 +37,22 @@ public class Redised implements ICache<Serializable>, Closeable {
     /**
      * Redis 的 Java Client
      */
-    Jedis jedis;
+    JedisPool redisPool;
 
+    /**
+     * redis db index
+     */
+    private final String host;
+
+    /**
+     * redis db index
+     */
+    private final int port;
+
+    /**
+     * redis db password
+     */
+    private final String password;
 
     /**
      * redis db index
@@ -51,7 +67,7 @@ public class Redised implements ICache<Serializable>, Closeable {
     /**
      * Redis Cache 序列化方式
      */
-    private final Serializer<Serializable> VALUE_SERDE = new JdkSerializer();
+    private Serializer<Serializable> VALUE_SERDE = new JdkSerializer();
 
 
 
@@ -77,11 +93,20 @@ public class Redised implements ICache<Serializable>, Closeable {
 
     public Redised(String host, int port, String password, int db){
         this.db = db;
-        jedis = new Jedis(host, port);
+        this.host = host;
+        this.port = port;
+        this.password = password;
+
+        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
+        poolConfig.setMaxIdle(100);
+        poolConfig.setMinIdle(5);
+
+        //jedis = new Jedis(host, port, 30000, 3000);
         if(StringUtils.isNotBlank(password)) {
-            jedis.auth(password);
+            this.redisPool = new JedisPool(poolConfig, host, port, 60000, password);
+        } else {
+            this.redisPool = new JedisPool(poolConfig, host, port);
         }
-        jedis.select(db);
     }
 
     @Override
@@ -89,14 +114,20 @@ public class Redised implements ICache<Serializable>, Closeable {
         if(value == null) {
             return;
         }
-        jedis.set(KEY_SERDE.serialize(key), VALUE_SERDE.serialize(value));
+        try(Jedis jedis = redisPool.getResource();) {
+            jedis.select(db);
+            jedis.set(KEY_SERDE.serialize(key), VALUE_SERDE.serialize(value));
+        }
     }
 
     @Override
     public void add(String key, int exp, Serializable value) {
         byte[] keybytes = KEY_SERDE.serialize(key);
-        jedis.set(keybytes, VALUE_SERDE.serialize(value));
-        jedis.expire(keybytes, exp);
+        try(Jedis jedis = redisPool.getResource();) {
+            jedis.select(db);
+            jedis.set(keybytes, VALUE_SERDE.serialize(value));
+            jedis.expire(keybytes, exp);
+        }
     }
 
     @Override
@@ -104,11 +135,14 @@ public class Redised implements ICache<Serializable>, Closeable {
         if(key == null) {
             return null;
         }
-        byte[] bytes = jedis.get(KEY_SERDE.serialize(key));
-        if(bytes == null) {
-            return null;
+        try(Jedis jedis = redisPool.getResource();) {
+            jedis.select(db);
+            byte[] bytes = jedis.get(KEY_SERDE.serialize(key));
+            if (bytes == null) {
+                return null;
+            }
+            return VALUE_SERDE.deserialize(bytes);
         }
-        return VALUE_SERDE.deserialize(bytes);
     }
 
 
@@ -123,34 +157,46 @@ public class Redised implements ICache<Serializable>, Closeable {
             return null;
         }
         logger.info("remove cache key: {}", key);
-        return jedis.del(KEY_SERDE.serialize(key));
+        try(Jedis jedis = redisPool.getResource();) {
+            jedis.select(db);
+            return jedis.del(KEY_SERDE.serialize(key));
+        }
     }
 
 
     @Override
     public int removeByPrefix(String prefix) {
-        Set<String> keys = jedis.keys(prefix+"*");
-        if(keys != null && keys.size() > 0) {
-            for (String key : keys) {
-                remove(key);
+        try(Jedis jedis = redisPool.getResource();) {
+            jedis.select(db);
+            Set<String> keys = jedis.keys(prefix + "*");
+            if (keys != null && keys.size() > 0) {
+                for (String key : keys) {
+                    remove(key);
+                }
             }
+            return keys == null ? 0 : keys.size();
         }
-        return keys == null ? 0 : keys.size();
     }
 
     @Override
     public void clear() {
-        jedis.flushDB();
+        try(Jedis jedis = redisPool.getResource();) {
+            jedis.select(db);
+            jedis.flushDB();
+        }
     }
 
     @Override
     public int size() {
-        return jedis.dbSize().intValue();
+        try(Jedis jedis = redisPool.getResource();) {
+            jedis.select(db);
+            return jedis.dbSize().intValue();
+        }
     }
 
     @Override
     public void close() throws IOException {
-        jedis.close();
+        redisPool.close();
     }
 
     @Override
@@ -161,4 +207,32 @@ public class Redised implements ICache<Serializable>, Closeable {
 
         }
     }
+
+    /**
+     * 序列化方式
+     * @param valueSerde
+     */
+    public void setValueSerde(Serializer<Serializable> valueSerde) {
+        this.VALUE_SERDE = valueSerde;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public int getDb() {
+        return db;
+    }
+
+    public JedisPool getRedisPool() {
+        return redisPool;
+    }
 }

+ 17 - 15
serializer/GZipSerializer.java

@@ -1,4 +1,4 @@
-package com.sdyc.ndmp.protobuf.serializer;
+package com.yiidata.proxyserver.common.serializer;
 
 import org.apache.commons.compress.compressors.CompressorException;
 import org.apache.commons.compress.compressors.CompressorInputStream;
@@ -11,15 +11,17 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 
 /**
+ *
+ * GZip 压缩
+ *
  * <pre>
  * Created with IntelliJ IDEA.
- * User: lwj
+ * User: zhzhenqin
  * Date: 2015/2/27
  * Time: 10:48
- * To change this template use File | Settings | File Templates.
  * </pre>
  *
- * @author lwj
+ * @author zhzhenqin
  */
 public class GZipSerializer extends CompressSerializer {
 
@@ -29,13 +31,12 @@ public class GZipSerializer extends CompressSerializer {
 
     @Override
     public byte[] compress(byte[] bytes) {
-        CompressorInputStream inputStream = null;
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
         try {
-            inputStream = new CompressorStreamFactory().createCompressorInputStream(
-                    CompressorStreamFactory.GZIP, new ByteArrayInputStream(bytes));
-            ByteArrayOutputStream out = new ByteArrayOutputStream();
-            IOUtils.copy(inputStream, out);
-            inputStream.close();
+            CompressorOutputStream outputStream = new CompressorStreamFactory().createCompressorOutputStream(
+                    CompressorStreamFactory.GZIP, out);
+            IOUtils.copy(new ByteArrayInputStream(bytes), outputStream);
+            outputStream.close();
             return out.toByteArray();
         } catch (IOException e) {
             throw new IllegalStateException(e);
@@ -48,12 +49,13 @@ public class GZipSerializer extends CompressSerializer {
 
     @Override
     public byte[] uncompress(byte[] bytes) {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        CompressorInputStream inputStream = null;
         try {
-            CompressorOutputStream outputStream = new CompressorStreamFactory().createCompressorOutputStream(
-                    CompressorStreamFactory.GZIP, out);
-            IOUtils.copy(new ByteArrayInputStream(bytes), outputStream);
-            outputStream.close();
+            inputStream = new CompressorStreamFactory().createCompressorInputStream(
+                    CompressorStreamFactory.GZIP, new ByteArrayInputStream(bytes));
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            IOUtils.copy(inputStream, out);
+            inputStream.close();
             return out.toByteArray();
         } catch (IOException e) {
             throw new IllegalStateException(e);

+ 2 - 1
serializer/JSONFixSerializer.java

@@ -22,14 +22,15 @@ import com.google.common.primitives.Ints;
 /**
  * Java Serialization Redis strserializer.
  * Delegates to the default (Java based) strserializer in Spring 3.
+ *
  * <p/>
+ *
  * <pre>
  *
  * Created by IntelliJ IDEA.
  * User: zhenqin
  * Date: 13-11-13
  * Time: 上午8:58
- * To change this template use File | Settings | File Templates.
  *
  * </pre>
  *

+ 121 - 0
serializer/KryoFixSerializer.java

@@ -0,0 +1,121 @@
+package com.yiidata.intergration.common.cache.serde;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import com.esotericsoftware.kryo.util.Pool;
+import com.google.common.primitives.Ints;
+import com.google.gson.Gson;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+
+/**
+ *
+ * Kryo 序列化
+ *
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2022/5/30
+ * Time: 下午4:41
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+public class KryoFixSerializer<T> implements Serializer<T> {
+
+    // Pool constructor arguments: thread safe, soft references, maximum capacity
+    final Pool<Kryo> kryoPool = new Pool<Kryo>(true, false, 8) {
+        protected Kryo create () {
+            Kryo kryo = new Kryo();
+            // Kryo 配置
+            return kryo;
+        }
+    };
+
+
+
+    private final StringSerializer serializer;
+
+    public KryoFixSerializer() {
+        serializer = new StringSerializer();
+    }
+
+
+    public KryoFixSerializer(String charset) {
+        this.serializer = new StringSerializer(Charset.forName(charset));
+    }
+
+
+    public KryoFixSerializer(StringSerializer serializer) {
+        this.serializer = serializer;
+    }
+
+    @Override
+    public byte[] serialize(T o) {
+        // 获取池中的Kryo对象
+        Kryo kryo = kryoPool.obtain();
+        try {
+            String clazz = o.getClass().getName();
+            byte[] classString = clazz.getBytes(serializer.getCharset());
+
+            int length = classString.length;
+            byte[] head = Ints.toByteArray(length);
+
+            // 写入数据缓冲区
+            Output opt = new Output(1024, -1);
+            kryo.writeClassAndObject(opt, o);
+            opt.flush();
+            byte[] body = opt.getBuffer();
+
+            byte[] bytes = new byte[head.length + length + body.length];
+            System.arraycopy(head, 0, bytes, 0, head.length);
+            System.arraycopy(classString, 0, bytes, head.length, classString.length);
+            System.arraycopy(body, 0, bytes, head.length + length, body.length);
+            return bytes;
+        } finally {
+            // 将kryo对象归还到池中
+            kryoPool.free(kryo);
+        }
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) {
+        // 获取池中的Kryo对象
+        Kryo kryo = kryoPool.obtain();
+        try {
+            // 前面 4 个字节是头,代表 类名 的长度
+            byte[] head = new byte[4];
+            System.arraycopy(bytes, 0, head, 0, head.length);
+
+            // 类名称,全名
+            int length = Ints.fromBytes(bytes[0], bytes[1], bytes[2], bytes[3]);
+            String classString = new String(bytes, 4, length);
+
+            // 后面是实际的序列化数据
+            byte[] data = new byte[bytes.length - length - 4];
+            System.arraycopy(bytes, length + 4, data, 0, data.length);
+            return deserialize(kryo, data, (Class<T>) Class.forName(classString));
+        } catch (ClassNotFoundException e) {
+            throw new IllegalStateException(e);
+        } finally {
+            // 将kryo对象归还到池中
+            kryoPool.free(kryo);
+        }
+    }
+
+
+
+    public T deserialize(Kryo kryo, byte[] bytes, Class<T> clazz) {
+        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
+        Input input = new Input(in);
+        return kryo.readObject(input, clazz);
+    }
+}

+ 14 - 15
serializer/ZipSerializer.java

@@ -1,4 +1,4 @@
-package com.sdyc.ndmp.protobuf.serializer;
+package com.yiidata.proxyserver.common.serializer;
 
 import org.apache.commons.compress.compressors.CompressorException;
 import org.apache.commons.compress.compressors.CompressorInputStream;
@@ -13,13 +13,12 @@ import java.io.IOException;
 /**
  * <pre>
  * Created with IntelliJ IDEA.
- * User: lwj
+ * User: zhzhenqin
  * Date: 2015/2/27
  * Time: 10:48
- * To change this template use File | Settings | File Templates.
  * </pre>
  *
- * @author lwj
+ * @author zhzhenqin
  */
 public class ZipSerializer extends CompressSerializer {
 
@@ -29,13 +28,12 @@ public class ZipSerializer extends CompressSerializer {
 
     @Override
     public byte[] compress(byte[] bytes) {
-        CompressorInputStream inputStream = null;
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
         try {
-            inputStream = new CompressorStreamFactory().createCompressorInputStream(
-                    CompressorStreamFactory.BZIP2, new ByteArrayInputStream(bytes));
-            ByteArrayOutputStream out = new ByteArrayOutputStream();
-            IOUtils.copy(inputStream, out);
-            inputStream.close();
+            CompressorOutputStream outputStream = new CompressorStreamFactory().createCompressorOutputStream(
+                    CompressorStreamFactory.BZIP2, out);
+            IOUtils.copy(new ByteArrayInputStream(bytes), outputStream);
+            outputStream.close();
             return out.toByteArray();
         } catch (IOException e) {
             throw new IllegalStateException(e);
@@ -48,12 +46,13 @@ public class ZipSerializer extends CompressSerializer {
 
     @Override
     public byte[] uncompress(byte[] bytes) {
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        CompressorInputStream inputStream = null;
         try {
-            CompressorOutputStream outputStream = new CompressorStreamFactory().createCompressorOutputStream(
-                    CompressorStreamFactory.BZIP2, out);
-            IOUtils.copy(new ByteArrayInputStream(bytes), outputStream);
-            outputStream.close();
+            inputStream = new CompressorStreamFactory().createCompressorInputStream(
+                    CompressorStreamFactory.BZIP2, new ByteArrayInputStream(bytes));
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            IOUtils.copy(inputStream, out);
+            inputStream.close();
             return out.toByteArray();
         } catch (IOException e) {
             throw new IllegalStateException(e);

+ 403 - 0
solr/SolrServerFactory.java

@@ -0,0 +1,403 @@
+/**
+ * 
+ */
+package com.sdyc.ndcls2.solr;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.solr.client.solrj.SolrServer;
+import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.xml.sax.SAXException;
+
+/**
+ * 
+ * <p>
+ * 该类是一个静态工厂方法.属于单利模式,在多线程环境下各个线程安全是安全的.<br><br>
+ * 
+ * Solr的CoreContainer在初始化的时候默认需要solr.solr.home的System.Property信息,
+ * 所以你需要在得到 {@link #getInstance()}前显式的指定一个solr.solr.home.
+ * 或者使用 {@link #getInstance(String)} 初始化,当使用 {@link #getInstance(String)}
+ * 进行初始化时,如果已经在System.getProperties()中加入solr.solr.home,
+ * Factory会优先使用solr.solr.home信息进行初始化,
+ * 并把solrHome覆盖为solr.solr.home的值<br><br>
+ * 
+ * 
+ * 初始化该类时,Solr配置中的所有Core并不会立即发生初始化(defaultCore除外).
+ * 
+ * <br><br>
+ * defaultCore在初始化时有一个复杂的过程,具体有以下几种情况:<br>
+ * 1.当在Solr配置文件中的solr.xml文件中没有配置defaultCoreName节点信息.
+ * 那么在Factory中你有必要指定一个defaultCoreName(并且defaultCoreName必须满足一些规定).
+ * 则默认的defaultCore是以defaultCoreName为Core的SolrServer.<br>
+ * 2.当在Solr配置文件中的solr.xml文件中已经配置了defaultCoreName.
+ * 那么即时你手动的设置Factory的defaultCoreName也是无效的.
+ * 也就是说,defaultCoreName永远会以Solr中的配置为优先的原则.<br><br>
+ * </p>
+ * 
+ * 
+ * <p>
+ * 传入的coreName必须是一个有效的Java 标示符名称.在内部的实现使用了Map字典,
+ * 这个字典的Key是所有Solr Core内有效的coreName,因此你必须保证字典中的所有key满足Java
+ * 标示符的命名规则.在这里传入null, "", OR "  "都是不允许的.
+ * 
+ * </p>
+ * 
+ * 
+ * <p>
+ * 在没有特别指定的情况下,任何需要传入参数的方法,当传入null值时,
+ * 会发生NullPointException异常.
+ * </p>
+ * @author ZhenQin
+ * 
+ * @since 2.0
+ *
+ */
+public final class SolrServerFactory {
+
+	
+	/**
+	 * Solr.home
+	 */
+	private static String solrHome = null;
+	
+	
+	/**
+	 * 默认的defaultCoreName,该参数默认是defaultCoreName = ""
+	 */
+	private static String defaultCoreName = "";
+
+	
+	/**
+	 * 初始化的solrCoreContainer
+	 * 
+	 */
+	private static CoreContainer solrCoreContainer = null;
+	
+	
+	/**
+	 * 默认的defaultSolrServer
+	 * 
+	 */
+	private static SolrServer defaultSolrServer = null;
+	
+	
+	
+	/**
+	 * 
+	 * 保存这的SolrServer,对应多个solrServer的时候,保存的SolrServer
+	 * 
+	 */
+	private final Map<String, SolrServer> solrServerCache = new HashMap<String, SolrServer>();
+	
+	
+	/**
+	 * 锁
+	 */
+	private java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
+	
+	
+	/**
+	 * 日志
+	 */
+	private static Log log = LogFactory.getLog(SolrServerFactory.class);
+	
+	/**
+	 * 
+	 * 静态工厂方法,单例模式.SolrServerFactory只能有一个对象实例
+	 * 
+	 */
+	private static SolrServerFactory instance = null;
+	
+	
+	
+	/**
+	 * 私有化构造方法
+	 */
+	private SolrServerFactory() {
+		initBuild();
+	}
+	
+
+	
+	/**
+	 * 初始化时的Build,该方法只能被初始化的时候执行一次
+	 * 
+	 */
+	private void initBuild() {
+		try {
+			setSolrCoreContainer(new CoreContainer.Initializer().initialize());
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		} catch (ParserConfigurationException e) {
+			throw new RuntimeException(e);
+		} catch (SAXException e) {
+			throw new RuntimeException(e);
+		}
+		
+		String defaultCoreName = SolrServerFactory.getSolrCoreContainer().getDefaultCoreName();
+		if(defaultCoreName == null){
+			if(validate(SolrServerFactory.getDefaultCoreName())){
+				setDefaultSolrServer(new EmbeddedSolrServer(SolrServerFactory.getSolrCoreContainer(), 
+						SolrServerFactory.getDefaultCoreName()));
+			}
+		}
+		if(defaultCoreName.equals("")){
+			setDefaultCoreName(defaultCoreName);
+			setDefaultSolrServer(new EmbeddedSolrServer(SolrServerFactory.getSolrCoreContainer(), 
+					SolrServerFactory.getDefaultCoreName()));
+		} else if(defaultCoreName.trim().length() > 0){
+			setDefaultCoreName(defaultCoreName);
+			setDefaultSolrServer(new EmbeddedSolrServer(SolrServerFactory.getSolrCoreContainer(), 
+					SolrServerFactory.getDefaultCoreName()));
+		}
+			
+		lock.lock();
+		try {
+			if(StringUtils.isNotBlank(getDefaultCoreName())){
+				this.solrServerCache.put(getDefaultCoreName(), getDefaultSolrServer());
+			}
+		} finally {
+			lock.unlock();
+		}
+	}
+	
+	
+	
+	/**
+	 * 
+	 * 通过静态工厂方法获得类的实例
+	 * @throws java.lang.RuntimeException 当没有查找到System.getProperty("solr.solr.home")的参数时,
+	 * 发生该异常.如果需要显式的传入SolrHome,请使用:{@link #getInstance(String)}
+	 * @return 返回SolrServerFactory唯一实例
+	 */
+	public static SolrServerFactory getInstance() {
+		if(instance != null){
+			return instance;
+		}
+		synchronized (SolrServerFactory.class) {
+			if(instance == null) {
+				String solrHome = System.getProperty("solr.solr.home");
+				if(StringUtils.isNotBlank(solrHome)){
+					SolrServerFactory.solrHome = solrHome;
+					instance = new SolrServerFactory();
+				} else if(StringUtils.isNotBlank(SolrServerFactory.solrHome)) {
+					System.setProperty("solr.solr.home", SolrServerFactory.solrHome);
+					instance = new SolrServerFactory();
+				} else {
+					throw new RuntimeException("not fount System.getProperty(\"solr.solr.home\") attr.");
+				}
+			}
+		}
+		return instance;
+	}
+	
+	
+	
+	/**
+	 * 
+	 * 通过静态工厂方法获得类的实例
+	 * 
+	 * @return 返回SolrServerFactory唯一实例
+	 */
+	public static SolrServerFactory getInstance(String solrHome) {
+		if(StringUtils.isNotBlank(solrHome)){
+			SolrServerFactory.solrHome = solrHome;
+			return getInstance();
+		} 
+		throw new RuntimeException("solrHome is an invalid ${solr.solr.home}.");
+	}
+	
+
+
+	/**
+	 * @return
+	 * @see org.apache.solr.core.CoreContainer#getSolrHome()
+	 */
+	public String getSolrHome() {
+		return solrCoreContainer.getSolrHome();
+	}
+
+
+	/**
+	 * 
+	 * @see org.apache.solr.core.CoreContainer#persist()
+	 */
+	public void persist() {
+		solrCoreContainer.persist();
+	}
+
+
+	/**
+	 * @param core
+	 * @param returnPrev
+	 * @return
+	 * @see org.apache.solr.core.CoreContainer#register(org.apache.solr.core.SolrCore, boolean)
+	 */
+	public SolrCore register(SolrCore core, boolean returnPrev) {
+		return solrCoreContainer.register(core, returnPrev);
+	}
+
+
+	/**
+	 * @param name
+	 * @param core
+	 * @param returnPrevNotClosed
+	 * @return
+	 * @see org.apache.solr.core.CoreContainer#register(java.lang.String, org.apache.solr.core.SolrCore, boolean)
+	 */
+	public SolrCore register(String name, SolrCore core,
+			boolean returnPrevNotClosed) {
+		return solrCoreContainer.register(name, core,
+				returnPrevNotClosed);
+	}
+
+
+	/**
+	 * 
+	 * @see org.apache.solr.core.CoreContainer#shutdown()
+	 */
+	public void shutdown() {
+		solrCoreContainer.shutdown();
+	}
+
+
+	/**
+	 * @return the defaultSolrCoreContainer
+	 */
+	public static CoreContainer getSolrCoreContainer(){
+		if(SolrServerFactory.solrCoreContainer == null){
+			SolrServerFactory.getInstance();
+		}
+		return SolrServerFactory.solrCoreContainer;
+	}
+	
+	
+	/**
+	 * 
+	 * 返回一个SolrCore对象,这个对象可以允许使用Lucene原生API进行编程.
+	 * @param coreName Solr的一个coreName,
+	 * 这个coreName需要 {@link #validate(String)}验证通过.否则返回null
+	 * @return the Solr Lucene API
+	 */
+	public SolrCore getSolrCore(String coreName){
+		if(validate(coreName)){
+			return SolrServerFactory.solrCoreContainer.getCore(coreName);
+		}
+		return null;
+	}
+	
+	
+	
+	
+	/**
+	 * 
+	 * @param solrCoreContainer the defaultSolrCoreContainer to set
+	 */
+	protected void setSolrCoreContainer(CoreContainer solrCoreContainer) {
+		SolrServerFactory.solrCoreContainer = solrCoreContainer;
+	}
+
+
+	/**
+	 * <p>
+	 * 取得默认的SolrServer对象.<br><br>
+	 * <b>注意:</b><br>
+	 * 返回的SolrServer有可能是null值,这取决着你是否在Solr的solr.xml
+	 * 是否配置了defaultCoreName信息或者显式的设置Factory的defaultCoreName信息.
+	 * </p>
+	 * @return 返回Solr的SolrServer
+	 */
+	public static SolrServer getDefaultSolrServer(){
+		if(SolrServerFactory.solrCoreContainer == null){
+			SolrServerFactory.getInstance();
+		}
+		return SolrServerFactory.defaultSolrServer;
+	}
+	
+	
+	
+	/**
+	 * 检验一个CoreName是否是Solr核心配置所支持的
+	 * 
+	 * @param coreName Solr core,具体见${solr.solr.home}/solr.xml中的cor.name属性
+	 * 
+	 * @return 返回是否是合法的Solr coreName.
+	 */
+	public static boolean validate(String coreName) {
+		return coreName.equals(defaultCoreName) ? true : SolrServerFactory.getSolrCoreContainer().getCoreNames().contains(coreName);
+	}
+	
+	
+	
+	
+	
+	/**
+	 * <p>
+	 * 根据名称取得一个SolrServer.<br>
+	 * 传入的coreName必须是一个有效的Java 标示符名称.在内部的实现使用了Map字典,
+	 * 这个字典的Key是所有Solr Core内有效的coreName,因此你必须保证字典中的所有key满足Java
+	 * 标示符的命名规则.在这里传入null, "", OR "  "都是不允许的.
+	 * </p>
+	 * 
+	 * <p>
+	 * <b>注意:</b><br>
+	 * SolrServerFactory在初始化的时候并不会自动注册每一个SolrCore(除了defaultSolrCore).
+	 * 具体见:{@link com.sdyc.ndcls2.solr.SolrServerFactory}
+	 * </p>
+	 * 
+	 * @param coreName 这个coreName需要 {@link #validate(String)}验证通过.否则返回null
+	 * @return 返回SolrServer,当coreName不满足必要的条件则返回null
+	 */
+	public SolrServer getSolrServer(String coreName){
+		if(StringUtils.isNotBlank(coreName) && validate(coreName)){
+			SolrServer tmpSolrServer = solrServerCache.get(coreName);
+			if(tmpSolrServer == null){
+				lock.lock();
+				try {
+					tmpSolrServer = new EmbeddedSolrServer(getSolrCoreContainer(), coreName);
+					solrServerCache.put(coreName, tmpSolrServer);
+				} catch(Exception e){
+					log.error(e);
+				} finally {
+					lock.unlock();
+				}
+			} 
+			return tmpSolrServer;
+		}
+		return null;
+	}
+	
+
+	/**
+	 * @param defaultSolrServer the defaultSolrServer to set
+	 */
+	protected void setDefaultSolrServer(SolrServer defaultSolrServer) {
+		SolrServerFactory.defaultSolrServer = defaultSolrServer;
+	}
+
+
+	/**
+	 * @param defaultCoreName the defaultCoreName to set
+	 */
+	public static void setDefaultCoreName(String defaultCoreName) {
+		SolrServerFactory.defaultCoreName = defaultCoreName;
+	}
+
+
+	/**
+	 * @return the defaultCoreName
+	 */
+	public static String getDefaultCoreName() {
+		return defaultCoreName;
+	}
+	
+}

+ 24 - 0
spring-mutil-datasource/annotation/DataSource.java

@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 多数据源注解
+ *
+ * @author zhenqin
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface DataSource {
+    String value() default "";
+}

+ 36 - 0
spring-mutil-datasource/application.yml

@@ -0,0 +1,36 @@
+spring:
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    druid:
+      driver-class-name: com.mysql.cj.jdbc.Driver
+      url: jdbc:mysql://localhost:3306/intg?useSSL=false&useUnicode=true&characterEncoding=UTF-8
+      username: root
+      password: 123456
+      initial-size: 10
+      max-active: 100
+      min-idle: 10
+      max-wait: 60000
+      pool-prepared-statements: true
+      max-pool-prepared-statement-per-connection-size: 20
+      time-between-eviction-runs-millis: 60000
+      min-evictable-idle-time-millis: 300000
+      max-evictable-idle-time-millis: 600000
+      #Oracle需要打开注释
+      validation-query: SELECT 1 FROM DUAL
+      test-while-idle: true
+      test-on-borrow: false
+      test-on-return: false
+      stat-view-servlet:
+        enabled: true
+        url-pattern: /druid/*
+        allow: 127.0.0.1
+        #login-username: admin
+        #login-password: admin
+      filter:
+        stat:
+          log-slow-sql: true
+          slow-sql-millis: 1000
+          merge-sql: false
+        wall:
+          config:
+            multi-statement-allow: true

+ 70 - 0
spring-mutil-datasource/aspect/DataSourceAspect.java

@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.aspect;
+
+
+import com.yiidata.intergration.web.datasource.annotation.DataSource;
+import com.yiidata.intergration.web.datasource.config.DynamicContextHolder;
+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.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 多数据源,切面处理类
+ *
+ * @author zhenqin
+ */
+@Aspect
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
+public class DataSourceAspect {
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Pointcut("@annotation(com.yiidata.intergration.web.datasource.annotation.DataSource) " +
+            "|| @within(com.yiidata.intergration.web.datasource.annotation.DataSource)")
+    public void dataSourcePointCut() {
+
+    }
+
+    @Around("dataSourcePointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Class targetClass = point.getTarget().getClass();
+        Method method = signature.getMethod();
+
+        DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
+        DataSource methodDataSource = method.getAnnotation(DataSource.class);
+        if(targetDataSource != null || methodDataSource != null){
+            String value;
+            if(methodDataSource != null){
+                value = methodDataSource.value();
+            }else {
+                value = targetDataSource.value();
+            }
+
+            DynamicContextHolder.push(value);
+            logger.debug("set datasource is {}", value);
+        }
+
+        try {
+            return point.proceed();
+        } finally {
+            DynamicContextHolder.poll();
+        }
+    }
+}

+ 57 - 0
spring-mutil-datasource/config/DynamicContextHolder.java

@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.config;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * 多数据源上下文
+ *
+ * @author zhenqin
+ */
+public class DynamicContextHolder {
+    @SuppressWarnings("unchecked")
+    private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
+        @Override
+        protected Object initialValue() {
+            return new ArrayDeque();
+        }
+    };
+
+    /**
+     * 获得当前线程数据源
+     *
+     * @return 数据源名称
+     */
+    public static String peek() {
+        return CONTEXT_HOLDER.get().peek();
+    }
+
+    /**
+     * 设置当前线程数据源
+     *
+     * @param dataSource 数据源名称
+     */
+    public static void push(String dataSource) {
+        CONTEXT_HOLDER.get().push(dataSource);
+    }
+
+    /**
+     * 清空当前线程数据源
+     */
+    public static void poll() {
+        Deque<String> deque = CONTEXT_HOLDER.get();
+        deque.poll();
+        if (deque.isEmpty()) {
+            CONTEXT_HOLDER.remove();
+        }
+    }
+
+}

+ 25 - 0
spring-mutil-datasource/config/DynamicDataSource.java

@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.config;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+/**
+ * 多数据源
+ *
+ * @author zhenqin
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource {
+
+    @Override
+    protected Object determineCurrentLookupKey() {
+        return DynamicContextHolder.peek();
+    }
+
+}

+ 65 - 0
spring-mutil-datasource/config/DynamicDataSourceConfig.java

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.yiidata.intergration.web.datasource.properties.DataSourceProperties;
+import com.yiidata.intergration.web.datasource.properties.DynamicDataSourceProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 配置多数据源
+ *
+ * @author zhenqin
+ */
+@Configuration
+@EnableConfigurationProperties(DynamicDataSourceProperties.class)
+public class DynamicDataSourceConfig {
+    @Autowired
+    private DynamicDataSourceProperties properties;
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.druid")
+    public DataSourceProperties dataSourceProperties() {
+        return new DataSourceProperties();
+    }
+
+    @Bean
+    public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
+        DynamicDataSource dynamicDataSource = new DynamicDataSource();
+        //默认数据源
+        DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
+        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
+
+        // 绑定的第三方数据源
+        final Map<Object, Object> dynamicDataSource1 = getDynamicDataSource();
+        dynamicDataSource.setTargetDataSources(dynamicDataSource1);
+        dynamicDataSource1.put("default", defaultDataSource);
+        return dynamicDataSource;
+    }
+
+    private Map<Object, Object> getDynamicDataSource(){
+        Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
+        Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size() + 1);
+        dataSourcePropertiesMap.forEach((name, ds) -> {
+            DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(ds);
+            targetDataSources.put(name, druidDataSource);
+        });
+
+        return targetDataSources;
+    }
+
+}

+ 54 - 0
spring-mutil-datasource/config/DynamicDataSourceFactory.java

@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.yiidata.intergration.web.datasource.properties.DataSourceProperties;
+
+import java.sql.SQLException;
+
+/**
+ * DruidDataSource
+ *
+ * @author zhenqin
+ * @since 1.0.0
+ */
+public class DynamicDataSourceFactory {
+
+    public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
+        DruidDataSource druidDataSource = new DruidDataSource();
+        druidDataSource.setDriverClassName(properties.getDriverClassName());
+        druidDataSource.setUrl(properties.getUrl());
+        druidDataSource.setUsername(properties.getUsername());
+        druidDataSource.setPassword(properties.getPassword());
+
+        druidDataSource.setInitialSize(properties.getInitialSize());
+        druidDataSource.setMaxActive(properties.getMaxActive());
+        druidDataSource.setMinIdle(properties.getMinIdle());
+        druidDataSource.setMaxWait(properties.getMaxWait());
+        druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
+        druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
+        druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
+        druidDataSource.setValidationQuery(properties.getValidationQuery());
+        druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
+        druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
+        druidDataSource.setTestOnReturn(properties.isTestOnReturn());
+        druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
+        druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
+        druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
+
+        try {
+            druidDataSource.setFilters(properties.getFilters());
+            druidDataSource.init();
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+        return druidDataSource;
+    }
+}

+ 202 - 0
spring-mutil-datasource/properties/DataSourceProperties.java

@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.properties;
+
+/**
+ * 多数据源属性
+ *
+ * @author zhenqin
+ * @since 1.0.0
+ */
+public class DataSourceProperties {
+    private String driverClassName;
+    private String url;
+    private String username;
+    private String password;
+
+    /**
+     * Druid默认参数
+     */
+    private int initialSize = 2;
+    private int maxActive = 10;
+    private int minIdle = -1;
+    private long maxWait = 60 * 1000L;
+    private long timeBetweenEvictionRunsMillis = 60 * 1000L;
+    private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
+    private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
+    private String validationQuery = "select 1";
+    private int validationQueryTimeout = -1;
+    private boolean testOnBorrow = false;
+    private boolean testOnReturn = false;
+    private boolean testWhileIdle = true;
+    private boolean poolPreparedStatements = false;
+    private int maxOpenPreparedStatements = -1;
+    private boolean sharePreparedStatements = false;
+    private String filters = "stat,wall";
+
+    public String getDriverClassName() {
+        return driverClassName;
+    }
+
+    public void setDriverClassName(String driverClassName) {
+        this.driverClassName = driverClassName;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public int getInitialSize() {
+        return initialSize;
+    }
+
+    public void setInitialSize(int initialSize) {
+        this.initialSize = initialSize;
+    }
+
+    public int getMaxActive() {
+        return maxActive;
+    }
+
+    public void setMaxActive(int maxActive) {
+        this.maxActive = maxActive;
+    }
+
+    public int getMinIdle() {
+        return minIdle;
+    }
+
+    public void setMinIdle(int minIdle) {
+        this.minIdle = minIdle;
+    }
+
+    public long getMaxWait() {
+        return maxWait;
+    }
+
+    public void setMaxWait(long maxWait) {
+        this.maxWait = maxWait;
+    }
+
+    public long getTimeBetweenEvictionRunsMillis() {
+        return timeBetweenEvictionRunsMillis;
+    }
+
+    public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
+        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
+    }
+
+    public long getMinEvictableIdleTimeMillis() {
+        return minEvictableIdleTimeMillis;
+    }
+
+    public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
+        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
+    }
+
+    public long getMaxEvictableIdleTimeMillis() {
+        return maxEvictableIdleTimeMillis;
+    }
+
+    public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
+        this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
+    }
+
+    public String getValidationQuery() {
+        return validationQuery;
+    }
+
+    public void setValidationQuery(String validationQuery) {
+        this.validationQuery = validationQuery;
+    }
+
+    public int getValidationQueryTimeout() {
+        return validationQueryTimeout;
+    }
+
+    public void setValidationQueryTimeout(int validationQueryTimeout) {
+        this.validationQueryTimeout = validationQueryTimeout;
+    }
+
+    public boolean isTestOnBorrow() {
+        return testOnBorrow;
+    }
+
+    public void setTestOnBorrow(boolean testOnBorrow) {
+        this.testOnBorrow = testOnBorrow;
+    }
+
+    public boolean isTestOnReturn() {
+        return testOnReturn;
+    }
+
+    public void setTestOnReturn(boolean testOnReturn) {
+        this.testOnReturn = testOnReturn;
+    }
+
+    public boolean isTestWhileIdle() {
+        return testWhileIdle;
+    }
+
+    public void setTestWhileIdle(boolean testWhileIdle) {
+        this.testWhileIdle = testWhileIdle;
+    }
+
+    public boolean isPoolPreparedStatements() {
+        return poolPreparedStatements;
+    }
+
+    public void setPoolPreparedStatements(boolean poolPreparedStatements) {
+        this.poolPreparedStatements = poolPreparedStatements;
+    }
+
+    public int getMaxOpenPreparedStatements() {
+        return maxOpenPreparedStatements;
+    }
+
+    public void setMaxOpenPreparedStatements(int maxOpenPreparedStatements) {
+        this.maxOpenPreparedStatements = maxOpenPreparedStatements;
+    }
+
+    public boolean isSharePreparedStatements() {
+        return sharePreparedStatements;
+    }
+
+    public void setSharePreparedStatements(boolean sharePreparedStatements) {
+        this.sharePreparedStatements = sharePreparedStatements;
+    }
+
+    public String getFilters() {
+        return filters;
+    }
+
+    public void setFilters(String filters) {
+        this.filters = filters;
+    }
+}

+ 33 - 0
spring-mutil-datasource/properties/DynamicDataSourceProperties.java

@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2018 yiidata.com All rights reserved.
+ *
+ * http://yiidata.com
+ *
+ *
+ */
+
+package com.yiidata.intergration.web.datasource.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 多数据源属性
+ *
+ * @author zhenqin
+ * @since 1.0.0
+ */
+@ConfigurationProperties(prefix = "dynamic")
+public class DynamicDataSourceProperties {
+    private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>();
+
+    public Map<String, DataSourceProperties> getDatasource() {
+        return datasource;
+    }
+
+    public void setDatasource(Map<String, DataSourceProperties> datasource) {
+        this.datasource = datasource;
+    }
+}

+ 80 - 0
spring-shedlock/shedlock.md

@@ -0,0 +1,80 @@
+## @SchedulerLock注解
+
+@SchedulerLock注解支持的五个参数配置:
+
+name 用来标注一个定时服务的名字,被用于写入数据库作为区分不同服务的标识,如果有多个同名定时任务则同一时间点只有一个执行成功
+lockAtMostFor 成功执行任务的节点所能拥有独占锁的最长时间,单位是毫秒ms
+lockAtMostForString 成功执行任务的节点所能拥有的独占锁的最长时间的字符串表达,例如“PT5M”表示为5分钟
+lockAtLeastFor 成功执行任务的节点所能拥有独占锁的最短时间,单位是毫秒ms
+lockAtLeastForString 成功执行任务的节点所能拥有的独占锁的最短时间的字符串表达,例如“PT5M”表示为5分钟
+
+## 实际使用案例
+
+(1) pom依赖文件:
+
+```xml
+<dependency>    
+    <groupId>net.javacrumbs.shedlock</groupId>
+    <artifactId>shedlock-spring</artifactId>
+    <version>2.5.0</version>
+</dependency>
+ 
+<dependency>
+    <groupId>net.javacrumbs.shedlock</groupId>
+    <artifactId>shedlock-provider-jdbc-template</artifactId>
+    <version>2.5.0</version>
+</dependency>
+```
+
+(2) 接下来开始写配置类:
+
+实现配置类,以提供LockProvider
+
+```java
+@EnableSchedulerLock(defaultLockAtMostFor = "PT5M")
+@Configuration
+@EnableScheduling
+public class ShedlockConfig {
+ 
+    @Bean
+    public LockProvider lockProvider(DataSource dataSource) {
+        return new JdbcTemplateLockProvider(dataSource);
+    }
+ 
+    @Bean
+    public ScheduledLockConfiguration scheduledLockConfiguration(LockProvider lockProvider) {
+        return ScheduledLockConfigurationBuilder
+                .withLockProvider(lockProvider)
+                .withPoolSize(10)
+                .withDefaultLockAtMostFor(Duration.ofMinutes(10))
+                .build();
+    }
+}
+```
+
+ (3) 在数据库里加上创建提供锁的外部存储表(shedlock):
+
+```sql
+CREATE TABLE shedlock(
+    name VARCHAR(64), 
+    lock_until TIMESTAMP(3) NULL, 
+    locked_at TIMESTAMP(3) NULL, 
+    locked_by  VARCHAR(255), 
+    PRIMARY KEY (name)
+) 
+```
+
+(4) 实际使用:
+
+```java
+@Component
+public class FileScheduledTask {
+    
+    //每30秒执行一次
+    @Scheduled(cron = "0/30 * * * * ? ")
+    @SchedulerLock(name = "test", lockAtMostForString = "PT40S", lockAtLeastForString = "PT40S")
+    public void test() {
+        System.out.println("定时任务");
+    }
+}
+```

+ 61 - 25
spring-zuul/ApiGatewayConfig.java

@@ -1,7 +1,25 @@
-package com.yiidata.dataops.server.config;
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
 
-import com.yiidata.dataops.server.modules.gateway.entity.OpsGatewayServer;
-import com.yiidata.dataops.server.modules.gateway.service.OpsGatewayServerService;
+package com.datasophon.api.configuration;
+
+import com.google.common.collect.Sets;
+import lombok.Getter;
+import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang.StringUtils;
 import org.springframework.beans.factory.InitializingBean;
@@ -11,32 +29,39 @@ import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.core.annotation.Order;
 
-import java.util.List;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
-import java.util.stream.Collectors;
+import java.util.Set;
 
 /**
+ *
+ * Http 代理, WebSocket 代理
+ *
  * <pre>
  *
  * Created by zhenqin.
  * User: zhenqin
  * Date: 2022/4/25
  * Time: 下午7:08
- * Vendor: yiidata.com
  *
  * </pre>
  *
  * @author zhenqin
  */
 @Slf4j
+@Setter
+@Getter
 @Configuration
 public class ApiGatewayConfig implements InitializingBean {
 
 
     @Autowired
-    OpsGatewayServerService opsGatewayServerService;
+    ApiRouteProperties apiRouteProperties;
+
 
     @Autowired
     ServerProperties serverProperties;
@@ -48,47 +73,58 @@ public class ApiGatewayConfig implements InitializingBean {
 
     @Override
     public void afterPropertiesSet() throws Exception {
-        final List<OpsGatewayServer> gatewayServers = opsGatewayServerService.list();
-        final Map<String, List<OpsGatewayServer>> listMap = gatewayServers.stream()
-                .collect(Collectors.groupingBy(OpsGatewayServer::getApiRouter));
-
-        for (Map.Entry<String, List<OpsGatewayServer>> entry : listMap.entrySet()) {
-            if(entry.getValue() == null || entry.getValue().isEmpty()) {
+        final Map<String, ApiRoute> routers = apiRouteProperties.getRouters();
+        final Set<String> ignoreUrlSet = new HashSet<>();  // 排除的 URL
+        for (Map.Entry<String, ApiRoute> entry : routers.entrySet()) {
+            if(entry.getValue() == null || StringUtils.isBlank(entry.getValue().getUrl())) {
                 continue;
             }
 
             // 代理地址
-            String url = entry.getValue().stream()
-                    .map(OpsGatewayServer::getApiUrl)
-                    .map(StringUtils::trimToNull)
-                    .filter(StringUtils::isNotBlank)
-                    .collect(Collectors.joining(","));
+            final String url = entry.getValue().getUrl();
+            final String api = entry.getValue().getApi();
 
             // 代理路由
-            String path = Optional.ofNullable(StringUtils.trimToNull(entry.getValue().get(0).getApiPath())).orElse("/" + entry.getKey() + "/**");
+            String path = Optional.ofNullable(StringUtils.trimToNull(api)).orElse("/" + entry.getKey() + "/**");
             final ZuulProperties.ZuulRoute route = new ZuulProperties.ZuulRoute();
             route.setId(entry.getKey());
             route.setPath(path);
             route.setUrl(url);
 
-            log.info("add router {} to path: {}", path, url);
-            zuulProperties.getRoutes().putIfAbsent(entry.getKey(), route);
+            // WebSocket 的代理需要特殊处理
+            if ("WS".equalsIgnoreCase(entry.getValue().getType())) {
+                // websocket 是绝对路径,不需要做正则处理
+                route.setPath(api);
+                route.setUrl(url);
+                log.info("add websocket router {}{} to path: {}", serverProperties.getServlet().getContextPath(), api, url);
+                WebSocketConfig.addWebSocketRouter(api, entry.getValue());
+                ignoreUrlSet.add(api);
+            } else {
+                // 返回拼接的地址
+                String tmpUrl = apiRouteProperties.getServletPath().endsWith("/") ?
+                        apiRouteProperties.getServletPath().substring(0, apiRouteProperties.getServletPath().length() - 1) + path
+                        :
+                        apiRouteProperties.getServletPath() + path;
+                log.info("add router {}{} to path: {}", serverProperties.getServlet().getContextPath(), tmpUrl, url);
+                zuulProperties.getRoutes().putIfAbsent(entry.getKey(), route);
+            }
         }
 
+        // 需要排除的 URL, 因为 WebSocket 连接地址 /api/live/ws 是包含在:/api/**, 因此需要排除
+        zuulProperties.setIgnoredPatterns(Sets.newHashSet(ignoreUrlSet));
         // 初始化
         zuulProperties.init();
     }
 
 
-
     /**
      * 支持简单的负载均衡
      * @return
      */
     @Bean
     public SimpleRouteLocator simpleRouteLocator() {
-        return new ApiRouteLocator(this.serverProperties.getServlet().getContextPath(),
-                this.zuulProperties);
+        final ApiRouteLocator apiRouteLocator = new ApiRouteLocator(apiRouteProperties.getServletPath(), this.zuulProperties);
+        apiRouteLocator.getIgnoredPaths();
+        return apiRouteLocator;
     }
-
 }

+ 31 - 0
spring-zuul/README.md

@@ -0,0 +1,31 @@
+POM Import
+
+```xml
+<dependency>
+    <groupId>org.springframework.cloud</groupId>
+    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
+    <version>2.1.6.RELEASE</version>
+</dependency>
+```
+
+配置
+
+```yml
+datasophon:
+  routers:
+    d:
+      api: /d/**
+      url: http://192.168.1.10:3000/d
+    api:
+      api: /api/**
+      url: http://192.168.1.10:3000/api
+    public:
+      api: /public/**
+      url: http://192.168.1.10:3000/public
+```
+
+在  SpringBoot Main 中加上启用注解
+
+```java
+@EnableZuulProxy
+```

+ 129 - 0
spring-zuul/WebSocketProxyFilter.java

@@ -0,0 +1,129 @@
+package com.datasophon.api.configuration;
+
+import com.netflix.zuul.ZuulFilter;
+import com.netflix.zuul.context.RequestContext;
+import com.netflix.zuul.exception.ZuulException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *
+ * Zuul 支持 websocket 的代理
+ *
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2023/3/17
+ * Time: 上午11:32
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@Component
+@Slf4j
+public class WebSocketProxyFilter extends ZuulFilter {
+
+
+    /**
+     * 所有 websocket 的特殊处理
+     */
+    final static Map<String, ApiRoute> WEBSOCKET_ACTION_MAPPING = new ConcurrentHashMap<>();
+
+
+    /**
+     * 添加 WebSocket 地址
+     * @param path
+     * @param route
+     */
+    public static void addWebSocketRouter(String path, ApiRoute route) {
+        WEBSOCKET_ACTION_MAPPING.put(path, route);
+    }
+
+    /**
+     * 过滤器类型
+     * pre 前置
+     * routing
+     * post 后置
+     * error
+     *
+     * @return
+     */
+    @Override
+    public String filterType() {
+        return "pre";
+    }
+
+
+    /**
+     * 执行顺序
+     * 数值越小,优先级越高
+     *
+     * @return
+     */
+    @Override
+    public int filterOrder() {
+        return 0;
+    }
+
+
+    /**
+     * 执行条件
+     * true 开启, 执行 run 的特殊处理
+     * false 关闭,否则不需要特殊处理,直接代理
+     *
+     * @return
+     */
+    @Override
+    public boolean shouldFilter() {
+        RequestContext rc = RequestContext.getCurrentContext();
+        HttpServletRequest request = rc.getRequest();
+        if(request == null) {
+            return false;
+        }
+        final String path = request.getServletPath();
+        final String connectionHeader = request.getHeader("Connection");
+        final String upgradeHeader = request.getHeader("Upgrade");
+        // 检验是不是 websocket 的链接, websocket 连接 Connection: Upgrade, Upgrade: websocket, Websocket, WebSocket
+        return StringUtils.equals("Upgrade", connectionHeader) && StringUtils.equalsIgnoreCase("websocket", upgradeHeader) && WEBSOCKET_ACTION_MAPPING.get(path) != null;
+    }
+
+
+    /**
+     * 动作(具体操作)
+     * 具体逻辑
+     *
+     * @return
+     * @throws ZuulException
+     */
+    @Override
+    public Object run() throws ZuulException {
+        RequestContext rc = RequestContext.getCurrentContext();
+        HttpServletRequest request = rc.getRequest();
+        final String path = request.getServletPath();
+        rc.addZuulRequestHeader("Connection", request.getHeader("Connection"));
+        rc.addZuulRequestHeader("Upgrade", request.getHeader("Upgrade"));
+        //过滤该请求,不进行路由
+        rc.setSendZuulResponse(false);
+        //返回对应http status状态码
+        rc.setResponseStatusCode(401);
+        //设置返回的内容
+        rc.setResponseBody("{'error':'token is empty'}");
+        final ApiRoute apiRoute = WEBSOCKET_ACTION_MAPPING.get(path);
+        log.info(path);
+        try {
+            Thread.sleep(10000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        return apiRoute;
+    }
+}

+ 187 - 0
springsecurity-shiro-cors/SecurityConfig.java

@@ -0,0 +1,187 @@
+package com.hydropower.framework.config;
+
+import com.google.common.collect.Lists;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.security.web.header.Header;
+import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter;
+import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
+import org.springframework.security.web.header.writers.StaticHeadersWriter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import com.hydropower.framework.config.properties.PermitAllUrlProperties;
+import com.hydropower.framework.security.filter.JwtAuthenticationTokenFilter;
+import com.hydropower.framework.security.handle.AuthenticationEntryPointImpl;
+import com.hydropower.framework.security.handle.LogoutSuccessHandlerImpl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * spring security配置
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+    /**
+     * 自定义用户认证逻辑
+     */
+    @Autowired
+    private UserDetailsService userDetailsService;
+
+    /**
+     * 认证失败处理类
+     */
+    @Autowired
+    private AuthenticationEntryPointImpl unauthorizedHandler;
+
+    /**
+     * 退出处理类
+     */
+    @Autowired
+    private LogoutSuccessHandlerImpl logoutSuccessHandler;
+
+    /**
+     * token认证过滤器
+     */
+    @Autowired
+    private JwtAuthenticationTokenFilter authenticationTokenFilter;
+
+    /**
+     * 允许匿名访问的地址
+     */
+    @Autowired
+    private PermitAllUrlProperties permitAllUrl;
+
+    /**
+     * 解决 无法直接注入 AuthenticationManager
+     *
+     * @return
+     * @throws Exception
+     */
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception
+    {
+        return super.authenticationManagerBean();
+    }
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity httpSecurity) throws Exception
+    {
+        // 注解标记允许匿名访问的url
+        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
+        permitAllUrl.getUrls().forEach(url -> {
+            registry.antMatchers(url).permitAll();
+            log.info("add ignore auth url: {}", url);
+        });
+
+        httpSecurity
+                // CSRF禁用,因为不使用session
+                .csrf().disable()
+                // 认证失败处理类
+                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
+                // 基于token,所以不需要session
+                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+                // 过滤请求
+                .authorizeRequests()
+                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
+                .antMatchers("/login", "/register", "/captchaImage","/websocket/**","/rabbitmq/**","/api/**").permitAll()
+                // 静态资源,可匿名访问
+                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
+                .antMatchers("/swagger-ui.html", "/favicon.ico", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**", "/static/**").permitAll()
+                // 除上面外的所有请求全部需要鉴权认证
+                .anyRequest().authenticated()
+                .and()
+                .cors().configurationSource(corsConfigurationSource())
+                .and()
+                // 禁用HTTP响应标头
+                .headers().cacheControl().disable().and()
+                .headers(headers -> {
+
+                    headers.referrerPolicy().policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN);
+                    headers.frameOptions().disable();
+
+                    final List<@Nullable Header> headerList = Lists.newArrayList();
+                    headerList.add(new Header("Access-Control-Allow-Origin", "*"));
+                    headerList.add(new Header("Access-Control-Allow-Methods", "*"));
+                    headerList.add(new Header("Access-Control-Max-Age", "3600"));
+                    headers.addHeaderWriter(new StaticHeadersWriter(headerList));
+
+
+                });
+        // 添加Logout filter
+        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
+        // 添加JWT filter
+        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+        // 添加CORS filter
+        //httpSecurity.addFilterBefore(authenticationTokenFilter, LogoutFilter.class);
+        //httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+    }
+
+    //配置跨域访问资源
+    private CorsConfigurationSource corsConfigurationSource() {
+        CorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        corsConfiguration.addAllowedOriginPattern("*");
+        corsConfiguration.addAllowedOrigin("*"); // 同源配置,*表示任何请求都视为同源,若需指定ip和端口可以改为如“localhost:8080”,多个以“,”分隔;
+        corsConfiguration.addAllowedHeader("*");// header,允许哪些header,本案中使用的是token,此处可将*替换为token;
+        corsConfiguration.addAllowedMethod("*"); // 允许的请求方法,PSOT、GET等
+        ((UrlBasedCorsConfigurationSource) source).registerCorsConfiguration("/**", corsConfiguration); // 配置允许跨域访问的url
+        return source;
+    }
+
+    /**
+     * 强散列哈希加密实现
+     */
+    @Bean
+    public BCryptPasswordEncoder bCryptPasswordEncoder()
+    {
+        return new BCryptPasswordEncoder();
+    }
+
+    /**
+     * 身份认证接口
+     */
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception
+    {
+        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
+    }
+}

+ 48 - 0
websocket/MyTextWebSocketHandler.java

@@ -0,0 +1,48 @@
+package com.yiidata.dataops.apiserver.servlet;
+
+import lombok.extern.slf4j.Slf4j;
+import org.joda.time.DateTime;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+/**
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2023/3/17
+ * Time: 下午3:31
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@Slf4j
+public class MyTextWebSocketHandler extends TextWebSocketHandler {
+
+    /**
+     * WebSocket 目标点
+     */
+    String endPoint;
+
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+        log.info("连接成功。。。" + session.getUri());
+    }
+
+    @Override
+    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+        //super.handleTextMessage(session, message);
+        log.info("【websocket消息】收到客户端消息:" + message);
+        String result = "【websocket消息】【" + DateTime.now().toString("yyyy-MM-dd HH:mm:ss") + "】收到客户端消息: " + message.getPayload();
+        session.sendMessage(new TextMessage(result));
+    }
+
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+        log.info("断开连接。。。" + session.getUri());
+    }
+}

+ 278 - 0
websocket/ProxyWebSocketHandler.java

@@ -0,0 +1,278 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package cn.exlive.video.handler;
+
+import com.google.common.net.HttpHeaders;
+import org.apache.commons.lang.StringUtils;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+import org.springframework.web.socket.BinaryMessage;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.AbstractWebSocketHandler;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+
+
+/**
+ *
+ * WebSocket 代理的核心类,将 Client 的 WS 请求,转发到后台的 WebSocket 服务器,并把服务器的响应返回给 Client
+ *
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2023/3/17
+ * Time: 下午3:31
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@Slf4j
+public class ProxyWebSocketHandler extends AbstractWebSocketHandler {
+
+    /**
+     * WebSocket Proxy 需要移除的 Header
+     */
+    final static Set<String> WEBSOCKET_EXCLUDE_HEADER_NAME =
+            ImmutableSet.of("sec-websocket-version", "sec-websocket-extensions");
+
+    /**
+     * 远端 WebSocket 目标点
+     */
+    final String endPoint;
+
+    /**
+     * 代理远端的 websocket
+     */
+    MsgWebSocketClient webSocketClient;
+
+    public ProxyWebSocketHandler(String endPoint) {
+        this.endPoint = endPoint;
+    }
+
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+        final org.springframework.http.HttpHeaders handshakeHeaders = session.getHandshakeHeaders();
+        final Map<String, String> headers = new HashMap<>();
+        copyRequestHeaders(handshakeHeaders, headers);
+        try {
+            addProxyHeaders(handshakeHeaders, headers, session.getRemoteAddress().getHostName());
+        } catch (Exception ignore) {
+        }
+        try {
+            // 打开远端 websocket
+            this.webSocketClient = new MsgWebSocketClient(endPoint, session, headers);
+            this.webSocketClient.connect();
+            log.info("连接成功。。。" + endPoint);
+        } catch (Exception e) {
+            log.error(endPoint + " 连接异常。", e);
+            // 远端连接失败,则立即关闭
+            // afterConnectionClosed(session, CloseStatus.SERVER_ERROR);
+        }
+    }
+
+    @Override
+    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+        // 将客户端发送消息,发送到 远端 websocket
+        if (!this.webSocketClient.isOpen()) {
+            try {
+                afterConnectionEstablished(session);
+            } catch (Exception ignore) { }
+        }
+
+        if(this.webSocketClient.isOpen()) {
+            this.webSocketClient.send(message.getPayload());
+        }
+    }
+
+    @Override
+    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
+        // 将客户端发送消息,发送到 远端 websocket
+        if (!this.webSocketClient.isOpen()) {
+            try {
+                afterConnectionEstablished(session);
+            } catch (Exception ignore) { }
+        }
+
+        if(this.webSocketClient.isOpen()) {
+            this.webSocketClient.send(message.getPayload());
+        }
+    }
+
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+        log.info("断开连接。。。" + endPoint);
+        // 关闭远端 websocket
+        if (this.webSocketClient != null) {
+            this.webSocketClient.close();
+        }
+    }
+
+    /**
+     * 请求的 Header
+     * @param httpHeaders
+     * @param requestHeader
+     */
+    protected void copyRequestHeaders(org.springframework.http.HttpHeaders httpHeaders, Map<String, String> requestHeader) {
+        for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
+            String headerName = entry.getKey();
+            String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
+            // Remove hop-by-hop headers.
+            if (WEBSOCKET_EXCLUDE_HEADER_NAME.contains(lowerHeaderName)) {
+                continue;
+            }
+
+            final List<String> value = entry.getValue();
+            if (value != null) {
+                requestHeader.put(headerName, Joiner.on(", ").join(value));
+            }
+        }
+    }
+
+    /**
+     * 代理的相关配置
+     * @param httpHeaders
+     * @param requestHeader
+     */
+    protected void addProxyHeaders(org.springframework.http.HttpHeaders httpHeaders, Map<String, String> requestHeader, String remoteHostName) {
+        try {
+            requestHeader.put(HttpHeaders.VIA, "http/1.1 " + InetAddress.getLocalHost().getHostName());
+            requestHeader.put(HttpHeaders.X_FORWARDED_HOST, InetAddress.getLocalHost().getHostName());
+        } catch (Exception ignore) {
+        }
+        String xForwardFor = getHeader(httpHeaders, "X-Forwarded-For");
+        if (StringUtils.isBlank(xForwardFor)) {
+            // xForwardFor, 第一层代理
+            requestHeader.put(HttpHeaders.X_FORWARDED_FOR, remoteHostName);
+        } else {
+            // xForwardFor,多层代理,将外层 IP全部 copy
+            requestHeader.put(HttpHeaders.X_FORWARDED_FOR, xForwardFor + ", " + remoteHostName);
+        }
+        requestHeader.put(HttpHeaders.X_FORWARDED_HOST, getHeader(httpHeaders, HttpHeaders.HOST));
+    }
+
+    /**
+     * 返回 Header
+     * @param httpHeaders
+     * @param headerName
+     * @return
+     */
+    protected String getHeader(org.springframework.http.HttpHeaders httpHeaders, String headerName) {
+        final List<String> valuesAsList = httpHeaders.getValuesAsList(headerName);
+        return valuesAsList.size() > 0 ? Joiner.on(", ").join(valuesAsList) : "";
+    }
+
+    static class MsgWebSocketClient extends WebSocketClient {
+
+        /**
+         * client ref
+         */
+        final WebSocketSession session;
+
+        /**
+         * 发起请求的 Header
+         */
+        final Map<String, String> httpHeaders;
+
+        /**
+         * 远端服务器返回的 Header
+         */
+        final Map<String, String> responseHeaders = new HashMap<>();
+
+        public MsgWebSocketClient(String url, WebSocketSession session,
+                                  Map<String, String> httpHeaders) throws URISyntaxException {
+            super(new URI(url), httpHeaders); // 以 client 的 Header 访问 remote,否则部分有认证的,无法通过认证
+            this.httpHeaders = httpHeaders;
+            log.info("======= WebSocket Request Headers =======");
+            for (Map.Entry<String, String> entry : httpHeaders.entrySet()) {
+                log.info(entry.getKey() + ": " + entry.getValue());
+            }
+            log.info("========================================");
+            this.setConnectionLostTimeout(30000);
+            this.session = session;
+        }
+
+        @Override
+        public void onOpen(ServerHandshake shake) {
+            log.info("远端 {} 握手成功...", getURI());
+            log.info("====== WebSocket Response Headers ======");
+            for (Iterator<String> it = shake.iterateHttpFields(); it.hasNext();) {
+                String key = it.next();
+                responseHeaders.put(key, shake.getFieldValue(key));
+                log.info(key + ": " + shake.getFieldValue(key));
+            }
+            log.info("========================================");
+        }
+
+        @Override
+        public void onMessage(String paramString) {
+            log.info("receive message: {} remote: {}", paramString, getURI());
+            // String result = "【websocket消息】【" + DateTime.now().toString("yyyy-MM-dd HH:mm:ss") + "】收到客户端消息: " +
+            // paramString;
+            try {
+                session.sendMessage(new TextMessage(paramString));
+            } catch (Exception e) {
+                log.error("WS发送消息异常。", e);
+            }
+        }
+
+        @Override
+        public void onMessage(ByteBuffer bytes) {
+            log.info("receive binary message length: {} remote: {}", bytes.position(), getURI());
+            try {
+                session.sendMessage(new BinaryMessage(bytes));
+            } catch (Exception e) {
+                log.error("WS发送消息异常。", e);
+            }
+        }
+
+        @Override
+        public void onClose(int paramInt, String paramString, boolean paramBoolean) {
+            log.info("close remote, reason: {} .", paramString);
+            if (session != null) {
+                try {
+                    session.close(CloseStatus.SESSION_NOT_RELIABLE);
+                } catch (Exception e) {
+                }
+            }
+        }
+
+        @Override
+        public void onError(Exception e) {
+            log.error("WS:" + getURI() + " 异常。", e);
+        }
+    }
+}

+ 18 - 0
websocket/README.md

@@ -0,0 +1,18 @@
+> WebSocekt 实现
+
+POM Def
+
+```xml
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-websocket</artifactId>
+    <version>${spring.boot.version}</version>
+</dependency>
+
+<dependency>
+    <groupId>org.java-websocket</groupId>
+    <artifactId>Java-WebSocket</artifactId>
+    <version>1.5.3</version>
+</dependency>
+```
+

+ 76 - 0
websocket/WebSocketClientTest.java

@@ -0,0 +1,76 @@
+package ws;
+
+
+import org.java_websocket.WebSocket;
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.enums.ReadyState;
+import org.java_websocket.handshake.ServerHandshake;
+import org.joda.time.DateTime;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Iterator;
+
+/**
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2023/3/17
+ * Time: 下午4:19
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+public class WebSocketClientTest {
+
+
+    public static void main(String[] args) throws Exception {
+        MsgWebSocketClient client = new MsgWebSocketClient("ws://localhost:9696/dataopsapi/websocket/1");
+        client.connect();
+        while (client.getReadyState() != ReadyState.OPEN) {
+            System.out.println("还没有打开");
+            Thread.sleep(200);
+        }
+        System.out.println("建立websocket连接");
+        client.send("asd: " + DateTime.now().toString("yyyy-MM-dd HH:mm:ss"));
+
+        Thread.sleep(5000);
+        client.close();
+    }
+
+    static class MsgWebSocketClient extends WebSocketClient {
+
+        public MsgWebSocketClient(String url) throws URISyntaxException {
+            super(new URI(url));
+        }
+
+        @Override
+        public void onOpen(ServerHandshake shake) {
+            System.out.println("握手...");
+            for (Iterator<String> it = shake.iterateHttpFields(); it.hasNext(); ) {
+                String key = it.next();
+                System.out.println(key + ":" + shake.getFieldValue(key));
+            }
+        }
+
+        @Override
+        public void onMessage(String paramString) {
+            System.out.println("接收到消息:" + paramString);
+        }
+
+        @Override
+        public void onClose(int paramInt, String paramString, boolean paramBoolean) {
+            System.out.println("关闭..." + paramString);
+        }
+
+        @Override
+        public void onError(Exception e) {
+            e.printStackTrace();
+            System.out.println("异常" + e);
+
+        }
+    }
+}

+ 64 - 0
websocket/WebSocketConfig.java

@@ -0,0 +1,64 @@
+package cn.exlive.video.config;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import cn.exlive.video.handler.ProxyWebSocketHandler;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+/**
+ * 开启WebSocket支持,支持 WebSocket 路由代理配置类
+ *
+ * @author cxf
+ */
+@Slf4j
+@Setter
+@Getter
+@EnableWebSocket
+@Configuration
+public class WebSocketConfig {
+
+    /**
+     * 所有 websocket 的特殊处理
+     */
+    final static Map<String, ApiRoute> WEBSOCKET_ACTION_MAPPING = new ConcurrentHashMap<>();
+
+    /**
+     * 添加 WebSocket 地址
+     * @param path
+     * @param route
+     */
+    public static void addWebSocketRouter(String path, ApiRoute route) {
+        WEBSOCKET_ACTION_MAPPING.put(path, route);
+    }
+
+    /**
+     * 	注入ServerEndpointExporter,
+     * 	这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
+     */
+    // @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+
+    @Bean
+    @DependsOn("simpleRouteLocator")
+    public WebSocketConfigurer webSocketConfigurer() {
+        return (WebSocketHandlerRegistry registry) -> {
+            for (Map.Entry<String, ApiRoute> entry : WEBSOCKET_ACTION_MAPPING.entrySet()) {
+                registry.addHandler(new ProxyWebSocketHandler(entry.getValue().getUrl()), entry.getValue().getApi()) // 设置连接路径和处理
+                        .setAllowedOrigins("*");
+            }
+        };
+    }
+}