Преглед изворни кода

增加 excel 导入导出,增加 zuul 路由

zhzhenqin пре 9 месеци
родитељ
комит
eceac3819e

+ 305 - 0
java-excel/ExcelImportFactory.java

@@ -0,0 +1,305 @@
+package cn.exlive.exbooter.excel;
+
+import cn.hutool.core.util.ReflectUtil;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.read.builder.ExcelReaderSheetBuilder;
+import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.opencsv.CSVReader;
+import com.opencsv.CSVReaderBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.FastDateFormat;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2025/2/13
+ * Time: 09:17
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@Slf4j
+public abstract class ExcelImportFactory<T> {
+
+    /**
+     * 返回表头
+     * @return
+     */
+    public abstract Map<Integer, String> getHeaderMap();
+
+    /**
+     * 返回数据
+     *
+     * @param processCallback 进度回调. 返回 0-100 之间的进度条
+     * @return
+     */
+    public abstract List<T> getData(ProcessCallback processCallback);
+
+
+    /**
+     * 获取 实例
+     * @param excelFile excel or csv 文件
+     * @param headerFieldMapping 文件头对应的映射
+     * @return
+     */
+    public static <T> ExcelImportFactory<T> newInstance(File excelFile,
+                                                        Map<String, String> headerFieldMapping,
+                                                        Class<T> clazz) {
+        try (InputStream in = new FileInputStream(excelFile);) {
+            if (excelFile.getName().endsWith(".csv")) {
+                return new CsvFactory(in, headerFieldMapping);
+            }
+            return newInstance(in, headerFieldMapping, clazz);
+        } catch (IOException e) {
+            throw new IllegalStateException("文件无法读取", e);
+        }
+    }
+
+    /**
+     * 获取 实例
+     * @param in excel 文件
+     * @param headerFieldMapping 文件头对应的映射
+     * @return
+     */
+    public static <T> ExcelImportFactory<T> newInstance(InputStream in,
+                                                        Map<String, String> headerFieldMapping,
+                                                        Class<T> clazz) {
+        ExcelFactory factory = new ExcelFactory(in, headerFieldMapping, clazz);
+        return factory;
+    }
+
+    /**
+     * Excel 数据导入
+     */
+    static class ExcelFactory<T> extends ExcelImportFactory<T> {
+
+        /**
+         * 转换的目标 类型
+         */
+        final Class<T> clazz;
+
+        /**
+         * 表头
+         */
+        final Map<String, String> headerAndFieldMapping;
+
+        /**
+         * 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
+         */
+        final OnceReadListener readListener;
+
+        public ExcelFactory(InputStream in, Map<String, String> headerFieldMapping, Class<T> clazz) {
+            this.clazz = clazz;
+            this.readListener = new OnceReadListener(headerFieldMapping);
+            // 这里直接赋值,OnceReadListener 中可能会修改字段
+            this.headerAndFieldMapping = headerFieldMapping;
+            final ExcelReaderSheetBuilder sheet = EasyExcel.read(in, readListener).sheet();
+            sheet.doRead();
+        }
+
+        /**
+         * 返回表头
+         * @return
+         */
+        @Override
+        public Map<Integer, String> getHeaderMap() {
+            return readListener.getHeadMap();
+        }
+
+        @Override
+        public List<T> getData(ProcessCallback processCallback) {
+            final Map<Integer, String> headMap = readListener.getHeadMap();
+            if(headMap == null) {
+                throw new IllegalStateException("无法读取 Excel 表头,非法的 Excel 表格。");
+            }
+            final Map<Integer, String> headFieldMap = new HashMap<>();
+            for (Map.Entry<Integer, String> entry : headMap.entrySet()) {
+                final String value = headerAndFieldMapping.get(StringUtils.trimToEmpty(entry.getValue()));
+                if(StringUtils.isBlank(value)) {
+                    continue;
+                }
+                headFieldMap.put(entry.getKey(), value);
+            }
+            log.info("header: {}", headMap);
+
+            // 通过反射,获取 Bean 的字段和字段类型的映射,方便日期类型转换
+            Map<String, Field> objClassFields = new HashMap<>();
+            final Field[] fields = ReflectUtil.getFields(clazz);
+            for (Field field : fields) {
+                objClassFields.put(field.getName(), field);
+            }
+
+            ObjectMapper mapper = new ObjectMapper();
+            final List<Object> list = readListener.getList();
+            final List<T> fxaPeople = new ArrayList<>(list.size());
+            final int total = list.size();
+            int index = 0;
+            for (Object o : list) {
+                index++;
+                // 每 10 条数据,更新一次进度
+                if (index % 10 == 0) {
+                    processCallback.process((int) (index * 100.0f / total));
+                }
+                Map<Integer, Object> mapData = (Map) o;
+                JSONObject json = new JSONObject();
+
+                // mapData,key 是第 N 列,value 是值
+                for (Map.Entry<Integer, Object> entry : mapData.entrySet()) {
+                    final String fieldName = headFieldMap.get(entry.getKey());
+                    if (StringUtils.isBlank(fieldName)) {
+                        // 没有字段名称,需要舍弃的
+                        continue;
+                    }
+
+                    // 字段和值,放入,值没有转换,如:日期,字典码
+                    final Field field = objClassFields.get(fieldName);
+                    if(field == null || entry.getValue() == null) {
+                        // 如果字段为空,或者值为空,则写入空
+                        json.put(fieldName, entry.getValue());
+                    } else if(field.getType() == Date.class || field.getType() == java.sql.Date.class ) {
+                        // 日期类型
+                        final JsonFormat annotation = field.getAnnotation(JsonFormat.class);
+                        // System.out.println(fieldName + "    " + annotation + "  " + entry.getValue());
+                        if(entry.getValue() instanceof String) {
+                            try {
+                                String v = (String) entry.getValue();
+                                if (v.contains("/")) {
+                                    json.put(fieldName, FastDateFormat.getInstance("yyyy/MM/dd").parse(v));
+                                } else if (annotation != null) {
+                                    final String pattern = annotation.pattern();
+                                    json.put(fieldName, FastDateFormat.getInstance(pattern).parse(v));
+                                }
+                            } catch (ParseException e) {
+                                throw new IllegalStateException("第 " + index + " 行,错误的日期格式:" + entry.getValue());
+                            }
+                        } else if(entry.getValue() instanceof Number) {
+                            json.put(fieldName, new Date(((Number)entry.getValue()).longValue()));
+                        }
+                    } else {
+                        json.put(fieldName, entry.getValue());
+                    }
+                }
+                try {
+                    T obj = mapper.readValue(json.toJSONString(), clazz);
+                    fxaPeople.add(obj);
+                } catch (JsonProcessingException e) {
+                    throw new IllegalStateException("解析实体异常。", e);
+                }
+            }
+            // 入库量
+            log.info("excel data size: {}", fxaPeople.size());
+            return fxaPeople;
+        }
+    }
+
+    /**
+     * CSV 数据导入
+     *
+     * @author zhenqin
+     */
+    static class CsvFactory extends ExcelImportFactory {
+
+        /**
+         * 前端传入的表头
+         */
+        final Map<String, String> headerAndFieldMapping;
+
+        final InputStream in;
+
+        public CsvFactory(InputStream in, Map<String, String> headerFieldMapping) {
+            this.in = in;
+            this.headerAndFieldMapping = headerFieldMapping;
+        }
+
+        /**
+         * 返回表头
+         * @return
+         */
+        @Override
+        public Map<Integer, String> getHeaderMap() {
+            return null;
+        }
+
+        /**
+         * 返回数据
+         *
+         * @param processCallback 进度回调. 返回 0-100 之间的进度条
+         * @return
+         */
+        @Override
+        public List<JSONObject> getData(ProcessCallback processCallback) {
+            try (CSVReader csvReader = new CSVReaderBuilder(
+                    new BufferedReader(
+                            new InputStreamReader(in, StandardCharsets.UTF_8))).build();) {
+                String[] headTitle = csvReader.readNext();
+                List<String> headList = Arrays.asList(headTitle);
+                Map<Integer, String> headFieldMap = new HashMap<>();
+                for (int i = 0; i < headList.size(); i++) {
+                    String titleName = headList.get(i);
+                    String value = headerAndFieldMapping.get(StringUtils.trimToEmpty(titleName));
+                    if (StringUtils.isBlank(value)) {
+                        continue;
+                    }
+                    headFieldMap.put(i, value);
+                }
+                log.info("header: {}", headTitle);
+
+                List<String[]> dataList = csvReader.readAll();
+                List<JSONObject> fxaPeople = new ArrayList<>(dataList.size());
+                final int total = dataList.size();
+                int index = 0;
+                for (String[] data : dataList) {
+                    JSONObject json = new JSONObject();
+                    index++;
+                    // 每 10 条数据,更新一次进度
+                    if (index % 10 == 0) {
+                        processCallback.process((int) (index * 100.0f / total));
+                    }
+                    for (int i = 0; i < data.length; i++) {
+                        String fieldName = headFieldMap.get(i);
+                        String dataValue = data[i];
+                        if (StringUtils.isBlank(fieldName)) {
+                            // 没有字段名称,需要舍弃的
+                            continue;
+                        }
+
+                        json.put(fieldName, dataValue);
+                    }
+
+                    fxaPeople.add(json);
+                }
+
+                // 入库量
+                log.info("csv data size: {}", fxaPeople.size());
+                return fxaPeople;
+            } catch (Exception e) {
+                e.printStackTrace();
+                return new ArrayList<JSONObject>();
+            }
+        }
+    }
+}

+ 170 - 0
java-excel/OnceReadListener.java

@@ -0,0 +1,170 @@
+package cn.exlive.exbooter.excel;
+
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Excel 读取数据,滑动读取表头,读取到表头后,开始读取数据。如果没有读取到表头,就没有数据。
+ *
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2022/10/6
+ * Time: 下午5:44
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+public class OnceReadListener extends AnalysisEventListener<Object> {
+
+    /**
+     * 预期的标题 Header
+     */
+    final Map<String, String> headerFieldMapping;
+
+    /**
+     * 实际数据
+     */
+    private List<Object> list = new ArrayList<Object>();
+
+    /**
+     * 实际的 Header
+     */
+    Map<Integer, String> headMap;
+
+    /**
+     * 是否有 header
+     */
+    volatile boolean hasHeader;
+
+    public OnceReadListener(Map<String, String> headerFieldMapping) {
+        this.headerFieldMapping = headerFieldMapping;
+    }
+
+    /**
+     * 滑动读取表头
+     * @param headMap Excel 实际表头
+     * @param context
+     */
+    @Override
+    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
+        Map<Integer, String> newHeadMap = new HashMap<>(headMap.size());
+        // 使用 Stream API 对换 Key 和 Value
+        Map<String, Integer> headMapInverse = new HashMap<>(headMap.size());
+        for (Map.Entry<Integer, String> entry : headMap.entrySet()) {
+            final String oriHead = entry.getValue();
+            headMapInverse.put(oriHead, entry.getKey());
+
+            String head = oriHead;
+            // head 可能包含特殊字符
+            if(head.contains("*")) {
+                head = head.replaceAll("\\*", "");
+            } else if(head.contains("(")) {
+                head = head.startsWith("(") ? head.substring(head.indexOf(")") + 1) : head.substring(0, head.indexOf("("));
+            } else if(head.contains("(")) {
+                head = head.startsWith("(") ? head.substring(head.indexOf(")") + 1) : head.substring(0, head.indexOf("("));
+            } else if(head.contains("【")) {
+                head = head.startsWith("【") ? head.substring(head.indexOf("】") + 1) : head.substring(0, head.indexOf("【"));
+            } else if(head.contains("[")) {
+                head = head.startsWith("[") ? head.substring(head.indexOf("]") + 1) : head.substring(0, head.indexOf("["));
+            }
+            // oriHead 中 包含特殊字符
+            if(!StringUtils.equals(oriHead, head)) {
+                headMapInverse.put(head, entry.getKey());
+            }
+        }
+        int matchCount = 0;
+        final HashSet<Map.Entry<String, String>> entries = new HashSet<>(headerFieldMapping.entrySet());
+        for (Map.Entry<String, String> entry : entries) {
+            String key = entry.getKey();
+            if(key.contains("*")) {
+                key = key.replaceAll("\\*", "");
+            } else if(key.contains("(")) {
+                key = key.startsWith("(") ? key.substring(key.indexOf(")") + 1) : key.substring(0, key.indexOf("("));
+            } else if(key.contains("(")) {
+                key = key.startsWith("(") ? key.substring(key.indexOf(")") + 1) : key.substring(0, key.indexOf("("));
+            } else if(key.contains("【")) {
+                key = key.startsWith("【") ? key.substring(key.indexOf("】") + 1) : key.substring(0, key.indexOf("【"));
+            } else if(key.contains("[")) {
+                key = key.startsWith("[") ? key.substring(key.indexOf("]") + 1) : key.substring(0, key.indexOf("["));
+            }
+            final Integer index = headMapInverse.get(key);
+            if(index != null) {
+                matchCount++;
+                newHeadMap.put(index, key);
+            }
+            // 检测是否发生了变化,如果变化,则重新放入
+            if(!StringUtils.equals(entry.getKey(), key)) {
+                // 重新放入新的字段
+                headerFieldMapping.put(key, entry.getValue());
+            }
+        }
+
+        // 如果 10个 header,满足了 6 个,则成功
+        if((matchCount * 1.0f / headerFieldMapping.size()) >= 0.5f) {
+            this.headMap = newHeadMap;
+            this.hasHeader = true;
+        }
+    }
+
+    /**
+     * 处理数据,需要判断 header 是否读取到,读取不到则往下继续读,直到结束
+     * @param object    one row value. It is same as {@link AnalysisContext#readRowHolder()}
+     * @param context analysis context
+     */
+    @Override
+    public void invoke(Object object, AnalysisContext context) {
+        // 先找到表头,有时候表头不在第一行
+        if(!hasHeader) {
+            this.invokeHeadMap((Map<Integer, String>)object, context);
+            return;
+        }
+        //System.out.println(object);
+        list.add(object);
+    }
+
+    @Override
+    public void doAfterAllAnalysed(AnalysisContext context) {
+
+    }
+
+    /**
+     * 返回表头,如果没有读取到表头,则返回 null
+     * @return
+     */
+    public Map<Integer, String> getHeadMap() {
+        return headMap;
+    }
+
+    @Override
+    public boolean hasNext(AnalysisContext context) {
+        return super.hasNext(context);
+    }
+
+    /**
+     * 返回数据列表
+     * @return
+     */
+    public List<Object> getList() {
+        return list;
+    }
+
+    public void setList(List<Object> list) {
+        this.list = list;
+    }
+}

+ 24 - 0
java-excel/ProcessCallback.java

@@ -0,0 +1,24 @@
+package cn.exlive.exbooter.excel;
+
+/**
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2022/10/22
+ * Time: 下午10:34
+ * Vendor: yiidata.com
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@FunctionalInterface
+public interface ProcessCallback {
+
+    /**
+     * 数据读取进度
+     * @param process
+     */
+    public void process(int process);
+}

+ 3 - 11
spring-zuul/ApiGatewayConfig.java

@@ -15,7 +15,7 @@
  *  limitations under the License.
  */
 
-package com.datasophon.api.configuration;
+package cn.exlive.exbooter.configure;
 
 import com.google.common.collect.Sets;
 import lombok.Getter;
@@ -29,8 +29,6 @@ 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.HashSet;
 import java.util.Map;
@@ -58,25 +56,21 @@ import java.util.Set;
 @Configuration
 public class ApiGatewayConfig implements InitializingBean {
 
-
     @Autowired
     ApiRouteProperties apiRouteProperties;
 
-
     @Autowired
     ServerProperties serverProperties;
 
-
     @Autowired
     ZuulProperties zuulProperties;
 
-
     @Override
     public void afterPropertiesSet() throws Exception {
         final Map<String, ApiRoute> routers = apiRouteProperties.getRouters();
-        final Set<String> ignoreUrlSet = new HashSet<>();  // 排除的 URL
+        final Set<String> ignoreUrlSet = new HashSet<>(); // 排除的 URL
         for (Map.Entry<String, ApiRoute> entry : routers.entrySet()) {
-            if(entry.getValue() == null || StringUtils.isBlank(entry.getValue().getUrl())) {
+            if (entry.getValue() == null || StringUtils.isBlank(entry.getValue().getUrl())) {
                 continue;
             }
 
@@ -97,7 +91,6 @@ public class ApiGatewayConfig implements InitializingBean {
                 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 {
                 // 返回拼接的地址
@@ -116,7 +109,6 @@ public class ApiGatewayConfig implements InitializingBean {
         zuulProperties.init();
     }
 
-
     /**
      * 支持简单的负载均衡
      * @return

+ 47 - 0
spring-zuul/ApiRoute.java

@@ -0,0 +1,47 @@
+/*
+ *  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.exbooter.configure;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2023/3/15
+ * Time: 下午6:26
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@Setter
+@Getter
+public class ApiRoute {
+
+    String api;
+
+    String url;
+
+    /**
+     * 路由类型,支持 http 和 ws 两种类型。ws 是 websocket 长连接
+     */
+    String type = "http";
+}

+ 51 - 8
spring-zuul/ApiRouteLocator.java

@@ -1,11 +1,38 @@
-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.
+ */
+
+package cn.exlive.exbooter.configure;
 
+import com.netflix.zuul.context.RequestContext;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.keyvalue.DefaultKeyValue;
+import org.apache.commons.lang.StringUtils;
 import org.springframework.cloud.netflix.zuul.filters.Route;
 import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
 
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.Random;
+import java.util.stream.Collectors;
 
 /**
  * <pre>
@@ -14,7 +41,6 @@ import java.util.Random;
  * User: zhenqin
  * Date: 2022/4/25
  * Time: 下午7:12
- * Vendor: yiidata.com
  *
  * </pre>
  *
@@ -28,7 +54,6 @@ public class ApiRouteLocator extends SimpleRouteLocator {
      */
     final ZuulProperties properties;
 
-
     /**
      * 随机策略
      */
@@ -39,15 +64,25 @@ public class ApiRouteLocator extends SimpleRouteLocator {
         this.properties = properties;
     }
 
-
     @Override
     protected Route getRoute(ZuulProperties.ZuulRoute route, String path) {
         if (route == null) {
             return null;
         }
-        if (log.isDebugEnabled()) {
-            log.debug("route matched=" + route);
+        RequestContext ctx = RequestContext.getCurrentContext();
+        final String referer = ctx.getRequest().getHeader("Referer");
+        final Map<String, List<String>> queryParams = Optional.ofNullable(ctx.getRequestQueryParams()).orElse(new HashMap<>());
+        String clusterId = queryParams.get("clusterId") == null ? null : StringUtils.join(queryParams.get("clusterId"), ",");
+        if (StringUtils.isBlank(clusterId) && StringUtils.isNotBlank(referer)) {
+            // 如果 URL 中没有,从 Referer 中获取
+            final String query = URI.create(referer).getQuery();
+            final Map<String, String> kv = Arrays.stream(query.split("&")).filter(StringUtils::isNotBlank).map(it -> {
+                final String[] split = it.split("=");
+                return split.length > 1 ? new DefaultKeyValue(split[0], split[1]) : new DefaultKeyValue(split[0], "");
+            }).collect(Collectors.toMap(e -> (String)e.getKey(), e -> (String)e.getValue()));
+            clusterId = kv.get("clusterId");
         }
+        log.info("clusterId: {} and referer: {}", clusterId, referer);
         String targetPath = path;
         String prefix = this.properties.getPrefix();
         if (prefix.endsWith("/")) {
@@ -69,12 +104,17 @@ public class ApiRouteLocator extends SimpleRouteLocator {
             retryable = route.getRetryable();
         }
 
+        final HashMap<String, String> globalVariables = new HashMap<>();
+        // cluster: 1 path: /damp/aaa/bbb match to: http://localhost:8083/damp
+        log.info("cluster: {} path: {} match to: {}", clusterId, path, route.getLocation());
         // 如果包含逗号,说明有多个地址,需要负载均衡
-        if(route.getLocation().contains(",")) {
+        if (route.getLocation().contains(",")) {
             final String[] segs = route.getLocation().split(",");
             // 随机策略
             String loc = segs[r.nextInt(segs.length)];
-            //log.info("loc->> " + loc);
+            // 替换路由地址
+            // loc = PlaceholderUtils.replacePlaceholders(loc, globalVariables, Constants.REGEX_VARIABLE);
+            // log.info("loc->> " + loc);
             return new Route(route.getId(),
                     targetPath,
                     loc,
@@ -83,6 +123,8 @@ public class ApiRouteLocator extends SimpleRouteLocator {
                     route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null,
                     route.isStripPrefix());
         }
+        // 替换路由地址
+        // String loc = PlaceholderUtils.replacePlaceholders(route.getLocation(), globalVariables, Constants.REGEX_VARIABLE);
         return new Route(route.getId(),
                 targetPath,
                 route.getLocation(),
@@ -91,4 +133,5 @@ public class ApiRouteLocator extends SimpleRouteLocator {
                 route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null,
                 route.isStripPrefix());
     }
+
 }

+ 93 - 0
spring-zuul/ApiRouteProperties.java

@@ -0,0 +1,93 @@
+/*
+ *  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.exbooter.configure;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.validation.constraints.NotBlank;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * <pre>
+ *
+ * Created by zhenqin.
+ * User: zhenqin
+ * Date: 2022/4/25
+ * Time: 下午7:12
+ *
+ * </pre>
+ *
+ * @author zhenqin
+ */
+@Slf4j
+@Setter
+@Getter
+@Configuration
+@ConfigurationProperties(prefix = "exbooter")
+public class ApiRouteProperties {
+
+    /**
+     * 绑定的 tasks
+     */
+    Map<String, ApiRoute> routers;
+
+    /**
+     * 排除的地址
+     */
+    String excludePatterns = "";
+
+    /**
+     * 代理的总路由,这里默认是 /, 实际在 DDH 中访问是 /jdh/
+     */
+    String servletPath = "/";
+
+    public ApiRouteProperties() {
+    }
+
+    @NotBlank
+    public String getServletPath() {
+        return Optional.ofNullable(StringUtils.trimToNull(servletPath)).orElse("/");
+    }
+
+    /**
+     * 返回所有的 task Name
+     * @return
+     */
+    public Set<String> getAllRouters() {
+        if (routers == null) {
+            routers = new HashMap<>();
+        }
+        return routers.keySet();
+    }
+
+    public Map<String, ApiRoute> getRouters() {
+        getAllRouters();
+        return routers;
+    }
+}

+ 3 - 0
spring-zuul/README.md

@@ -12,6 +12,9 @@ POM Import
 
 ```yml
 datasophon:
+  # 这里的路由地址访问实际是:/${contextPath}/${servletPath}
+  servletPath: "/"
+  excludePatterns: "/api/user/**,/api/frame/**"
   routers:
     d:
       api: /d/**