├── .gitattributes ├── Dockerfile ├── LICENSE ├── README.md ├── deploy ├── swagger-kubernetes-ac.yaml └── swagger-kubernetes-deploy.yaml ├── pom.xml └── src └── main ├── java └── club │ └── mydlq │ └── swagger │ └── kubernetes │ ├── Application.java │ ├── config │ ├── DiscoveryConfig.java │ ├── EnableSwaggerKubernetes.java │ ├── StartConnection.java │ ├── SwaggerConfig.java │ └── ZuulConfig.java │ ├── discovery │ ├── KubernetesConnect.java │ └── KubernetesDiscovery.java │ ├── entity │ └── ServiceInfo.java │ ├── param │ ├── DiscoveryAutoConfig.java │ ├── KubernetesAutoConfig.java │ └── SwaggerAutoConfig.java │ ├── swagger │ ├── MvcController.java │ ├── SwaggerResources.java │ └── SwaggerUIMvcConfig.java │ ├── utils │ ├── FileUtils.java │ ├── HttpUtils.java │ └── ValidationUtils.java │ └── zuul │ ├── RefreshRoute.java │ └── ZuulRouteLocator.java └── resources ├── application.yml └── ui ├── swaggerbootstrapui-md.css ├── swaggerbootstrapui.css └── swaggerbootstrapui.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=java 2 | *.css linguist-language=java 3 | *.html linguist-language=java 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u282 as builder 2 | WORKDIR application 3 | COPY target/*.jar application.jar 4 | RUN java -Djarmode=layertools -jar application.jar extract 5 | 6 | FROM openjdk:8u282-jre 7 | WORKDIR application 8 | COPY --from=builder application/dependencies/ ./ 9 | COPY --from=builder application/snapshot-dependencies/ ./ 10 | COPY --from=builder application/spring-boot-loader/ ./ 11 | COPY --from=builder application/application/ ./ 12 | ENV TZ="Asia/Shanghai" 13 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 14 | ENV JVM_OPTS="-XX:MaxRAMPercentage=90.0 -Duser.timezone=Asia/Shanghai -Xss256k" 15 | ENV JAVA_OPTS="" 16 | ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![](https://mydlq-club.oss-cn-beijing.aliyuncs.com/images/swagger-kubernetes-1001.jpg) 3 | 4 | # Swagger Kubernetes 5 | 6 | ## 一、简介 7 | 8 | Swagger Kubernetes 是能够将 Kubernetes 环境下 Spring 项目的 Swagger 文档聚合,只要 Spring 项目中引用了 Swagger 工具暴露 Swagger API,就可以将其所有的这类项目 Swagger 接口聚合到 Swagger Kubernetes 项目当中。 9 | 10 | Swagger Kubernetes 是拥有在 Kubernetes 环境中服务发现功能,能够自动服务发现那些暴露 Swagger API 的服务,然后生成 Markdown 格式的文档展示在页面上,通过反向代理可以直接调用对应服务接口进行调试工作。 11 | 12 | 由于方便,已经将该项目以 Docker 镜像的方式存放到 Docker Hub 仓库。 13 | 14 | - hub地址:https://hub.docker.com/r/mydlqclub/swagger-kubernetes 15 | - Docker镜像: mydlqclub/swagger-kubernetes 16 | 17 | 18 | ![](https://mydlq-club.oss-cn-beijing.aliyuncs.com/images/swagger-kubernetes-1002.jpg?x-oss-process=style/shuiyin) 19 | 20 | ## 二、架构图 21 | 22 | 23 | ![](https://mydlq-club.oss-cn-beijing.aliyuncs.com/images/swagger-kubernetes-1003.jpg?x-oss-process=style/shuiyin) 24 | 25 | ## 三、注意事项 26 | 27 | 注意:在swagger2配置文件中,请不要配置归属组名“groupName”参数,否则将无法将其加入聚合列表 28 | 29 | ## 四、如何使用 30 | 31 | Swagger Kubernetes 是应用在 Kubernetes 环境下,监控服务所在 Namespace 的各个 Spring 应用 Swagger API 接口,所以需要将此应用部署到 Kubernetes 环境下。 32 | 33 | 下面将演示如何在 Kubernetes 集群部署 Swagger Kubernetes。 34 | 35 | ### 1、创建 ServiceAccount 36 | 37 | **swagger-kubernetes-ac.yaml** 38 | 39 | > 请提前修改里面的全部 Namespace 的值为你自己的 Namespace 名称 40 | 41 | ```yaml 42 | apiVersion: v1 43 | kind: ServiceAccount 44 | metadata: 45 | name: swagger-kubernetes 46 | namespace: mydlqcloud 47 | --- 48 | kind: Role 49 | apiVersion: rbac.authorization.k8s.io/v1beta1 50 | metadata: 51 | name: swagger-kubernetes-role 52 | namespace: mydlqcloud 53 | rules: 54 | - apiGroups: [""] 55 | resources: ["services","endpoints"] 56 | verbs: ["get","list","watch"] 57 | --- 58 | kind: RoleBinding 59 | apiVersion: rbac.authorization.k8s.io/v1 60 | metadata: 61 | name: rbac-role-binding 62 | namespace: mydlqcloud 63 | subjects: 64 | - kind: ServiceAccount 65 | name: swagger-kubernetes 66 | namespace: mydlqcloud 67 | roleRef: 68 | apiGroup: rbac.authorization.k8s.io 69 | kind: Role 70 | name: swagger-kubernetes-role 71 | ``` 72 | 73 | **创建 ServiceAccount** 74 | 75 | ```bash 76 | $ kubectl apply -f swagger-kubernetes-ac.yaml 77 | ``` 78 | 79 | ### 2、创建 Swagger kubernetes 服务 80 | 81 | **swagger-kubernetes-deploy.yaml** 82 | 83 | ```yaml 84 | apiVersion: v1 85 | kind: Service 86 | metadata: 87 | name: swagger-kubernetes 88 | namespace: mydlqcloud 89 | labels: 90 | app: swagger-kubernetes 91 | spec: 92 | ports: 93 | - name: tcp 94 | port: 8080 95 | nodePort: 32255 96 | targetPort: 8080 97 | type: NodePort 98 | selector: 99 | app: swagger-kubernetes 100 | --- 101 | apiVersion: apps/v1 102 | kind: Deployment 103 | metadata: 104 | name: swagger-kubernetes 105 | namespace: mydlqcloud 106 | labels: 107 | app: swagger-kubernetes 108 | spec: 109 | selector: 110 | matchLabels: 111 | app: swagger-kubernetes 112 | template: 113 | metadata: 114 | labels: 115 | app: swagger-kubernetes 116 | spec: 117 | serviceAccountName: swagger-kubernetes 118 | containers: 119 | - name: swagger-kubernetes 120 | image: mydlqclub/swagger-kubernetes:latest 121 | #国内使用 aliyun 镜像仓库 122 | #image: registry.cn-beijing.aliyuncs.com/mydlq/swagger-kubernetes:latest 123 | imagePullPolicy: IfNotPresent 124 | ports: 125 | - name: server 126 | containerPort: 8080 127 | resources: 128 | limits: 129 | cpu: 2000m 130 | memory: 512Mi 131 | requests: 132 | cpu: 500m 133 | memory: 512Mi 134 | ``` 135 | 136 | **创建 ServiceAccount** 137 | 138 | -n:指定启动的 namespace,执行前请先修改此值 139 | 140 | ```bash 141 | $ kubectl apply -f swagger-kubernetes-deploy.yaml -n mydlqcloud 142 | ``` 143 | 144 | ### 3、查看创建的资源 145 | 146 | ```bash 147 | $ kubectl get pod,service -n mydlqcloud | grep swagger-kubernetes 148 | 149 | pod/swagger-kubernetes-5577dc9d8d-6sz4f 1/1 Running 0 150 | service/swagger-kubernetes NodePort 10.10.204.142 8080:32255/TCP 151 | ``` 152 | 153 | ### 4、访问 Swagger Kubernetes 154 | 155 | 输入地址: http://Kuberntes集群地址:32255 访问 Swagger Kubernetes 156 | 157 | ## 五、可配置环境变量参数 158 | 159 | 一般情况用默认配置即可,有些特殊情况需要自定义设置,可以做如下配置: 160 | 161 | 变量名 | 默认值 |描述 162 | ---|----|--- 163 | KUBERNETES_CONNECT_URL | https://kubernetes.default.svc.cluster.local |Kubernetes API 地址 164 | KUBERNETES_CONNECT_TOKEN | 应用 Pod 中设置的 ServiceAccount 关联的 Token| 连接 Kubernetes API-Server 的 Token,应用会根据此 Token 而拥有不同的权限 165 | DISCOVERY_NAMESPACE | Service 所在的 Namespace | Swagger 聚合文档的 Kubernetes Namespace 166 | DISCOVERY_PORT_TYPE | ClusterIP | Swagger-kubernetes 监控应用 Service 端口类型,支持 ClusterIP 和 NodePort 167 | DISCOVERY_INITIAL_INTERVAL | 60 | 服务发现的更新间隔,推荐60秒 168 | SWGGER_API_PATH | /v2/api-docs | 应用 Swagger API 地址 169 | IGNORE_SERVICES | - | 默认的忽略列表,例如设置为"service1,service2,......" 170 | ACTUATOR_PORT | 8080 | SpringBoot management 端口设置 171 | ACTUATOR_TYPE | * | SpringBoot Actuator 暴露的参数,可以设置为 health,info,env,metrics,prometheus.... 172 | 173 | 174 | -------------------------------------------------------------------------------- /deploy/swagger-kubernetes-ac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: swagger-kubernetes 5 | namespace: mydlqcloud 6 | --- 7 | kind: Role 8 | apiVersion: rbac.authorization.k8s.io/v1beta1 9 | metadata: 10 | name: swagger-kubernetes-role 11 | namespace: mydlqcloud 12 | rules: 13 | - apiGroups: [""] 14 | resources: ["services","endpoints"] 15 | verbs: ["get","list","watch"] 16 | --- 17 | kind: RoleBinding 18 | apiVersion: rbac.authorization.k8s.io/v1 19 | metadata: 20 | name: rbac-role-binding 21 | namespace: mydlqcloud 22 | subjects: 23 | - kind: ServiceAccount 24 | name: swagger-kubernetes 25 | namespace: mydlqcloud 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: Role 29 | name: swagger-kubernetes-role 30 | -------------------------------------------------------------------------------- /deploy/swagger-kubernetes-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: swagger-kubernetes 5 | namespace: mydlqcloud 6 | labels: 7 | app: swagger-kubernetes 8 | spec: 9 | ports: 10 | - name: tcp 11 | port: 8080 12 | nodePort: 32255 13 | targetPort: 8080 14 | type: NodePort 15 | selector: 16 | app: swagger-kubernetes 17 | --- 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: swagger-kubernetes 22 | namespace: mydlqcloud 23 | labels: 24 | app: swagger-kubernetes 25 | spec: 26 | selector: 27 | matchLabels: 28 | app: swagger-kubernetes 29 | template: 30 | metadata: 31 | labels: 32 | app: swagger-kubernetes 33 | spec: 34 | serviceAccountName: swagger-kubernetes 35 | containers: 36 | - name: swagger-kubernetes 37 | image: mydlqclub/swagger-kubernetes:latest 38 | #国内使用 aliyun 镜像仓库 39 | #image: registry.cn-beijing.aliyuncs.com/mydlq/swagger-kubernetes:latest 40 | imagePullPolicy: IfNotPresent 41 | ports: 42 | - name: server 43 | containerPort: 8080 44 | resources: 45 | limits: 46 | cpu: 2000m 47 | memory: 512Mi 48 | requests: 49 | cpu: 500m 50 | memory: 512Mi -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.3.9.RELEASE 10 | 11 | 12 | club.mydlq 13 | swagger-kubernetes 14 | 1.1.1 15 | jar 16 | 17 | swagger-kubernetes 18 | https://github.com/my-dlq/swagger-kubernetes 19 | swagger api doc for kubernetes discovery. 20 | 21 | 22 | 23 | The Apache Software License, Version 2.0 24 | http://www.apache.org/licenses/LICENSE-2.0.txt 25 | repo 26 | 27 | 28 | 29 | 30 | http://mydlq.club 31 | scm:git:https://github.com/my-dlq/swagger-kubernetes.git 32 | scm:git:https://github.com/my-dlq/swagger-kubernetes.git 33 | 34 | 35 | 36 | 37 | mydlq 38 | mynamedlq@163.com 39 | http://mydlq.club 40 | http://mydlq.club 41 | 42 | 43 | 44 | 45 | 1.8 46 | 2.1.3.RELEASE 47 | 11.0.0 48 | 2.0.8 49 | UTF-8 50 | 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-web 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-configuration-processor 61 | true 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-actuator 67 | 68 | 69 | 70 | io.micrometer 71 | micrometer-registry-prometheus 72 | 73 | 74 | 75 | org.springframework.cloud 76 | spring-cloud-starter-netflix-zuul 77 | ${version.zuul} 78 | 79 | 80 | 81 | io.kubernetes 82 | client-java 83 | ${version.kubernetes} 84 | 85 | 86 | 87 | org.projectlombok 88 | lombok 89 | 90 | 91 | 92 | com.github.xiaoymin 93 | knife4j-spring-boot-starter 94 | ${version.knife4j} 95 | 96 | 97 | 98 | com.google.guava 99 | guava 100 | 21.0 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.springframework.boot 108 | spring-boot-maven-plugin 109 | 110 | 111 | true 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/Application.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes; 2 | 3 | import club.mydlq.swagger.kubernetes.config.EnableSwaggerKubernetes; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | @EnableSwaggerKubernetes 9 | public class Application { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(Application.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/config/DiscoveryConfig.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.config; 2 | 3 | import club.mydlq.swagger.kubernetes.discovery.KubernetesConnect; 4 | import club.mydlq.swagger.kubernetes.discovery.KubernetesDiscovery; 5 | import club.mydlq.swagger.kubernetes.param.DiscoveryAutoConfig; 6 | import club.mydlq.swagger.kubernetes.param.KubernetesAutoConfig; 7 | import club.mydlq.swagger.kubernetes.param.SwaggerAutoConfig; 8 | import club.mydlq.swagger.kubernetes.swagger.SwaggerResources; 9 | import club.mydlq.swagger.kubernetes.zuul.RefreshRoute; 10 | import club.mydlq.swagger.kubernetes.zuul.ZuulRouteLocator; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 13 | import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.scheduling.annotation.EnableScheduling; 16 | 17 | /** 18 | * @author mydlq 19 | */ 20 | @EnableScheduling 21 | @EnableConfigurationProperties({KubernetesAutoConfig.class, DiscoveryAutoConfig.class}) 22 | public class DiscoveryConfig { 23 | 24 | @Autowired 25 | SwaggerResources swaggerResourcesProcessor; 26 | 27 | @Autowired 28 | ZuulRouteLocator zuulRouteLocator; 29 | 30 | @Autowired 31 | RefreshRoute refreshRouteService; 32 | 33 | @Autowired 34 | SwaggerAutoConfig swaggerAutoConfig; 35 | 36 | @Autowired 37 | protected ZuulProperties zuulProperties; 38 | 39 | @Autowired 40 | private DiscoveryAutoConfig discoveryAutoConfig; 41 | 42 | @Autowired 43 | KubernetesAutoConfig kubernetesAutoConfig; 44 | 45 | @Bean 46 | public KubernetesConnect connectKubernetes() { 47 | return new KubernetesConnect(kubernetesAutoConfig); 48 | } 49 | 50 | @Bean 51 | public KubernetesDiscovery kubernetesDiscovery() { 52 | return new KubernetesDiscovery(swaggerResourcesProcessor, zuulRouteLocator, 53 | refreshRouteService, swaggerAutoConfig, zuulProperties, discoveryAutoConfig); 54 | } 55 | 56 | @Bean 57 | public StartConnection startConnection() { 58 | return new StartConnection(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/config/EnableSwaggerKubernetes.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.config; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Import; 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * @author mydlq 9 | */ 10 | @Target({ElementType.TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Documented 13 | @ComponentScan 14 | @Import({DiscoveryConfig.class, SwaggerConfig.class, ZuulConfig.class}) 15 | public @interface EnableSwaggerKubernetes { 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/config/StartConnection.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.config; 2 | 3 | import club.mydlq.swagger.kubernetes.discovery.KubernetesDiscovery; 4 | import club.mydlq.swagger.kubernetes.discovery.KubernetesConnect; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.CommandLineRunner; 7 | import org.springframework.core.annotation.Order; 8 | 9 | /** 10 | * SpringBoot 应用启动成功后首先要执行的任务 11 | * 12 | * @author mydlq / 小豆丁 13 | * Blog: http://www.mydlq.club 14 | * Github: https://github.com/my-dlq/ 15 | */ 16 | @Order(value = 0) 17 | public class StartConnection implements CommandLineRunner { 18 | 19 | @Autowired 20 | KubernetesConnect connectKubernetes; 21 | @Autowired 22 | KubernetesDiscovery kubernetesDiscovery; 23 | 24 | @Override 25 | public void run(String... args) throws Exception { 26 | // Connection kubernetes 27 | connectKubernetes.connection(); 28 | // Init service data 29 | kubernetesDiscovery.freshServiceList(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.config; 2 | 3 | import club.mydlq.swagger.kubernetes.param.SwaggerAutoConfig; 4 | import club.mydlq.swagger.kubernetes.swagger.SwaggerResources; 5 | import club.mydlq.swagger.kubernetes.swagger.SwaggerUIMvcConfig; 6 | import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Primary; 11 | import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; 12 | 13 | /** 14 | * @author mydlq 15 | */ 16 | @EnableKnife4j 17 | @EnableSwagger2WebMvc 18 | @EnableConfigurationProperties(SwaggerAutoConfig.class) 19 | public class SwaggerConfig { 20 | 21 | @Autowired 22 | private SwaggerAutoConfig swaggerAutoConfig; 23 | 24 | @Bean 25 | @Primary 26 | public SwaggerResources swaggerResourcesProcessor() { 27 | return new SwaggerResources(swaggerAutoConfig); 28 | } 29 | 30 | @Bean 31 | public SwaggerUIMvcConfig swaggerUIModifyMvcConfig() { 32 | return new SwaggerUIMvcConfig(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/config/ZuulConfig.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.config; 2 | 3 | import club.mydlq.swagger.kubernetes.zuul.RefreshRoute; 4 | import club.mydlq.swagger.kubernetes.zuul.ZuulRouteLocator; 5 | import org.springframework.boot.autoconfigure.web.ServerProperties; 6 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 7 | import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | /** 11 | * @author mydlq 12 | * @author WeiX Sun 13 | */ 14 | @EnableZuulProxy 15 | public class ZuulConfig { 16 | 17 | @Bean 18 | public ZuulRouteLocator routeLocator(ServerProperties server, 19 | ZuulProperties zuulProperties) { 20 | return new ZuulRouteLocator(server.getServlet().getContextPath(), zuulProperties); 21 | } 22 | 23 | @Bean 24 | public RefreshRoute refreshRoute(ZuulRouteLocator routeLocator) { 25 | return new RefreshRoute(routeLocator); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/discovery/KubernetesConnect.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.discovery; 2 | 3 | import club.mydlq.swagger.kubernetes.param.KubernetesAutoConfig; 4 | import club.mydlq.swagger.kubernetes.utils.FileUtils; 5 | import io.kubernetes.client.openapi.ApiClient; 6 | import io.kubernetes.client.openapi.Configuration; 7 | import io.kubernetes.client.util.Config; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.StringUtils; 10 | import java.io.*; 11 | 12 | /** 13 | * Author: mydlq / 小豆丁 14 | * Blog: http://www.mydlq.club 15 | * Github: https://github.com/my-dlq/ 16 | * 17 | * Describe: Connection kubernetes 18 | */ 19 | @Slf4j 20 | public class KubernetesConnect { 21 | 22 | private KubernetesAutoConfig kubernetesAutoConfig; 23 | 24 | public KubernetesConnect(KubernetesAutoConfig kubernetesAutoConfig) { 25 | this.kubernetesAutoConfig = kubernetesAutoConfig; 26 | } 27 | 28 | /** 29 | * 连接 Kubernetes 集群 30 | * Connection kubernetes 31 | */ 32 | public void connection() { 33 | String token = kubernetesAutoConfig.getToken(); 34 | String tokenPath = kubernetesAutoConfig.getTokenPath(); 35 | String url = kubernetesAutoConfig.getUrl(); 36 | String caPath = kubernetesAutoConfig.getCaPath(); 37 | String formConfigPath = kubernetesAutoConfig.getFromConfigPath(); 38 | boolean isFromCluster = kubernetesAutoConfig.isFromCluster(); 39 | boolean isFromDefault = kubernetesAutoConfig.isFromDefault(); 40 | boolean isValidateSSL = kubernetesAutoConfig.isValidateSsl(); 41 | // form token 42 | if (StringUtils.isNotEmpty(tokenPath) && StringUtils.isNotEmpty(url)) { 43 | log.info("from token file connection kubernetes"); 44 | token = FileUtils.readFile(tokenPath); 45 | connectFromToken(url, token, isValidateSSL, caPath); 46 | } else if (StringUtils.isNotEmpty(token) && StringUtils.isNotEmpty(url)) { 47 | log.info("from token connection kubernetes"); 48 | connectFromToken(url, token, isValidateSSL, caPath); 49 | } 50 | // form cluster 51 | else if (isFromCluster) { 52 | log.info("from cluster env connection kubernetes"); 53 | connectFromCluster(); 54 | } 55 | // form config 56 | else if (StringUtils.isNotEmpty(formConfigPath)) { 57 | log.info("from config file connection kubernetes"); 58 | connectFromConfig(formConfigPath); 59 | } else if (isFromDefault) { 60 | log.info("from $HOME/.kube/config connection kubernetes"); 61 | connectFromSystemConfig(); 62 | } 63 | } 64 | 65 | /** 66 | * 默认方式,从系统配置 $HOME/.kube/config 读取配置文件连接 Kubernetes 集群 67 | * By default, connect to the Kubernetes cluster by reading a configuration file from the system configuration $HOME/.kube/config 68 | */ 69 | private void connectFromSystemConfig() { 70 | ApiClient apiClient = null; 71 | try { 72 | apiClient = Config.defaultClient(); 73 | } catch (IOException e) { 74 | log.error("read config file error",e); 75 | } 76 | Configuration.setDefaultApiClient(apiClient); 77 | } 78 | 79 | /** 80 | * 从指定文件读取配置文件连接 Kubernetes 集群 81 | * Reads a configuration file from the specified file to connect to the Kubernetes cluster 82 | * 83 | * @param configPath 84 | */ 85 | private void connectFromConfig(String configPath) { 86 | ApiClient apiClient = null; 87 | try { 88 | apiClient = Config.fromConfig(configPath); 89 | } catch (IOException e) { 90 | log.error("read config file error",e); 91 | } 92 | Configuration.setDefaultApiClient(apiClient); 93 | } 94 | 95 | /** 96 | * 通过 Token 连接 Kubernetes 集群 97 | * Connect to the Kubernetes cluster over Token 98 | * 99 | * @param url 100 | * @param token 101 | * @param validateSSL 102 | * @param caPath 103 | */ 104 | private void connectFromToken(String url, String token, boolean validateSSL, String caPath) { 105 | ApiClient apiClient = Config.fromToken(url, token, validateSSL); 106 | // validateSSL 107 | if (validateSSL) { 108 | try { 109 | apiClient.setSslCaCert(new FileInputStream(caPath)); 110 | } catch (FileNotFoundException e) { 111 | log.error("Check that the certificate file exists"); 112 | } 113 | } 114 | Configuration.setDefaultApiClient(apiClient); 115 | } 116 | 117 | /** 118 | * 如果在 kubernetes 集群内,则利用 kubernetes 环境 119 | * If within the kubernetes cluster, the kubernetes environment is utilized 120 | */ 121 | private void connectFromCluster() { 122 | // 判断是否为集群内的一个 pod 来判断是使用 pod 权限连接 123 | if (!FileUtils.checkFolderExist(Config.SERVICEACCOUNT_ROOT)){ 124 | log.error("error connecting to Kubernetes cluster!"); 125 | return; 126 | } 127 | ApiClient apiClient = null; 128 | try { 129 | apiClient = Config.fromCluster(); 130 | } catch (Exception e) { 131 | log.error("read container token error"); 132 | log.debug("error info", e); 133 | } 134 | Configuration.setDefaultApiClient(apiClient); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/discovery/KubernetesDiscovery.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.discovery; 2 | 3 | import club.mydlq.swagger.kubernetes.param.DiscoveryAutoConfig; 4 | import club.mydlq.swagger.kubernetes.param.SwaggerAutoConfig; 5 | import club.mydlq.swagger.kubernetes.entity.ServiceInfo; 6 | import club.mydlq.swagger.kubernetes.swagger.SwaggerResources; 7 | import club.mydlq.swagger.kubernetes.utils.FileUtils; 8 | import club.mydlq.swagger.kubernetes.utils.HttpUtils; 9 | import club.mydlq.swagger.kubernetes.utils.ValidationUtils; 10 | import club.mydlq.swagger.kubernetes.zuul.RefreshRoute; 11 | import club.mydlq.swagger.kubernetes.zuul.ZuulRouteLocator; 12 | import io.kubernetes.client.openapi.ApiException; 13 | import io.kubernetes.client.openapi.apis.CoreV1Api; 14 | import io.kubernetes.client.openapi.models.*; 15 | import io.kubernetes.client.util.Config; 16 | import lombok.extern.slf4j.Slf4j; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 19 | import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute; 20 | import org.springframework.scheduling.annotation.SchedulingConfigurer; 21 | import org.springframework.scheduling.config.IntervalTask; 22 | import org.springframework.scheduling.config.ScheduledTaskRegistrar; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.Set; 26 | 27 | /** 28 | * Author: mydlq / 小豆丁 29 | * Blog: http://www.mydlq.club 30 | * Github: https://github.com/my-dlq/ 31 | *

32 | * Describe: kubernetes Service Discovery 33 | */ 34 | @Slf4j 35 | public class KubernetesDiscovery implements SchedulingConfigurer { 36 | 37 | static final String PORT_TYPE_NODEPORT = "NodePort"; 38 | 39 | private DiscoveryAutoConfig discoveryAutoConfig; 40 | private SwaggerResources swaggerResourcesProcessor; 41 | private ZuulRouteLocator zuulRouteLocator; 42 | private RefreshRoute refreshRouteService; 43 | private SwaggerAutoConfig swaggerAutoConfig; 44 | private ZuulProperties zuulProperties; 45 | 46 | public KubernetesDiscovery(SwaggerResources swaggerResourcesProcessor, 47 | ZuulRouteLocator zuulRouteLocator, 48 | RefreshRoute refreshRouteService, 49 | SwaggerAutoConfig swaggerAutoConfig, 50 | ZuulProperties zuulProperties, 51 | DiscoveryAutoConfig discoveryAutoConfig) { 52 | this.swaggerResourcesProcessor = swaggerResourcesProcessor; 53 | this.zuulRouteLocator = zuulRouteLocator; 54 | this.refreshRouteService = refreshRouteService; 55 | this.swaggerAutoConfig = swaggerAutoConfig; 56 | this.zuulProperties = zuulProperties; 57 | this.discoveryAutoConfig = discoveryAutoConfig; 58 | } 59 | 60 | /** 61 | * 刷新服务列表 62 | * Refresh service list 63 | */ 64 | public void freshServiceList() { 65 | serviceFresh(); 66 | } 67 | 68 | /** 69 | * 定时任务 70 | * timed task 71 | */ 72 | @Override 73 | public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { 74 | taskRegistrar.addFixedRateTask( 75 | new IntervalTask( 76 | this::serviceFresh, 77 | discoveryAutoConfig.getInterval() * 1000, 78 | discoveryAutoConfig.getInitialDelay() * 1000)); 79 | } 80 | 81 | /** 82 | * 刷新服务列表 83 | * fresh kubernetes service list 84 | */ 85 | private void serviceFresh() { 86 | // 验证 Namespace 是否设置,如果未设置则默认读取集群所在的 Namespace 87 | readNamespace(); 88 | // 获取动态服务列表 89 | List serviceInfos = getServiceInfo(discoveryAutoConfig.getNamespace(), 90 | discoveryAutoConfig.getPortType(), 91 | discoveryAutoConfig.getUrl(), 92 | swaggerAutoConfig.getDocApiPath()); 93 | // 获取静态服务列表 94 | List staticServiceList = getStaticServiceList(); 95 | // 将静态服务列表加入总服务列表 96 | serviceInfos.addAll(staticServiceList); 97 | // 忽略用户指定的服务 98 | excludeService(serviceInfos, swaggerAutoConfig.getIgnoreServices()); 99 | // 刷新 swagger 服务列表 100 | swaggerResourcesProcessor.updateServiceInfos(serviceInfos); 101 | // 刷新 zuul 服务列表 102 | zuulRouteLocator.setServiceInfos(serviceInfos); 103 | refreshRouteService.refreshRoute(); 104 | } 105 | 106 | /** 107 | * 获取静态服务列表 108 | */ 109 | private List getStaticServiceList() { 110 | List serviceInfoList = new ArrayList<>(); 111 | for (ZuulRoute route : zuulProperties.getRoutes().values()) { 112 | ServiceInfo serviceInfo = analysisStaticService(route); 113 | if (serviceInfo != null) { 114 | serviceInfoList.add(serviceInfo); 115 | } 116 | } 117 | return serviceInfoList; 118 | } 119 | 120 | /** 121 | * 分析静态服务 122 | * 123 | * @param route 路由对象 124 | * @return 服务信息 125 | */ 126 | private ServiceInfo analysisStaticService(ZuulRoute route) { 127 | boolean isVerify = true; 128 | ServiceInfo serviceInfo = new ServiceInfo(); 129 | // 非空验证 130 | if (route.getUrl() == null || route.getPath() == null) { 131 | isVerify = false; 132 | } 133 | // 如果不是以 http 或者 https 开始,则默认加上 "http://" 134 | if (isVerify && (!(route.getUrl().startsWith("http") || route.getUrl().startsWith("https")))) { 135 | route.setUrl("http://" + route.getUrl()); 136 | } 137 | // 如果为域名,则默认加上 "80" 端口 138 | if (!ValidationUtils.validatePort(route.getUrl())) { 139 | route.setUrl(route.getUrl() + ":80"); 140 | } 141 | // URL验证 142 | if (!ValidationUtils.validateUrl(route.getUrl())) { 143 | isVerify = false; 144 | } 145 | // 截取 URL,设置Host & Port 146 | String host = StringUtils.substringBeforeLast(route.getUrl(), ":"); 147 | String port = StringUtils.substringAfterLast(route.getUrl(), ":"); 148 | // 检测截取端口是否为数字 149 | if (!StringUtils.isNumeric(port)) { 150 | isVerify = false; 151 | } 152 | if (!isVerify) { 153 | return null; 154 | } 155 | serviceInfo.setHost(host); 156 | serviceInfo.setPort(Integer.parseInt(port)); 157 | // Path验证 158 | if (!route.getPath().startsWith("/")) { 159 | route.setPath("/" + route.getPath()); 160 | } 161 | // 拆分Path,设置Name 162 | String[] paths = route.getPath().split("/"); 163 | serviceInfo.setName(paths[1]); 164 | // 设置SwaggerUrl 165 | serviceInfo.setPath(discoveryAutoConfig.getUrl()); 166 | return serviceInfo; 167 | } 168 | 169 | /** 170 | * 排除设置的不需要发现的服务 171 | * @param serviceInfoList 服务信息列表 172 | * @param excludeServiceList 排除的服务名称列表 173 | */ 174 | private void excludeService(List serviceInfoList, Set excludeServiceList) { 175 | if (excludeServiceList == null) { 176 | return; 177 | } 178 | List excludeServiceInfoList = new ArrayList<>(); 179 | for (String serviceName : excludeServiceList) { 180 | for (ServiceInfo serviceInfo : serviceInfoList) { 181 | if (StringUtils.equalsIgnoreCase(serviceInfo.getName(), serviceName)) { 182 | excludeServiceInfoList.add(serviceInfo); 183 | } 184 | } 185 | } 186 | serviceInfoList.removeAll(excludeServiceInfoList); 187 | } 188 | 189 | /** 190 | * 获取 ServiceInfo 列表 191 | * 192 | * @param namespace 命名空间 193 | * @param host 主机地址 194 | * @param portType 端口类型,支持ClusterIP Or NodePort 195 | * @param swaggerUrl swagger url 地址 196 | * @return ServiceInfo 列表 197 | */ 198 | private static List getServiceInfo(String namespace, String portType, String host, String swaggerUrl) { 199 | if (StringUtils.isEmpty(namespace)) { 200 | throw new NullPointerException("namespace is null!"); 201 | } 202 | List serviceInfos = new ArrayList<>(); 203 | // 从 Kubernetes 集群获取 Service 列表 204 | List serviceList = getKubernetesServiceList(namespace, portType); 205 | // 检测 swagger url 是否以 "/" 开始 206 | if (!swaggerUrl.startsWith("/")) { 207 | swaggerUrl = "/" + swaggerUrl; 208 | } 209 | // 检测接口是否符合要求 210 | for (V1Service service : serviceList) { 211 | // 设置 host 212 | String serviceHost = "http://" + service.getMetadata().getName() + "." + service.getMetadata().getNamespace(); 213 | if (portType.equalsIgnoreCase(PORT_TYPE_NODEPORT)) { 214 | serviceHost = host; 215 | } 216 | // 获取端口列表 217 | Integer[] ports = getPort(service, portType); 218 | // 根据 Port & swaggerUrl 检查地址是否是 Swagger Api 来确定是否加入服务列表 219 | for (Integer port : ports) { 220 | log.debug(serviceHost + ":" + port + swaggerUrl); 221 | ServiceInfo serviceInfo = new ServiceInfo(); 222 | serviceInfo.setName(service.getMetadata().getName()); 223 | serviceInfo.setHost(serviceHost); 224 | serviceInfo.setPort(port); 225 | serviceInfo.setPath(swaggerUrl); 226 | serviceInfos.add(serviceInfo); 227 | } 228 | // 过滤 Service,只保留拥有swagger api的服务 229 | HttpUtils.checkUrl(serviceInfos); 230 | } 231 | return serviceInfos; 232 | } 233 | 234 | 235 | /** 236 | * 获得端口列表 237 | * 238 | * @param service Service 对象 239 | * @param type 端口类型 240 | * @return 端口对象列表 241 | */ 242 | private static Integer[] getPort(V1Service service, String type) { 243 | List ports = new ArrayList<>(); 244 | if (service != null) { 245 | // 获取 Port 列表 246 | List portList = service.getSpec().getPorts(); 247 | for (V1ServicePort port : portList) { 248 | if (StringUtils.equalsIgnoreCase(type, PORT_TYPE_NODEPORT)) { 249 | ports.add(port.getNodePort()); 250 | } else { 251 | ports.add(port.getPort()); 252 | } 253 | } 254 | } 255 | return ports.toArray(new Integer[ports.size()]); 256 | } 257 | 258 | /** 259 | * 从 Kubernetes 中获取 Service 列表 260 | * 261 | * @param namespace 命名空间 262 | * @param portType 端口类型 263 | * @return Service 列表 264 | */ 265 | private static List getKubernetesServiceList(String namespace, String portType) { 266 | // 设置 Api 客户端 267 | CoreV1Api api = new CoreV1Api(); 268 | List kubernetesServiceList = new ArrayList<>(); 269 | V1ServiceList serviceList = null; 270 | V1EndpointsList endPointList = null; 271 | // 获取 ServiceList & EndPointList 272 | try { 273 | serviceList = api.listNamespacedService(namespace, null, null, null, null, null, null, null, null, null,null); 274 | endPointList = api.listNamespacedEndpoints(namespace, null, null, null, null, null, null, null, null, null,null); 275 | } catch (ApiException e) { 276 | log.error(e.getMessage()); 277 | } 278 | // 检测 Service 是否包含 Endpoints,如果端口类型为 NodePort,则检测端口类型 279 | if (endPointList != null && serviceList != null) { 280 | for (V1Service service : serviceList.getItems()) { 281 | if (!isContainEndpoints(endPointList, service.getMetadata().getName()) 282 | || StringUtils.equalsIgnoreCase(portType, PORT_TYPE_NODEPORT) 283 | && !StringUtils.equalsIgnoreCase(service.getSpec().getType(), portType)) { 284 | continue; 285 | } 286 | kubernetesServiceList.add(service); 287 | } 288 | } 289 | return kubernetesServiceList; 290 | } 291 | 292 | 293 | /** 294 | * 检测 Service 中是否包含 Endpoints 295 | * 296 | * @param endpointList Endpoints 列表 297 | * @param serviceName Service 名称 298 | * @return Service 中是否包含 Endpoints 299 | */ 300 | private static boolean isContainEndpoints(V1EndpointsList endpointList, String serviceName) { 301 | if (endpointList.getItems() != null) { 302 | for (V1Endpoints endpoints : endpointList.getItems()) { 303 | if (StringUtils.equalsIgnoreCase(endpoints.getMetadata().getName(), serviceName)) { 304 | return true; 305 | } 306 | } 307 | } 308 | return false; 309 | } 310 | 311 | /** 312 | * 读取容器所在的 Namespace 313 | * Read the Namespace where the container is located 314 | */ 315 | private void readNamespace() { 316 | if (StringUtils.isEmpty(discoveryAutoConfig.getNamespace())) { 317 | String namespace = FileUtils.readFile(Config.SERVICEACCOUNT_ROOT + "/namespace"); 318 | discoveryAutoConfig.setNamespace(namespace); 319 | log.info("read namespace " + namespace); 320 | } 321 | } 322 | 323 | } -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/entity/ServiceInfo.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.entity; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString 8 | public class ServiceInfo { 9 | private String name; 10 | private String host; 11 | private Integer port; 12 | private String path; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/param/DiscoveryAutoConfig.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.param; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties("swagger.discovery") 8 | public class DiscoveryAutoConfig { 9 | 10 | // Discovery interval 11 | private long interval = 30; 12 | 13 | // Discovery initial delay 14 | private long initialDelay = 30; 15 | 16 | // Discovery kubernetes url 17 | private String url = ""; 18 | 19 | // Discovery kubernetes namespace 20 | private String namespace = ""; 21 | 22 | // Discovery kubernetes port type 23 | private String portType = "ClusterIP"; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/param/KubernetesAutoConfig.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.param; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties("swagger.kubernetes.connect") 8 | public class KubernetesAutoConfig { 9 | 10 | /** Kubernetes connection from cluster */ 11 | private boolean fromCluster = true; 12 | 13 | /** Kubernetes url */ 14 | private String url = ""; 15 | 16 | /** Kubernetes authentication token */ 17 | private String token = ""; 18 | 19 | /** Kubernetes token file path */ 20 | private String tokenPath = ""; 21 | 22 | /** Validate SSL certificate */ 23 | boolean validateSsl = false; 24 | 25 | /** Kubernetes ssl ca file path */ 26 | private String caPath = ""; 27 | 28 | /** Kubernetes config file path */ 29 | private String fromConfigPath = ""; 30 | 31 | /** Whether to find a file from the default configuration address ($HOME/.kube/config) */ 32 | private boolean fromDefault = true; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/param/SwaggerAutoConfig.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.param; 2 | 3 | import lombok.Data; 4 | import java.util.Set; 5 | import java.util.HashSet; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | 8 | @Data 9 | @ConfigurationProperties("swagger.global") 10 | public class SwaggerAutoConfig { 11 | 12 | /** 13 | * Global swagger docs api path 14 | */ 15 | private String docApiPath; 16 | /** 17 | * Global Swagger docs version 18 | */ 19 | private String swaggerVersion = "2.0"; 20 | 21 | /** 22 | * Ignore service list 23 | */ 24 | private Set ignoreServices = new HashSet<>(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/swagger/MvcController.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.swagger; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class MvcController { 8 | 9 | @GetMapping("/") 10 | public String root(){ 11 | return "/doc.html"; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/swagger/SwaggerResources.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.swagger; 2 | 3 | import club.mydlq.swagger.kubernetes.param.SwaggerAutoConfig; 4 | import club.mydlq.swagger.kubernetes.entity.ServiceInfo; 5 | import springfox.documentation.swagger.web.SwaggerResource; 6 | import springfox.documentation.swagger.web.SwaggerResourcesProvider; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * @author mydlq 12 | */ 13 | public class SwaggerResources implements SwaggerResourcesProvider { 14 | 15 | // serviceInfo list 16 | List serviceInfos = new ArrayList<>(); 17 | private SwaggerAutoConfig swaggerAutoConfig; 18 | 19 | public SwaggerResources(SwaggerAutoConfig swaggerAutoConfig) { 20 | this.swaggerAutoConfig = swaggerAutoConfig; 21 | } 22 | 23 | /** 24 | * 更新服务列表 25 | * Update service list 26 | * 27 | * @param serviceInfos 28 | */ 29 | public void updateServiceInfos(List serviceInfos) { 30 | this.serviceInfos = serviceInfos; 31 | } 32 | 33 | /** 34 | * 增加 SwaggerResource 对象到 swagger 列表 35 | * 36 | * @return 37 | */ 38 | @Override 39 | public List get() { 40 | List resources = new ArrayList<>(); 41 | for (ServiceInfo serviceInfo : serviceInfos) { 42 | resources.add(swaggerResource(serviceInfo.getName(), 43 | "/" + serviceInfo.getName() + swaggerAutoConfig.getDocApiPath(), 44 | swaggerAutoConfig.getSwaggerVersion())); 45 | } 46 | return resources; 47 | } 48 | 49 | 50 | /** 51 | * 创建 SwaggerResource 对象 52 | * 53 | * @param name 54 | * @param location 55 | * @param swaggerVersion 56 | * @return 57 | */ 58 | private SwaggerResource swaggerResource(String name, String location, String swaggerVersion) { 59 | SwaggerResource swaggerResource = new SwaggerResource(); 60 | swaggerResource.setName(name); 61 | swaggerResource.setLocation(location); 62 | swaggerResource.setSwaggerVersion(swaggerVersion); 63 | return swaggerResource; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/swagger/SwaggerUIMvcConfig.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.swagger; 2 | 3 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 4 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 5 | 6 | /** 7 | * Author: mydlq / 小豆丁 8 | * Blog: http://www.mydlq.club 9 | * Github: https://github.com/my-dlq/ 10 | * 11 | * Describe: Replace the CSS file for the Swagger Ui 12 | */ 13 | public class SwaggerUIMvcConfig implements WebMvcConfigurer { 14 | 15 | @Override 16 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 17 | registry.addResourceHandler("/webjars/bycdao-ui/cdao/**").addResourceLocations("classpath:/ui/"); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.utils; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.io.*; 6 | import java.nio.charset.StandardCharsets; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | 11 | @Slf4j 12 | public class FileUtils { 13 | 14 | private FileUtils() { 15 | throw new IllegalStateException("Utility class"); 16 | } 17 | 18 | /** 19 | * 读取文件 20 | * read file 21 | * 22 | * @return 读取的文本内容 23 | */ 24 | public static String readFile(String fileName) { 25 | Path path = Paths.get(fileName); 26 | try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { 27 | return reader.readLine(); 28 | } catch (IOException e) { 29 | log.error(e.getMessage()); 30 | } 31 | return null; 32 | } 33 | 34 | /** 35 | * 检查文件是否存在 36 | * 37 | * @return 检查结果 38 | */ 39 | public static boolean checkFolderExist(String path){ 40 | File file = new File(path); 41 | return file.exists(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/utils/HttpUtils.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.utils; 2 | 3 | import club.mydlq.swagger.kubernetes.entity.ServiceInfo; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.http.HttpResponse; 6 | import org.apache.http.client.config.RequestConfig; 7 | import org.apache.http.client.methods.HttpGet; 8 | import org.apache.http.impl.client.CloseableHttpClient; 9 | import org.apache.http.impl.client.HttpClients; 10 | import org.apache.http.util.EntityUtils; 11 | 12 | import java.io.IOException; 13 | import java.net.URI; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | * Http 请求工具 19 | * Http Request Tool 20 | */ 21 | @Slf4j 22 | public class HttpUtils { 23 | 24 | private HttpUtils() { 25 | } 26 | 27 | // Timeout Setting 28 | private static final int TIMEOUT_CONNECT = 200; 29 | private static final int TIMEOUT_CONNECT_REQUEST = 200; 30 | private static final int TIMEOUT_SOCKET = 200; 31 | 32 | /** 33 | * 验证 uri 是否为 Swagger Api URL 34 | * Verify that URI is Swagger Api URL 35 | * 36 | * @param serviceInfos 服务信息 37 | */ 38 | public static void checkUrl(List serviceInfos) { 39 | List newServiceInfos = new ArrayList<>(); 40 | // HttpClient 客户端 41 | CloseableHttpClient httpCilent = null; 42 | HttpGet httpGet = null; 43 | try { 44 | httpCilent = HttpClients.createDefault(); 45 | // 过滤 URL 46 | for (ServiceInfo serviceInfo : serviceInfos) { 47 | // get request 48 | httpGet = createHttpGet(serviceInfo.getHost() + ":" + 49 | serviceInfo.getPort() + serviceInfo.getPath()); 50 | // get result 51 | String result = getHttpRequestResult(httpCilent, httpGet); 52 | if (!ValidationUtils.isSwagger(result)) { 53 | newServiceInfos.add(serviceInfo); 54 | } 55 | } 56 | // 关闭 httpclient 57 | httpCilent.close(); 58 | } catch (IOException e) { 59 | log.error("Close HttpClient Excepiton", e); 60 | } 61 | // 将非 swagger api 移除 62 | serviceInfos.removeAll(newServiceInfos); 63 | } 64 | 65 | /** 66 | * 执行 HTTP 请求,获取响应结果 67 | * Execute HTTP requests to obtain response results. 68 | * 69 | * @param httpGet Http Get 请求 70 | * @return 响应结果 71 | */ 72 | private static String getHttpRequestResult(CloseableHttpClient httpClient, HttpGet httpGet) { 73 | String result = null; 74 | try { 75 | HttpResponse httpResponse = httpClient.execute(httpGet); 76 | if (httpResponse.getStatusLine().getStatusCode() == 200) { 77 | result = EntityUtils.toString(httpResponse.getEntity()); 78 | } 79 | } catch (IOException e) { 80 | return result; 81 | } 82 | return result; 83 | } 84 | 85 | /** 86 | * 创建 HttpGet 请求对象 87 | * Create HttpGet request object. 88 | * 89 | * @param uri 请求地址 90 | * @return Http Get 请求对象 91 | */ 92 | private static HttpGet createHttpGet(String uri) { 93 | // 设置 RequestConfig 94 | RequestConfig requestConfig = RequestConfig.custom() 95 | //设置连接超时时间 96 | .setConnectTimeout(TIMEOUT_CONNECT) 97 | //设置请求超时时间 98 | .setConnectionRequestTimeout(TIMEOUT_CONNECT_REQUEST) 99 | //设置Socket超时时间 100 | .setSocketTimeout(TIMEOUT_SOCKET) 101 | //默认允许自动重定向 102 | .setRedirectsEnabled(false) 103 | .build(); 104 | HttpGet httpGet = new HttpGet(); 105 | httpGet.setURI(URI.create(uri)); 106 | httpGet.setConfig(requestConfig); 107 | return httpGet; 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/utils/ValidationUtils.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.utils; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonParser; 5 | import org.apache.commons.lang3.StringUtils; 6 | import java.util.regex.Pattern; 7 | 8 | public class ValidationUtils { 9 | 10 | private ValidationUtils() { 11 | } 12 | 13 | static final String REGEX_URL = "^(?:https?://)?[\\w]{1,}(?:\\.?[\\w]{1,})+[\\w-_/?&=#%:]*$"; 14 | static final String REGEX_URL_PORT = "^\\S*:[0-9]+$"; 15 | 16 | public static boolean validateUrl(String str) { 17 | return Pattern.matches(REGEX_URL, str); 18 | } 19 | 20 | /** 21 | * 验证 url 字符串中是否存在端口 22 | * 23 | * @param str str 待验证字符串 24 | * @return 如果返回 true 则表示是一个带端口的 url,否则返回 false 则不是 url 或者不带端口。 25 | */ 26 | public static boolean validatePort(String str) { 27 | return validateUrl(str) && Pattern.matches(REGEX_URL_PORT, str); 28 | } 29 | 30 | /** 31 | * 验证 String 是否为 Swagger Api 32 | * 33 | * @param jsonStr 待验证的 Json 字符串 34 | * @return 如果返回 true 则表示是一个 json 串,否则返回 false 则表示不是 json 串。 35 | */ 36 | public static boolean isSwagger(String jsonStr) { 37 | try { 38 | JsonElement jsonElement = JsonParser.parseString(jsonStr); 39 | String swaggerStr = jsonElement.getAsJsonObject().get("swagger").toString(); 40 | return StringUtils.isNotEmpty(swaggerStr); 41 | } catch (Exception e) { 42 | return false; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/zuul/RefreshRoute.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.zuul; 2 | 3 | import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent; 4 | import org.springframework.cloud.netflix.zuul.filters.RouteLocator; 5 | import org.springframework.context.ApplicationEventPublisher; 6 | import org.springframework.context.ApplicationEventPublisherAware; 7 | 8 | /** 9 | * Refresh {@code RouteLocator} endpoint. 10 | * @author mydlq 11 | * @author WeiX Sun 12 | * 13 | * @see org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener 14 | */ 15 | public class RefreshRoute implements ApplicationEventPublisherAware { 16 | 17 | private ApplicationEventPublisher publisher; 18 | 19 | private RouteLocator routeLocator; 20 | 21 | public RefreshRoute(RouteLocator routeLocator) { 22 | this.routeLocator = routeLocator; 23 | } 24 | 25 | @Override 26 | public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { 27 | this.publisher = publisher; 28 | } 29 | 30 | public void refreshRoute() { 31 | publisher.publishEvent(new RoutesRefreshedEvent(this.routeLocator)); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/club/mydlq/swagger/kubernetes/zuul/ZuulRouteLocator.java: -------------------------------------------------------------------------------- 1 | package club.mydlq.swagger.kubernetes.zuul; 2 | 3 | import club.mydlq.swagger.kubernetes.entity.ServiceInfo; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator; 6 | import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator; 7 | import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 8 | import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute; 9 | import java.util.ArrayList; 10 | import java.util.LinkedHashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | /** 15 | * Author: mydlq / 小豆丁 16 | * Blog: http://www.mydlq.club 17 | * Github: https://github.com/my-dlq/ 18 | * 19 | * Describe: Zuul routing management 20 | */ 21 | @Slf4j 22 | public class ZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator { 23 | 24 | private List serviceInfos = new ArrayList<>(); 25 | 26 | public ZuulRouteLocator(String servletPath, ZuulProperties properties) { 27 | super(servletPath, properties); 28 | } 29 | 30 | /** 31 | * 更新服务列表 32 | * Update service list 33 | * 34 | * @param serviceInfos 35 | */ 36 | public void setServiceInfos(List serviceInfos) { 37 | this.serviceInfos = serviceInfos; 38 | } 39 | 40 | /** 41 | * 加载 Kubernetes 服务列表 42 | * Load the Kubernetes service list 43 | * 44 | * @return 45 | */ 46 | private Map locateRoutesFromKubernetes() { 47 | Map routes = new LinkedHashMap<>(); 48 | try { 49 | for (ServiceInfo serviceInfo : serviceInfos) { 50 | ZuulRoute zuulRoute = new ZuulRoute(); 51 | zuulRoute.setId(serviceInfo.getName()); 52 | zuulRoute.setUrl(serviceInfo.getHost() + ":" + serviceInfo.getPort()); 53 | zuulRoute.setPath("/" + serviceInfo.getName() + "/**"); 54 | zuulRoute.setServiceId("/" + serviceInfo.getName() + "/**"); 55 | routes.put("/" + serviceInfo.getName() + "/**", zuulRoute); 56 | } 57 | } catch (Exception e) { 58 | log.error("update zuul service list error!",e); 59 | } 60 | return routes; 61 | } 62 | 63 | /** 64 | * 重写 Zuul 路由策略 65 | * Rewrite Zuul routing policy 66 | * 67 | * @return 68 | */ 69 | @Override 70 | protected LinkedHashMap locateRoutes() { 71 | LinkedHashMap routesMap = new LinkedHashMap<>(); 72 | /** ----------------------Loads custom routing information---------------------- **/ 73 | routesMap.putAll(locateRoutesFromKubernetes()); 74 | LinkedHashMap values = new LinkedHashMap<>(); 75 | for (Map.Entry entry : routesMap.entrySet()) { 76 | String path = entry.getKey(); 77 | values.put(path, entry.getValue()); 78 | } 79 | return values; 80 | } 81 | 82 | @Override 83 | public void refresh() { 84 | doRefresh(); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | #server config 2 | server: 3 | port: 8080 4 | tomcat: 5 | basedir: "/" 6 | 7 | #spring config 8 | spring: 9 | application: 10 | name: swagger-kubernetes 11 | 12 | #swagger kubernetes config 13 | swagger: 14 | kubernetes: 15 | connect: 16 | fromCluster: ${KUBERNETES_CONNECT_FROM_CLUSTER:true} 17 | url: ${KUBERNETES_CONNECT_URL:https://kubernetes.default.svc.cluster.local} 18 | token: ${KUBERNETES_CONNECT_TOKEN:} 19 | discovery: 20 | namespace: ${DISCOVERY_NAMESPACE:} 21 | interval: ${DISCOVERY_INITIAL_INTERVAL:60} 22 | url: ${DISCOVERY_URL:} 23 | portType: ${DISCOVERY_PORT_TYPE:ClusterIP} 24 | global: 25 | docApiPath: ${SWGGER_API_PATH:/v2/api-docs} 26 | ignoreServices: swagger-kubernetes,${IGNORE_SERVICES:} 27 | 28 | #manager 29 | management: 30 | server: 31 | port: ${ACTUATOR_PORT:${server.port}} 32 | endpoints: 33 | web: 34 | exposure: 35 | include: ${ACTUATOR_TYPE:*} 36 | -------------------------------------------------------------------------------- /src/main/resources/ui/swaggerbootstrapui-md.css: -------------------------------------------------------------------------------- 1 | 2 | .othermarkdown { 3 | margin: 0px auto; 4 | height: auto; 5 | padding: 30px; 6 | width: inherit; 7 | word-break: normal; 8 | word-wrap: break-word; 9 | position: relative; 10 | white-space: normal; 11 | overflow-x: visible; 12 | padding-top: 40px; 13 | font-family: Monaco, "Source Han Sans SC", sans-serif; 14 | color: rgb(85, 85, 85); 15 | background: rgb(255, 255, 255); 16 | } 17 | 18 | .othermarkdown.first-line-indent p { 19 | text-indent: 2em; 20 | } 21 | 22 | .othermarkdown.first-line-indent li p, .othermarkdown.first-line-indent p * { 23 | text-indent: 0px; 24 | } 25 | 26 | .othermarkdown.first-line-indent li { 27 | margin-left: 2em; 28 | } 29 | 30 | .for-image .othermarkdown { 31 | padding-left: 8px; 32 | padding-right: 8px; 33 | } 34 | 35 | body.typora-export { 36 | padding-left: 30px; 37 | padding-right: 30px; 38 | } 39 | 40 | .typora-export .footnote-line, .typora-export li, .typora-export p { 41 | white-space: pre-wrap; 42 | } 43 | 44 | 45 | .othermarkdown li > figure:first-child { 46 | margin-top: -20px; 47 | } 48 | 49 | .othermarkdown ol, .othermarkdown ul { 50 | position: relative; 51 | } 52 | 53 | .othermarkdown ul ol { 54 | padding-left: 2em; 55 | } 56 | 57 | .othermarkdown ul { 58 | display: block; 59 | list-style-type: disc; 60 | margin-block-start: 1em; 61 | margin-block-end: 1em; 62 | margin-inline-start: 0px; 63 | margin-inline-end: 0px; 64 | padding-inline-start: 40px; 65 | } 66 | 67 | .othermarkdown img { 68 | max-width: 100%; 69 | vertical-align: middle; 70 | } 71 | 72 | .othermarkdown button, input, select, textarea { 73 | color: inherit; 74 | font-style: inherit; 75 | font-variant: inherit; 76 | font-weight: inherit; 77 | font-stretch: inherit; 78 | font-size: inherit; 79 | line-height: inherit; 80 | font-family: inherit; 81 | } 82 | 83 | .othermarkdown input[type="checkbox"], input[type="radio"] { 84 | line-height: normal; 85 | padding: 0px; 86 | } 87 | 88 | 89 | .othermarkdown h1, .othermarkdown h2, .othermarkdown h3, .othermarkdown h4, .othermarkdown h5, .othermarkdown h6, .othermarkdown p, .othermarkdown pre { 90 | width: inherit; 91 | } 92 | 93 | .othermarkdown h1, .othermarkdown h2, .othermarkdown h3, .othermarkdown h4, .othermarkdown h5, .othermarkdown h6, .othermarkdown p { 94 | position: relative; 95 | } 96 | 97 | .othermarkdown h1, h2, h3, h4, h5, h6 { 98 | break-after: avoid-page; 99 | break-inside: avoid; 100 | orphans: 2; 101 | } 102 | 103 | .othermarkdown p { 104 | orphans: 4; 105 | padding: 5px; 106 | } 107 | 108 | .othermarkdown strong, b { 109 | font-weight: bold; 110 | } 111 | 112 | .othermarkdown h1 { 113 | font-size: 2rem; 114 | } 115 | 116 | .othermarkdown h2 { 117 | font-size: 1.8rem; 118 | } 119 | 120 | .othermarkdown h3 { 121 | font-size: 1.6rem; 122 | } 123 | 124 | .othermarkdown h4 { 125 | font-size: 1.4rem; 126 | } 127 | 128 | .othermarkdown h5 { 129 | font-size: 1.2rem; 130 | } 131 | 132 | .othermarkdown h6 { 133 | font-size: 1rem; 134 | } 135 | 136 | 137 | .othermarkdown a { 138 | cursor: pointer; 139 | } 140 | 141 | 142 | .othermarkdown input[type="checkbox"] { 143 | cursor: pointer; 144 | width: inherit; 145 | height: inherit; 146 | } 147 | 148 | .othermarkdown figure { 149 | overflow-x: auto; 150 | margin: 1.2em 0px; 151 | max-width: calc(100% + 16px); 152 | padding: 0px; 153 | } 154 | 155 | .othermarkdown figure > table { 156 | margin: 0px !important; 157 | } 158 | 159 | .othermarkdown tr { 160 | break-inside: avoid; 161 | break-after: auto; 162 | } 163 | 164 | .othermarkdown thead { 165 | display: table-header-group; 166 | border: 1px solid rgb(221, 221, 221); 167 | } 168 | 169 | .othermarkdown table { 170 | border-collapse: collapse; 171 | border-spacing: 0px; 172 | width: 100%; 173 | overflow: auto; 174 | break-inside: auto; 175 | text-align: left; 176 | } 177 | 178 | .othermarkdown table.md-table td { 179 | min-width: 32px; 180 | } 181 | 182 | .CodeMirror-gutters { 183 | border-right: 0px; 184 | background-color: inherit; 185 | } 186 | 187 | .CodeMirror-linenumber { 188 | user-select: none; 189 | } 190 | 191 | .CodeMirror { 192 | text-align: left; 193 | } 194 | 195 | .CodeMirror-placeholder { 196 | opacity: 0.3; 197 | } 198 | 199 | .CodeMirror pre { 200 | padding: 0px 4px; 201 | } 202 | 203 | .CodeMirror-lines { 204 | padding: 0px; 205 | } 206 | 207 | .othermarkdown div.hr:focus { 208 | cursor: none; 209 | } 210 | 211 | .othermarkdown pre { 212 | white-space: pre-wrap; 213 | } 214 | 215 | .othermarkdown.fences-no-line-wrapping pre { 216 | white-space: pre; 217 | } 218 | 219 | .othermarkdown pre.ty-contain-cm { 220 | white-space: normal; 221 | } 222 | 223 | .CodeMirror-gutters { 224 | margin-right: 4px; 225 | } 226 | 227 | .othermarkdown .md-fences { 228 | font-size: 0.9rem; 229 | display: block; 230 | break-inside: avoid; 231 | text-align: left; 232 | overflow: visible; 233 | white-space: pre; 234 | background: inherit; 235 | position: relative !important; 236 | } 237 | 238 | .othermarkdown .md-diagram-panel { 239 | width: 100%; 240 | margin-top: 10px; 241 | text-align: center; 242 | padding-top: 0px; 243 | padding-bottom: 8px; 244 | overflow-x: auto; 245 | } 246 | 247 | .othermarkdown .md-fences.mock-cm { 248 | white-space: pre-wrap; 249 | } 250 | 251 | .othermarkdown .md-fences.md-fences-with-lineno { 252 | padding-left: 0px; 253 | } 254 | 255 | .othermarkdown.fences-no-line-wrapping .md-fences.mock-cm { 256 | white-space: pre; 257 | overflow-x: auto; 258 | } 259 | 260 | .othermarkdown .md-fences.mock-cm.md-fences-with-lineno { 261 | padding-left: 8px; 262 | } 263 | 264 | .othermarkdown .CodeMirror-line, twitterwidget { 265 | break-inside: avoid; 266 | } 267 | 268 | .othermarkdown .footnotes { 269 | opacity: 0.8; 270 | font-size: 0.9rem; 271 | margin-top: 1em; 272 | margin-bottom: 1em; 273 | } 274 | 275 | .othermarkdown .footnotes + .footnotes { 276 | margin-top: 0px; 277 | } 278 | 279 | .othermarkdown .md-reset { 280 | margin: 0px; 281 | padding: 0px; 282 | border: 0px; 283 | outline: 0px; 284 | vertical-align: top; 285 | background: 0px 0px; 286 | text-decoration: none; 287 | text-shadow: none; 288 | float: none; 289 | position: static; 290 | width: auto; 291 | height: auto; 292 | white-space: nowrap; 293 | cursor: inherit; 294 | -webkit-tap-highlight-color: transparent; 295 | line-height: normal; 296 | font-weight: 400; 297 | text-align: left; 298 | box-sizing: content-box; 299 | direction: ltr; 300 | } 301 | 302 | .othermarkdown li div { 303 | padding-top: 0px; 304 | } 305 | 306 | .othermarkdown blockquote { 307 | margin: 1rem 0px; 308 | } 309 | 310 | .othermarkdown li .mathjax-block, li p { 311 | margin: 0.5rem 0px; 312 | } 313 | 314 | .othermarkdown li { 315 | margin: 0px; 316 | position: relative; 317 | list-style: disc; 318 | padding: 2px; 319 | } 320 | 321 | 322 | .othermarkdown blockquote > :last-child { 323 | margin-bottom: 0px; 324 | } 325 | 326 | .othermarkdown blockquote > :first-child, li > :first-child { 327 | margin-top: 0px; 328 | } 329 | 330 | .othermarkdown .footnotes-area { 331 | color: rgb(136, 136, 136); 332 | margin-top: 0.714rem; 333 | padding-bottom: 0.143rem; 334 | white-space: normal; 335 | } 336 | 337 | .othermarkdown .footnote-line { 338 | white-space: pre-wrap; 339 | } 340 | 341 | 342 | .othermarkdown .footnote-line { 343 | margin-top: 0.714em; 344 | font-size: 0.7em; 345 | } 346 | 347 | .othermarkdown a img, img a { 348 | cursor: pointer; 349 | } 350 | 351 | .othermarkdown pre.md-meta-block { 352 | font-size: 0.8rem; 353 | min-height: 0.8rem; 354 | white-space: pre-wrap; 355 | background: rgb(204, 204, 204); 356 | display: block; 357 | overflow-x: hidden; 358 | } 359 | 360 | .othermarkdown p > .md-image:only-child:not(.md-img-error) img, p > img:only-child { 361 | display: block; 362 | margin: auto; 363 | } 364 | 365 | .othermarkdown p > .md-image:only-child { 366 | display: inline-block; 367 | width: 100%; 368 | } 369 | 370 | .othermarkdown .MathJax_Display { 371 | margin: 0.8em 0px 0px; 372 | } 373 | 374 | .othermarkdown .md-math-block { 375 | width: 100%; 376 | } 377 | 378 | 379 | .othermarkdown .md-task-list-item { 380 | position: relative; 381 | list-style-type: none; 382 | } 383 | 384 | .othermarkdown .task-list-item.md-task-list-item { 385 | padding-left: 0px; 386 | } 387 | 388 | .othermarkdown .md-task-list-item > input { 389 | position: absolute; 390 | top: 0px; 391 | left: 0px; 392 | margin-left: -1.2em; 393 | margin-top: calc(1em - 10px); 394 | border: none; 395 | } 396 | 397 | .othermarkdown .math { 398 | font-size: 1rem; 399 | } 400 | 401 | .othermarkdown .md-toc { 402 | min-height: 3.58rem; 403 | position: relative; 404 | font-size: 0.9rem; 405 | border-radius: 10px; 406 | } 407 | 408 | .othermarkdown .md-toc-content { 409 | position: relative; 410 | margin-left: 0px; 411 | } 412 | 413 | .othermarkdown .md-toc-content::after, .md-toc::after { 414 | display: none; 415 | } 416 | 417 | .othermarkdown .md-toc-item { 418 | display: block; 419 | color: rgb(65, 131, 196); 420 | } 421 | 422 | 423 | .othermarkdown code, pre, samp, tt { 424 | font-family: var(--monospace); 425 | } 426 | 427 | .othermarkdown kbd { 428 | margin: 0px 0.1em; 429 | padding: 0.1em 0.6em; 430 | font-size: 0.8em; 431 | color: rgb(36, 39, 41); 432 | background: rgb(255, 255, 255); 433 | border: 1px solid rgb(173, 179, 185); 434 | border-radius: 3px; 435 | box-shadow: rgba(12, 13, 14, 0.2) 0px 1px 0px, rgb(255, 255, 255) 0px 0px 0px 2px inset; 436 | white-space: nowrap; 437 | vertical-align: middle; 438 | } 439 | 440 | .othermarkdown .md-comment { 441 | color: rgb(162, 127, 3); 442 | opacity: 0.8; 443 | font-family: var(--monospace); 444 | } 445 | 446 | .othermarkdown code { 447 | text-align: left; 448 | vertical-align: initial; 449 | } 450 | 451 | .othermarkdown a.md-print-anchor { 452 | white-space: pre !important; 453 | border-width: initial !important; 454 | border-style: none !important; 455 | border-color: initial !important; 456 | display: inline-block !important; 457 | position: absolute !important; 458 | width: 1px !important; 459 | right: 0px !important; 460 | outline: 0px !important; 461 | background: 0px 0px !important; 462 | text-decoration: initial !important; 463 | text-shadow: initial !important; 464 | } 465 | 466 | .othermarkdown .md-inline-math .MathJax_SVG .noError { 467 | display: none !important; 468 | } 469 | 470 | .othermarkdown .html-for-mac .inline-math-svg .MathJax_SVG { 471 | vertical-align: 0.2px; 472 | } 473 | 474 | .othermarkdown .md-math-block .MathJax_SVG_Display { 475 | text-align: center; 476 | margin: 0px; 477 | position: relative; 478 | text-indent: 0px; 479 | max-width: none; 480 | max-height: none; 481 | min-height: 0px; 482 | min-width: 100%; 483 | width: auto; 484 | overflow-y: hidden; 485 | display: block !important; 486 | } 487 | 488 | 489 | .othermarkdown table tr th { 490 | border: 1px solid rgb(221, 221, 221); 491 | } 492 | 493 | .othermarkdown video { 494 | max-width: 100%; 495 | display: block; 496 | margin: 0px auto; 497 | } 498 | 499 | .othermarkdown iframe { 500 | max-width: 100%; 501 | width: 100%; 502 | border: none; 503 | } 504 | 505 | .othermarkdown .highlight td, .highlight tr { 506 | border: 0px; 507 | } 508 | 509 | .othermarkdown .CodeMirror { 510 | height: auto; 511 | } 512 | 513 | .othermarkdown .CodeMirror.cm-s-inner { 514 | background: inherit; 515 | } 516 | 517 | .othermarkdown .CodeMirror-scroll { 518 | overflow-y: hidden; 519 | overflow-x: auto; 520 | z-index: 3; 521 | } 522 | 523 | 524 | .othermarkdown table { 525 | width: 100%; 526 | border-collapse: collapse; 527 | border: 1px solid rgb(221, 221, 221); 528 | border-spacing: 0px; 529 | } 530 | 531 | .othermarkdown table th { 532 | font-weight: bold; 533 | border: 1px solid rgb(221, 221, 221); 534 | padding-bottom: 0.5em; 535 | } 536 | 537 | .othermarkdown table td { 538 | border-bottom: 1px solid rgb(221, 221, 221); 539 | padding: 10px 0px; 540 | } 541 | 542 | .othermarkdown blockquote { 543 | border-left: 5px solid rgb(221, 221, 221); 544 | padding-left: 0.5em; 545 | font-family: "Source Han Serif SC", serif; 546 | color: rgb(85, 85, 85); 547 | } 548 | 549 | .othermarkdown blockquote blockquote { 550 | padding-right: 0px; 551 | } 552 | 553 | .othermarkdown .md-fences { 554 | margin: 0px 0.3em; 555 | padding: 0px 0.3em; 556 | background: rgb(238, 238, 238); 557 | font-family: mononoki, monospace; 558 | text-shadow: rgb(255, 255, 255) 0px 1px; 559 | } 560 | 561 | .othermarkdown tt { 562 | margin: 0px 0.3em; 563 | padding: 0px 0.3em; 564 | background: rgb(238, 238, 238); 565 | font-family: mononoki, monospace; 566 | text-shadow: rgb(255, 255, 255) 0px 1px; 567 | } 568 | 569 | .othermarkdown code { 570 | margin: 0px 0.3em; 571 | padding: 0px 0.3em; 572 | background: rgb(238, 238, 238); 573 | font-family: mononoki, monospace; 574 | text-shadow: rgb(255, 255, 255) 0px 1px; 575 | } 576 | 577 | /*自己追加*/ 578 | pre { 579 | font-family: "Source Code Pro", Monaco, Menlo, Consolas, monospace; 580 | color: #4eec00f0; 581 | background: #1f1c1c; 582 | font-size: 16px; 583 | } 584 | 585 | code { 586 | color: red; 587 | font-size: 16px; 588 | } 589 | 590 | .BlogAnchor li .item_h2 { 591 | font-size: 16px; 592 | color: #089c00; 593 | } 594 | 595 | .BlogAnchor li .item_h1 { 596 | font-size: 22px; 597 | color: #ff5400; 598 | } 599 | 600 | 601 | /*额外添加*/ 602 | .swu-menu { 603 | display: block; 604 | width: 54px; 605 | white-space: nowrap; 606 | font-family: Monaco; 607 | /* font-weight: bold; */ 608 | font-size: 14px; 609 | } 610 | 611 | .layui-layout-admin .layui-body .layui-tab .layui-tab-title li.layui-this, .layui-layout-admin .layui-body .layui-tab .layui-tab-title li:hover { 612 | background: red; 613 | } 614 | 615 | .offlineMarkdownShow table tr th, table tr td { 616 | color: #003665; 617 | } 618 | 619 | .layui-table td, .layui-table th { 620 | font-size: 16px; 621 | } 622 | 623 | .sbu-header { 624 | background-color: #193248; 625 | } 626 | 627 | /*后续增加*/ 628 | 629 | .layui-layout-admin .layui-body .layui-tab .layui-tab-title li.layui-this, .layui-layout-admin .layui-body .layui-tab .layui-tab-title li:hover { 630 | background-color: #3a9e1f; 631 | color: #ffffff; 632 | font-family: Microsoft YaHei; 633 | } 634 | 635 | .layui-layout-admin .layui-body .layui-tab .layui-tab-title li:hover { 636 | background-color: rgba(62, 174, 31, 0.64); 637 | } 638 | 639 | /**stable**/ 640 | .swbu-main table th { 641 | font-weight: bold; 642 | color: #ffffff; 643 | background: #3c763d; 644 | } 645 | 646 | .swbu-main { 647 | font-size: 15px; 648 | } 649 | 650 | .offlineMarkdownShow h1 { 651 | color: #ff5400; 652 | margin: 15px 0px 15px 0px; 653 | } 654 | 655 | .offlineMarkdownShow h2 { 656 | font-size: 28px; 657 | font-weight: bold; 658 | margin: 15px 0px 15px 0px; 659 | } 660 | 661 | .layui-tab-content{ 662 | height: 500px; 663 | } -------------------------------------------------------------------------------- /src/main/resources/ui/swaggerbootstrapui.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | .sbu-header{ 5 | height: 10%; 6 | background-color: #193248; 7 | color: white; 8 | line-height: 60px; 9 | vertical-align: middle; 10 | max-height: 60px; 11 | min-height: 60px; 12 | } 13 | .sbu-header-left{ 14 | float: left; 15 | } 16 | .sbu-header-left-wd{ 17 | width: 310px; 18 | } 19 | .sbu-header-left span{ 20 | font-size: 1.5em; 21 | font-weight: 700; 22 | -webkit-box-flex: 1; 23 | -ms-flex: 1; 24 | flex: 1; 25 | max-width: 300px; 26 | text-decoration: none; 27 | font-family: Titillium Web,sans-serif; 28 | color: #fff; 29 | /*margin-left:20px;*/ 30 | } 31 | .sbu-header-left span a{ 32 | color: #fff; 33 | cursor: pointer; 34 | text-decoration: none; 35 | } 36 | .sbu-header-left i{ 37 | cursor: pointer; 38 | } 39 | .sub-header-left span a:hover{ 40 | text-decoration: none; 41 | } 42 | 43 | .sbu-header-right{ 44 | /*width:20%;*/ 45 | float:right; 46 | vertical-align: middle; 47 | height: 60px; 48 | line-height: 60px; 49 | } 50 | .sbu-header-right select{ 51 | width:300px; 52 | float: right; 53 | /* margin-right: 25px;*/ 54 | margin-top:13px; 55 | } 56 | 57 | .sbu-header-right .sbu-header-group{ 58 | width:350px; 59 | float: right; 60 | /*margin-right: 25px;*/ 61 | top: 50%; 62 | } 63 | 64 | .sbu-param-query{ 65 | /*color: #1a1a1a;*/ 66 | } 67 | 68 | .sbu-param-header{ 69 | /* color: #0d5aa7;*/ 70 | } 71 | .sbu-mul-request-param-header{ 72 | padding:1px 3px; 73 | } 74 | 75 | .sbu-param-body{ 76 | /*color: #7f0055;*/ 77 | } 78 | 79 | .bycdao-left { 80 | width: 310px; 81 | margin-top: 0px; 82 | position: fixed; 83 | /*background: #337ab7;*/ 84 | background: #193248; 85 | /*height: 100%;*/ 86 | transition: all 0.2s; 87 | } 88 | .bycdao-main { 89 | /*margin-left: 320px;*/ 90 | padding-top: 5px; 91 | /* padding-left: 15px;*/ 92 | padding-right: 15px; 93 | transition: all 0.2s; 94 | } 95 | .bycdao-main ul li a i{ 96 | cursor: pointer; 97 | } 98 | 99 | .tab-content { 100 | /* border: 1px solid #c5d0dc; */ 101 | /* padding: 16px 12px; */ 102 | position: relative; 103 | z-index: 11; 104 | } 105 | .nav-list { 106 | margin: 0 1 0 0; 107 | padding: 0; 108 | list-style: none; 109 | } 110 | .nav-list>li { 111 | display: block; 112 | padding: 0; 113 | margin: 0; 114 | border: 0; 115 | border-top: 1px solid #fcfcfc; 116 | border-bottom: 1px solid #e5e5e5; 117 | position: relative; 118 | 119 | white-space: nowrap; 120 | word-break: break-all; 121 | text-overflow: ellipsis; 122 | overflow: hidden; 123 | } 124 | 125 | .nav-list>li.active { 126 | background-color: #fff; 127 | } 128 | 129 | .nav-list>li>ul>li.active{ 130 | background-color: #eeeeee; 131 | } 132 | 133 | .nav-list>li.active>a, .nav-list>li.active>a:hover, .nav-list>li.active>a:focus, .nav-list>li.active>a:active { 134 | background-color: #fff; 135 | color: #2b7dbc; 136 | font-weight: bold; 137 | font-size: 13px; 138 | } 139 | 140 | .nav-list>li>a { 141 | display: block; 142 | height: 38px; 143 | line-height: 36px; 144 | padding: 0 16px 0 7px; 145 | background-color: #f9f9f9; 146 | color: #585858; 147 | text-shadow: none!important; 148 | font-size: 13px; 149 | text-decoration: none; 150 | } 151 | .nav-list>li>a, .nav-list .nav-header { 152 | margin: 0; 153 | } 154 | 155 | .nav-list>li.open>a { 156 | background-color: #fafafa; 157 | color: #1963aa; 158 | } 159 | 160 | .nav-list>li>a>[class*="icon-"]:first-child { 161 | display: inline-block; 162 | vertical-align: unset; 163 | min-width: 30px; 164 | text-align: center; 165 | font-size: 18px; 166 | font-weight: normal; 167 | margin-right: 2px; 168 | } 169 | .nav-list>li a>.arrow { 170 | display: inline-block; 171 | width: 14px!important; 172 | height: 14px; 173 | line-height: 14px; 174 | text-shadow: none; 175 | font-size: 18px; 176 | position: absolute; 177 | right: 11px; 178 | top: 11px; 179 | padding: 0; 180 | color: #666; 181 | } 182 | .nav-list a .badge, .nav-list a .label { 183 | font-size: 12px; 184 | padding-left: 6px; 185 | padding-right: 6px; 186 | position: absolute; 187 | top: 9px; 188 | right: 11px; 189 | opacity: .88; 190 | } 191 | 192 | .label-primary, .badge-primary { 193 | background-color: #428bca!important; 194 | } 195 | .badge { 196 | text-shadow: none; 197 | font-size: 12px; 198 | padding-top: 1px; 199 | padding-bottom: 3px; 200 | font-weight: normal; 201 | line-height: 15px; 202 | 203 | } 204 | .badge-primary, .label-primary { 205 | background-color: #1ab394; 206 | } 207 | .nav-list>li>.submenu:before { 208 | content: ""; 209 | display: block; 210 | position: absolute; 211 | z-index: 1; 212 | left: 18px; 213 | top: 0; 214 | bottom: 0; 215 | border: 1px dotted #9dbdd6; 216 | border-width: 0 0 0 1px; 217 | } 218 | .nav-list li .submenu { 219 | overflow: hidden; 220 | } 221 | 222 | .nav-list>li.active:after { 223 | display: inline-block; 224 | content: ""; 225 | position: absolute; 226 | right: -2px; 227 | top: -1px; 228 | bottom: 0; 229 | z-index: 1; 230 | border: 2px solid #2b7dbc; 231 | border-width: 0 2px 0 0; 232 | } 233 | .nav-list>li .submenu { 234 | display: none; 235 | list-style: none; 236 | margin: 0; 237 | padding: 0; 238 | position: relative; 239 | background-color: #fff; 240 | border-top: 1px solid #e5e5e5; 241 | } 242 | 243 | .nav-list>li .submenu>li { 244 | margin-left: 0; 245 | position: relative; 246 | } 247 | .nav-list>li>.submenu>li:before { 248 | /*content: "";*/ 249 | display: inline-block; 250 | position: absolute; 251 | width: 7px; 252 | left: 20px; 253 | top: 23px; 254 | border-top: 1px dotted #9dbdd6; 255 | } 256 | 257 | li [class^="icon-"], li [class*=" icon-"], .nav-list li [class^="icon-"], .nav-list li [class*=" icon-"] { 258 | width: auto; 259 | } 260 | .nav-tabs>li>a:hover { 261 | /* background-color: #FFF; */ 262 | /* color: #4c8fbd; */ 263 | /* border-color: #c5d0dc; */ 264 | } 265 | .menuLi{ 266 | /*border: 1px solid #f3f3f4;*/ 267 | border-top: 1px solid #e3e3ec; 268 | padding: 1px 2px; 269 | margin-bottom: 2px; 270 | margin-top: 3px; 271 | } 272 | .menuLi .mhed{ 273 | cursor: pointer; 274 | padding-left: 30px; 275 | font-size: 12px; 276 | } 277 | .menuLidoc{ 278 | /*border: 1px solid #f3f3f4;*/ 279 | border-top: 1px solid #e3e3ec; 280 | padding: 1px 2px; 281 | margin-bottom: 2px; 282 | margin-top: 3px; 283 | } 284 | .menuLidoc .mhed{ 285 | cursor: pointer; 286 | padding-left: 30px; 287 | font-size: 12px; 288 | } 289 | code { 290 | padding: 2px 4px; 291 | /*font-size: 90%;*/ 292 | color: #ab0f3a; 293 | background-color: #fff; 294 | border-radius: 4px; 295 | } 296 | 297 | 298 | .swu-left{ 299 | float: left; 300 | } 301 | 302 | .swu-menu{ 303 | display: block; 304 | width: 54px; 305 | white-space: nowrap; 306 | font-family: Monaco; 307 | /*font-weight: bold;*/ 308 | /* font-size: 12px; */ 309 | } 310 | .swu-menu-api-des{ 311 | /* font-size: 12px; */ 312 | font-family: Monaco; 313 | color: #ab0f3a; 314 | height: 21px; 315 | } 316 | 317 | .swu-menu-api-des span{ 318 | position: absolute; 319 | bottom: 0px; 320 | padding: 0px; 321 | margin: 0px; 322 | white-space: nowrap; 323 | } 324 | .swu-hei{ 325 | line-height: 21px; 326 | height: 21px; 327 | } 328 | .swu-hei-none-url{ 329 | line-height: 35px; 330 | height: 30px; 331 | } 332 | .swu-wd-20{ 333 | width: 20px; 334 | } 335 | 336 | .widget{ 337 | border-radius: 5px; 338 | padding: 15px 20px; 339 | margin-bottom: 10px; 340 | margin-top: 10px; 341 | } 342 | 343 | .navy-bg { 344 | background-color: #2e5db7; 345 | color: #fff; 346 | } 347 | 348 | .font-bold { 349 | font-weight: bold; 350 | margin-bottom: 5px; 351 | } 352 | 353 | .jsonview .obj{ 354 | margin-top: 1px; 355 | margin-bottom: 1px; 356 | } 357 | 358 | 359 | .bar8 form { 360 | position: relative; 361 | width: 300px; 362 | /*margin: 0 auto;*/ 363 | float: right; 364 | } 365 | 366 | .bar8 input, button { 367 | border: none; 368 | outline: none; 369 | } 370 | 371 | .bar8 input { 372 | width: 50%; 373 | height: 42px; 374 | /* border: 1px solid red; */ 375 | /* padding-left: 13px; */ 376 | margin-right: 37px; 377 | margin-top: 3px; 378 | } 379 | 380 | .bar8 button { 381 | height: 42px; 382 | width: 42px; 383 | cursor: pointer; 384 | position: absolute; 385 | } 386 | 387 | /*搜索框8*/ 388 | .bar8 {background: #4887bd;} 389 | .bar8 form { 390 | height: 42px; 391 | } 392 | .bar8 input { 393 | /* width: 0; */ 394 | /* padding: 17px 0px 0 17px; 395 | !* border-bottom: 2px solid transparent; *! 396 | background: transparent; 397 | transition: .3s linear; 398 | position: absolute; 399 | top: 0; 400 | right: 0; 401 | z-index: 2;*/ 402 | 403 | /* width: 0; */ 404 | padding: 5px 0px 0 17px; 405 | /* border-bottom: 2px solid transparent; */ 406 | background: transparent; 407 | transition: .3s linear; 408 | position: absolute; 409 | top: 11px; 410 | right: 0; 411 | z-index: 2; 412 | height: 30px; 413 | } 414 | /*.bar8 input:focus { 415 | width: 300px; 416 | z-index: 1; 417 | !*border-bottom: 2px solid #F9F0DA;*! 418 | }*/ 419 | .bar8 button { 420 | 421 | top: 0; 422 | right: 0; 423 | cursor: pointer; 424 | } 425 | .bar8 button:before { 426 | content: "\f002"; 427 | font-family: FontAwesome; 428 | font-size: 16px; 429 | color: #F9F0DA; 430 | } 431 | .bar8 input::-webkit-input-placeholder{ 432 | color:white; 433 | } 434 | .bar8 input::-moz-placeholder{ /* Mozilla Firefox 19+ */ 435 | color:white; 436 | } 437 | .bar8 input:-moz-placeholder{ /* Mozilla Firefox 4 to 18 */ 438 | color:white; 439 | } 440 | .bar8 input:-ms-input-placeholder{ /* Internet Explorer 10-11 */ 441 | color:white; 442 | } 443 | .bar8 span{ 444 | cursor: pointer; 445 | display: inherit; 446 | /* border: 1px solid red; */ 447 | width: 37px; 448 | float: right; 449 | } 450 | 451 | 452 | .menu-url{ 453 | background-color: transparent; 454 | /* color: #ab0f3a; */ 455 | } 456 | 457 | .menu-url-post{ 458 | color: #61affd; 459 | text-align: left; 460 | /* width: 64px; */ 461 | display: inline-block; 462 | font-weight: bold; 463 | } 464 | .menu-url-put{ 465 | color: #fca130; 466 | text-align: left; 467 | /* width: 64px; */ 468 | display: inline-block; 469 | font-weight: bold; 470 | } 471 | .menu-url-get{ 472 | color: #0d5aa7; 473 | text-align: left; 474 | /* width: 64px; */ 475 | display: inline-block; 476 | font-weight: bold; 477 | } 478 | .menu-url-head{ 479 | color: #9012fe; 480 | text-align: left; 481 | /* width: 64px; */ 482 | display: inline-block; 483 | font-weight: bold; 484 | } 485 | .menu-url-delete{ 486 | color:#f93e3e; 487 | text-align: left; 488 | width: 64px; 489 | display: inline-block; 490 | font-weight: bold; 491 | } 492 | .menu-url-patch{ 493 | color: #50e3c2; 494 | text-align: left; 495 | /* width: 64px; */ 496 | display: inline-block; 497 | font-weight: bold; 498 | } 499 | .menu-url-options{ 500 | color: #49cc90; 501 | text-align: left; 502 | /* width: 49px; */ 503 | display: inline-block; 504 | font-weight: bold; 505 | } 506 | 507 | .debug-span-label{ 508 | color:#919191; 509 | } 510 | 511 | .debug-span-value{ 512 | color:#4dc095; 513 | font-size: 12px; 514 | font-weight: bold; 515 | } 516 | .btn-add-div{ 517 | margin-top: 5px; 518 | } 519 | .btn-add-string{ 520 | margin-top: 2px; 521 | } 522 | 523 | #sbu-dynamic-tab ul li{ 524 | white-space: nowrap; 525 | } 526 | .swbu-main .sbu-api-title{ 527 | font-weight: bold; 528 | width: 65px; 529 | /* border: 1px solid red; */ 530 | display: inline-block; 531 | 532 | } 533 | .sbu-debug-input-true{ 534 | border-color: #e5b2b1; 535 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); 536 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075); 537 | } 538 | .sbu-request-query{ 539 | color: #61affe; 540 | } 541 | .sbu-request-body{ 542 | color: #0d5aa7; 543 | } 544 | .sbu-request-formData{ 545 | color: #9012fe; 546 | } 547 | .sbu-request-form{ 548 | color: #9012fe; 549 | } 550 | .sbu-request-validate-jsr{ 551 | color: #10af88; 552 | border-bottom: 1px dashed #10af88; 553 | } 554 | 555 | .sbu-request-header{ 556 | color: #fca130; 557 | } 558 | .sbu-request-path{ 559 | color: #49cc90; 560 | } 561 | 562 | .sbu-tag-description{ 563 | font-size:10px; 564 | } 565 | .sbu-debug-content-type{ 566 | margin-top: 15px; 567 | height: 20px; 568 | line-height: 20px; 569 | } 570 | .sbu-debug-content-type-button{ 571 | background-color: white; 572 | color: #df4646; 573 | } 574 | 575 | .sbu-api-new-flag-icon{ 576 | position: absolute;font-size:26px; 577 | } 578 | /**离线文档*/ 579 | .offlineMarkdownShow{ 580 | 581 | } 582 | 583 | .offlineMarkdownShow h1, h2, h3, h4, h5, h6 { 584 | font-family: 'Old Standard TT', serif; 585 | font-weight: bold; 586 | } 587 | 588 | .offlineMarkdownShow h1{ 589 | font-size: 40px; 590 | } 591 | 592 | .offlineMarkdownShow h2{ 593 | font-size: 36px; 594 | } 595 | 596 | .offlineMarkdownShow h3{ 597 | font-size: 34px; 598 | } 599 | 600 | .offlineMarkdownShow h4{ 601 | font-size: 32px; 602 | } 603 | 604 | .offlineMarkdownShow p { 605 | font-family: inherit; 606 | font-size: 1rem; 607 | font-weight: normal; 608 | line-height: 1.6; 609 | margin-bottom: 1.25rem; 610 | text-rendering: optimizeLegibility; 611 | } 612 | 613 | .offlineMarkdownShow strong, b { 614 | font-weight: bold; 615 | line-height: inherit; 616 | } 617 | 618 | .offlineMarkdownShow table { 619 | background: #fff; 620 | border: solid 1px #ddd; 621 | margin-bottom: 1.25rem; 622 | table-layout: auto; 623 | width: 90%; 624 | } 625 | .offlineMarkdownShow table thead { 626 | background: #F5F5F5; 627 | } 628 | 629 | .offlineMarkdownShow table tbody { 630 | display: table-row-group; 631 | vertical-align: middle; 632 | border-color: inherit; 633 | } 634 | 635 | .offlineMarkdownShow table tr { 636 | display: table-row; 637 | vertical-align: inherit; 638 | border-color: inherit; 639 | } 640 | 641 | .offlineMarkdownShow table thead tr th, table tfoot tr th, table tfoot tr td, table tbody tr th, table tbody tr td, table tr td { 642 | display: table-cell; 643 | line-height: 1.125rem; 644 | } 645 | .offlineMarkdownShow table tr th, table tr td { 646 | color: #222; 647 | font-size: 0.875rem; 648 | padding: 0.5625rem 0.625rem; 649 | text-align: left; 650 | } 651 | 652 | /*导航*/ 653 | .BlogAnchor { 654 | background: #f1f1f1; 655 | padding: 10px; 656 | line-height: 180%; 657 | position: fixed; 658 | right: 20px; 659 | top: 110px; 660 | border: 1px solid #aaaaaa; 661 | } 662 | .BlogAnchor p { 663 | font-size: 18px; 664 | color: #15a230; 665 | margin: 0 0 0.3rem 0; 666 | text-align: right; 667 | } 668 | .BlogAnchor .AnchorContent { 669 | padding: 5px 0px; 670 | overflow: auto; 671 | } 672 | .BlogAnchor li{ 673 | text-indent: 0.5rem; 674 | font-size: 14px; 675 | list-style: none; 676 | } 677 | .BlogAnchor li .nav_item{ 678 | padding: 3px; 679 | } 680 | .BlogAnchor li .item_h1{ 681 | margin-left: 0rem; 682 | } 683 | .BlogAnchor li .item_h2{ 684 | margin-left: 2rem; 685 | font-size: 0.8rem; 686 | } 687 | .BlogAnchor li .nav_item.current{ 688 | color: white; 689 | background-color: #5cc26f; 690 | } 691 | #AnchorContentToggle { 692 | font-size: 13px; 693 | font-weight: normal; 694 | color: #FFF; 695 | display: inline-block; 696 | line-height: 20px; 697 | background: #5cc26f; 698 | font-style: normal; 699 | padding: 1px 8px; 700 | } 701 | .BlogAnchor a:hover { 702 | color: #5cc26f; 703 | } 704 | .BlogAnchor a { 705 | text-decoration: none; 706 | } 707 | 708 | /*额外添加*/ 709 | .swu-menu { 710 | display: block; 711 | width: 54px; 712 | white-space: nowrap; 713 | font-family: Monaco; 714 | /* font-weight: bold; */ 715 | font-size: 14px; 716 | } 717 | 718 | .layui-layout-admin .layui-body .layui-tab .layui-tab-title li.layui-this, .layui-layout-admin .layui-body .layui-tab .layui-tab-title li:hover { 719 | background: red; 720 | } 721 | 722 | .offlineMarkdownShow table tr th, table tr td { 723 | color: #003665; 724 | } 725 | 726 | .layui-table td, .layui-table th { 727 | font-size: 16px; 728 | } 729 | 730 | .sbu-header{ 731 | background-color: #193248; 732 | } --------------------------------------------------------------------------------